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:
@@ -16,6 +16,15 @@ from typing import Any
|
||||
|
||||
HIGH_CONVICTION_ACTIONS = {"LONG_HIGH", "SHORT_HIGH"}
|
||||
|
||||
# A projected (blue-sky) target has no S/R validation — it is a measured-move
|
||||
# extension used when nothing sits overhead. Because that is exactly the kind of
|
||||
# unvalidated target the gate exists to distrust, a projected setup clears a
|
||||
# STRICTER bar than an S/R-anchored one, regardless of whether the general
|
||||
# momentum gate is active: long-only (breakout continuation), strong residual
|
||||
# momentum, and a higher confidence floor. Mirrored in frontend/src/lib/qualification.ts.
|
||||
PROJECTED_MIN_MOMENTUM_PERCENTILE = 90.0
|
||||
PROJECTED_CONFIDENCE_MARGIN = 10.0
|
||||
|
||||
|
||||
def _action_direction(action: str | None) -> str:
|
||||
if not action or action == "NEUTRAL":
|
||||
@@ -46,6 +55,19 @@ def primary_target_probability(setup: Any) -> float | None:
|
||||
return best if best > 0 else None
|
||||
|
||||
|
||||
def primary_target_is_projected(setup: Any) -> bool:
|
||||
"""Whether the setup's headline target is a blue-sky measured-move projection.
|
||||
|
||||
Prefers the starred primary; falls back to any projected target when none is
|
||||
explicitly flagged primary (matches primary_target_probability's fallback).
|
||||
"""
|
||||
targets = getattr(setup, "targets", None) or []
|
||||
for target in targets:
|
||||
if isinstance(target, dict) and target.get("is_primary"):
|
||||
return bool(target.get("projected"))
|
||||
return any(isinstance(t, dict) and t.get("projected") for t in targets)
|
||||
|
||||
|
||||
def live_risk_reward(setup: Any, current_price: float) -> float | None:
|
||||
"""R:R recomputed from the CURRENT price, not the (possibly stale) entry.
|
||||
|
||||
@@ -102,6 +124,18 @@ def setup_qualifies(setup: Any, config: dict) -> bool:
|
||||
momentum_percentile = getattr(setup, "momentum_percentile", None)
|
||||
if momentum_percentile is None or momentum_percentile < min_pct:
|
||||
return False
|
||||
# Projected (blue-sky) targets clear a stricter bar than S/R-anchored ones,
|
||||
# independent of the general momentum gate above: long-only, strong residual
|
||||
# momentum, and a higher confidence floor. The target has no S/R validation,
|
||||
# so we only trust it for high-momentum breakout continuations.
|
||||
if primary_target_is_projected(setup):
|
||||
if (getattr(setup, "direction", "long") or "long").lower() != "long":
|
||||
return False
|
||||
momentum_percentile = getattr(setup, "momentum_percentile", None)
|
||||
if momentum_percentile is None or momentum_percentile < PROJECTED_MIN_MOMENTUM_PERCENTILE:
|
||||
return False
|
||||
if (setup.confidence_score or 0.0) < config["min_confidence"] + PROJECTED_CONFIDENCE_MARGIN:
|
||||
return False
|
||||
# A setup is actionable only when the live ticker action points in the same
|
||||
# direction. NEUTRAL means no clear signal; an opposite action means the
|
||||
# setup is counter-bias. ``exclude_neutral`` defaults on; callers that omit
|
||||
|
||||
Reference in New Issue
Block a user