Serve live recommendation context on trade setup APIs and alerts
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 1m0s
Deploy / deploy (push) Successful in 32s

Stored TradeSetup rows are point-in-time snapshots from the RR scan, so
the ticker page could show stale confidence/reasoning/composite (e.g.
sentiment=neutral in the setup card while the sentiment panel showed
bullish). Overlay current score/sentiment context onto the API payload
for GET /trades and GET /trades/{symbol}, gate and format Telegram
qualified-setup alerts on the same live values, and apply the
min_confidence/recommended_action filters after the overlay so they
judge what the caller actually sees. Stored setups stay frozen for
outcome analysis and backtests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 09:17:27 +02:00
parent 2b0068ae08
commit ac51e23949
5 changed files with 348 additions and 34 deletions
+138 -1
View File
@@ -21,7 +21,8 @@ from app.models.ohlcv import OHLCVRecord
from app.models.sr_level import SRLevel
from app.models.ticker import Ticker
from app.models.trade_setup import TradeSetup
from app.models.score import CompositeScore
from app.models.score import CompositeScore, DimensionScore
from app.models.sentiment import SentimentScore
from app.services.rr_scanner_service import scan_ticker, get_trade_setups
@@ -431,3 +432,139 @@ async def test_get_trade_setups_sorting_rr_desc_composite_desc(db_session: Async
f"Expected symbol order ['SORTD', 'SORTC', 'SORTB', 'SORTA'], "
f"got {symbols}"
)
async def _seed_stale_setup_with_current_scores(db_session: AsyncSession) -> TradeSetup:
"""Stored setup frozen at scan time (conf 82, neutral) vs. current context
(bullish sentiment, composite 96) that yields live confidence 97."""
old_scan = datetime(2026, 7, 1, tzinfo=timezone.utc)
current = datetime(2026, 7, 3, tzinfo=timezone.utc)
old_reasoning = (
"LONG (high confidence): 82% with aligned signals "
"(technical=88, momentum=60, sentiment=neutral)."
)
ticker = Ticker(symbol="TTWO")
db_session.add(ticker)
await db_session.flush()
stale_setup = TradeSetup(
ticker_id=ticker.id,
direction="long",
entry_price=235.0,
stop_loss=220.0,
target=265.0,
rr_ratio=2.0,
composite_score=71.8,
detected_at=old_scan,
confidence_score=82.0,
recommended_action="LONG_HIGH",
reasoning=old_reasoning,
risk_level="High",
)
db_session.add(stale_setup)
db_session.add_all([
DimensionScore(
ticker_id=ticker.id,
dimension="technical",
score=88.0,
is_stale=False,
computed_at=current,
),
DimensionScore(
ticker_id=ticker.id,
dimension="momentum",
score=60.0,
is_stale=False,
computed_at=current,
),
DimensionScore(
ticker_id=ticker.id,
dimension="fundamental",
score=95.0,
is_stale=False,
computed_at=current,
),
DimensionScore(
ticker_id=ticker.id,
dimension="sentiment",
score=85.0,
is_stale=False,
computed_at=current,
),
CompositeScore(
ticker_id=ticker.id,
score=96.0,
is_stale=False,
weights_json="{}",
computed_at=current,
),
SentimentScore(
ticker_id=ticker.id,
classification="bullish",
confidence=85,
source="test",
timestamp=current,
reasoning="",
citations_json="[]",
),
])
await db_session.flush()
return stale_setup
@pytest.mark.asyncio
async def test_live_recommendation_payload_uses_current_score_and_sentiment(
db_session: AsyncSession,
):
"""Latest setup payload should not show stale scan text when score context moved."""
stale_setup = await _seed_stale_setup_with_current_scores(db_session)
old_reasoning = stale_setup.reasoning
rows = await get_trade_setups(
db_session,
symbol="TTWO",
live_recommendation=True,
)
assert len(rows) == 1
row = rows[0]
assert row["composite_score"] == pytest.approx(96.0)
assert row["confidence_score"] == pytest.approx(97.0)
assert row["recommended_action"] == "LONG_HIGH"
assert "sentiment=bullish" in row["reasoning"]
assert "sentiment=neutral" not in row["reasoning"]
persisted = await db_session.get(TradeSetup, stale_setup.id)
assert persisted is not None
assert persisted.composite_score == pytest.approx(71.8)
assert persisted.reasoning == old_reasoning
@pytest.mark.asyncio
async def test_live_recommendation_filters_apply_to_live_values(
db_session: AsyncSession,
):
"""min_confidence must judge the overlaid live confidence, not the stored one."""
await _seed_stale_setup_with_current_scores(db_session)
# Stored confidence is 82 — a stored-column filter would drop this row.
# Live confidence is 97, so it must pass.
rows = await get_trade_setups(
db_session,
symbol="TTWO",
min_confidence=90.0,
live_recommendation=True,
)
assert len(rows) == 1
assert rows[0]["confidence_score"] == pytest.approx(97.0)
# And a floor above the live value must drop it.
rows = await get_trade_setups(
db_session,
symbol="TTWO",
min_confidence=98.0,
live_recommendation=True,
)
assert rows == []