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:
@@ -29,6 +29,7 @@ from app.models.trade_setup import TradeSetup
|
||||
from app.services.indicator_service import _extract_ohlcv, compute_atr
|
||||
from app.services.price_service import query_ohlcv
|
||||
from app.services.recommendation_service import (
|
||||
PROJECTED_TARGET_ATR_MULTIPLE,
|
||||
_risk_level_from_conflicts,
|
||||
build_recommendation_snapshot,
|
||||
enhance_trade_setup,
|
||||
@@ -67,6 +68,16 @@ def _compute_quality_score(
|
||||
return w_rr * norm_rr + w_strength * norm_strength + w_proximity * norm_proximity
|
||||
|
||||
|
||||
def _projected_target(direction: str, entry_price: float, atr_value: float) -> float:
|
||||
"""Measured-move target for a blue-sky direction (no overhead S/R).
|
||||
|
||||
Mirrors the projection in recommendation_service so the scanner's emission
|
||||
decision and the enhanced target agree.
|
||||
"""
|
||||
move = PROJECTED_TARGET_ATR_MULTIPLE * atr_value
|
||||
return entry_price + move if direction == "long" else entry_price - move
|
||||
|
||||
|
||||
async def _get_dimension_scores(db: AsyncSession, ticker_id: int) -> dict[str, float]:
|
||||
result = await db.execute(
|
||||
select(DimensionScore).where(DimensionScore.ticker_id == ticker_id)
|
||||
@@ -428,13 +439,13 @@ async def scan_ticker(
|
||||
now = datetime.now(timezone.utc)
|
||||
setups: list[TradeSetup] = []
|
||||
|
||||
if levels_above:
|
||||
stop = entry_price - (atr_value * atr_multiplier)
|
||||
risk = entry_price - stop
|
||||
if risk > 0:
|
||||
stop = entry_price - (atr_value * atr_multiplier)
|
||||
risk = entry_price - stop
|
||||
if risk > 0:
|
||||
best_candidate_rr = 0.0
|
||||
best_candidate_target = 0.0
|
||||
if levels_above:
|
||||
best_quality = 0.0
|
||||
best_candidate_rr = 0.0
|
||||
best_candidate_target = 0.0
|
||||
for lv in levels_above:
|
||||
reward = lv.price_level - entry_price
|
||||
if reward <= 0:
|
||||
@@ -448,21 +459,29 @@ async def scan_ticker(
|
||||
best_quality = quality
|
||||
best_candidate_rr = rr
|
||||
best_candidate_target = lv.price_level
|
||||
else:
|
||||
# Blue-sky: no resistance overhead. Project a measured-move target so
|
||||
# a breakout name still yields a setup (it faces a stricter gate).
|
||||
projected = _projected_target("long", entry_price, atr_value)
|
||||
projected_rr = (projected - entry_price) / risk
|
||||
if projected_rr >= rr_threshold:
|
||||
best_candidate_rr = projected_rr
|
||||
best_candidate_target = projected
|
||||
|
||||
if best_candidate_rr > 0:
|
||||
setups.append(TradeSetup(
|
||||
ticker_id=ticker.id,
|
||||
direction="long",
|
||||
entry_price=round(entry_price, 4),
|
||||
stop_loss=round(stop, 4),
|
||||
target=round(best_candidate_target, 4),
|
||||
rr_ratio=round(best_candidate_rr, 4),
|
||||
composite_score=round(composite_score, 4),
|
||||
detected_at=now,
|
||||
momentum_percentile=momentum_percentile,
|
||||
strategy_rank=strategy_rank,
|
||||
volatility_percentile=volatility_percentile,
|
||||
))
|
||||
if best_candidate_rr > 0:
|
||||
setups.append(TradeSetup(
|
||||
ticker_id=ticker.id,
|
||||
direction="long",
|
||||
entry_price=round(entry_price, 4),
|
||||
stop_loss=round(stop, 4),
|
||||
target=round(best_candidate_target, 4),
|
||||
rr_ratio=round(best_candidate_rr, 4),
|
||||
composite_score=round(composite_score, 4),
|
||||
detected_at=now,
|
||||
momentum_percentile=momentum_percentile,
|
||||
strategy_rank=strategy_rank,
|
||||
volatility_percentile=volatility_percentile,
|
||||
))
|
||||
|
||||
if levels_below:
|
||||
stop = entry_price + (atr_value * atr_multiplier)
|
||||
|
||||
Reference in New Issue
Block a user