Add blue-sky projected targets and played-out setup UX
Fixes stale below-price setups showing as current recommendations. Three distinct causes share the symptom (get_trade_setups returns the latest stored setup per direction and never expires it): - Genuine blue-sky (no overhead S/R): scanner + TargetGenerator now project a measured-move target (entry +/- 3*ATR, ~2:1 R:R), flagged projected with a low sr_strength probability haircut. Overhead check keys on level tag OR price so it never projects through a straddling resistance cluster. - Projected targets clear a stricter activation bar (long-only, momentum >= 90, confidence >= min+10), independent of the general momentum gate. Mirrored in frontend qualification.ts. - Played-out UX (fixes the reported TTWO case, which is R:R-starved under a resistance cluster, not blue-sky): when price is at/past target or through the stop, RecommendationPanel shows a "No current setup" state and softens the stale ticker-level header/reasoning, instead of a stale actionable card. No migration: the projected flag rides in existing targets_json. 504 backend unit tests pass; frontend typechecks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,20 @@ 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';
|
||||
@@ -51,6 +65,16 @@ 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,6 +577,8 @@ 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