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:
2026-07-08 22:03:25 +02:00
co-authored by Claude Opus 4.8
parent 294d935030
commit 65d2dae62a
8 changed files with 25 additions and 416 deletions
-24
View File
@@ -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);