Revert blue-sky projection; keep played-out setup UX
A local backtest (offline prod snapshot, 506 tickers) evaluated blue-sky projected targets under the PRODUCTION exit (3x ATR trailing + 30d max hold, paper_trade_service DEFAULT_EXIT_MODE="atr_trailing"). Blue-sky setups are dilutive: the qualified book scored 328% return / Sharpe 1.84 / DD -21.0% WITHOUT them vs 300% / 1.58 / -18.7% WITH them. They rank high on momentum by construction, so they grab slots from S/R setups that catch bigger runs under a trailing-stop exit (only ~2pp worse drawdown doesn't justify the lost return and Sharpe). Reverts the scanner/TargetGenerator measured-move projection, the stricter projected activation gate, the frontend qualification mirror, the `projected` type field, and the projected tests -- all backend files are now byte-identical to the pre-blue-sky commit. Keeps the played-out "No current setup" UX (RecommendationPanel): when price has run past the target (played out) or through the stop (invalidated), the panel shows a plain no-setup state instead of a stale actionable card. This is frontend-only (reads last close + existing setup fields) and is what actually fixes the reported stale-below-price bug -- no backend change or rescan needed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -103,7 +103,6 @@ function TargetTable({ setup }: { setup: TradeSetup }) {
|
||||
<td className="py-2 pr-3 text-gray-300">
|
||||
{target.is_primary && <span className="mr-1 text-blue-300">★</span>}
|
||||
{target.classification}
|
||||
{target.projected && <span className="ml-1 text-sky-400">(projected)</span>}
|
||||
</td>
|
||||
<td className="py-2 pr-3 font-mono text-gray-200">{formatPrice(target.price)}</td>
|
||||
<td className="py-2 pr-3 font-mono text-gray-200">{formatPercent((target.distance_from_entry / setup.entry_price) * 100)}</td>
|
||||
@@ -130,7 +129,6 @@ function SetupCard({ setup, action, currentPrice, risk, regime }: { setup?: Trad
|
||||
const drift = entryDrift(setup, currentPrice);
|
||||
const sizing = positionSize(risk.accountSize, risk.riskPct, setup.entry_price, setup.stop_loss);
|
||||
const counterTrend = regime ? isCounterTrend(setup.direction, regime.label) : false;
|
||||
const primaryProjected = setup.targets?.some((t) => t.is_primary && t.projected) ?? false;
|
||||
|
||||
// When price has run to/past the target (played out) or through the stop
|
||||
// (invalidated), there is no fresh setup — show a plain "no current setup"
|
||||
@@ -205,11 +203,6 @@ function SetupCard({ setup, action, currentPrice, risk, regime }: { setup?: Trad
|
||||
</p>
|
||||
)}
|
||||
|
||||
{primaryProjected && (
|
||||
<p className="text-[11px] text-sky-400">
|
||||
⚑ Blue-sky: no resistance overhead — target is an ATR measured-move projection, not an S/R level.
|
||||
</p>
|
||||
)}
|
||||
{drift && drift.status === 'invalidated' && (
|
||||
<p className="text-[11px] text-red-400">
|
||||
⚠ Price ({formatPrice(currentPrice!)}) is past the stop — this setup is invalidated.
|
||||
|
||||
@@ -2,20 +2,6 @@ import type { ActivationConfig, TradeSetup } from './types';
|
||||
|
||||
const HIGH_CONVICTION_ACTIONS = new Set(['LONG_HIGH', 'SHORT_HIGH']);
|
||||
|
||||
// Projected (blue-sky) targets clear a stricter bar than S/R-anchored ones —
|
||||
// long-only, strong momentum, higher confidence floor. Mirrors the constants in
|
||||
// app/services/qualification.py; keep the two in sync.
|
||||
const PROJECTED_MIN_MOMENTUM_PERCENTILE = 90;
|
||||
const PROJECTED_CONFIDENCE_MARGIN = 10;
|
||||
|
||||
/** Whether the setup's headline target is a blue-sky measured-move projection. */
|
||||
export function primaryTargetIsProjected(setup: TradeSetup): boolean {
|
||||
const targets = setup.targets ?? [];
|
||||
const primary = targets.find((t) => t.is_primary);
|
||||
if (primary) return Boolean(primary.projected);
|
||||
return targets.some((t) => t.projected);
|
||||
}
|
||||
|
||||
function actionDirection(action: TradeSetup['recommended_action']): 'long' | 'short' | 'neutral' {
|
||||
if (!action || action === 'NEUTRAL') return 'neutral';
|
||||
if (action.startsWith('LONG')) return 'long';
|
||||
@@ -65,16 +51,6 @@ export function qualifiesSetup(setup: TradeSetup, config: ActivationConfig): boo
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Projected (blue-sky) targets clear a stricter bar than S/R-anchored ones,
|
||||
// independent of the general momentum gate: long-only, strong momentum, higher
|
||||
// confidence floor. Mirrors app/services/qualification.py.
|
||||
if (primaryTargetIsProjected(setup)) {
|
||||
if (setup.direction !== 'long') return false;
|
||||
if (setup.momentum_percentile == null || setup.momentum_percentile < PROJECTED_MIN_MOMENTUM_PERCENTILE) {
|
||||
return false;
|
||||
}
|
||||
if ((setup.confidence_score ?? 0) < config.min_confidence + PROJECTED_CONFIDENCE_MARGIN) return false;
|
||||
}
|
||||
// NEUTRAL = "no clear setup"; an opposite action means this setup is counter-bias.
|
||||
if (config.exclude_neutral) {
|
||||
const actionDir = actionDirection(setup.recommended_action);
|
||||
|
||||
@@ -577,8 +577,6 @@ export interface TradeTarget {
|
||||
sr_level_id: number;
|
||||
sr_strength: number;
|
||||
is_primary?: boolean;
|
||||
/** Blue-sky measured-move target (no overhead S/R); sr_level_id is -1. */
|
||||
projected?: boolean;
|
||||
}
|
||||
|
||||
export interface RecommendationSummary {
|
||||
|
||||
Reference in New Issue
Block a user