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
-34
View File
@@ -16,15 +16,6 @@ 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":
@@ -55,19 +46,6 @@ 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.
@@ -124,18 +102,6 @@ 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