fix: scope sentiment collection to the gate's momentum leaders
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 42s
Deploy / deploy (push) Successful in 25s

Qualified setups could carry no sentiment because the sentiment job scoped
its relevant-set to watchlist + open trades + top-N composite score, while
the activation gate qualifies on 12-1 momentum percentile — a different axis.
A top-momentum ticker outside the composite top-N never got sentiment, so the
R:R scan enhanced it as neutral.

Add the gate's momentum leaders (percentile >= activation min_momentum_percentile)
to the sentiment relevant-set so scope tracks the gate. Best-effort: a momentum
or config failure falls back to the base set rather than aborting collection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 12:06:52 +02:00
parent 437ceacfc1
commit 5605915d45
2 changed files with 110 additions and 3 deletions
+29 -3
View File
@@ -218,9 +218,10 @@ async def _get_sentiment_priority_tickers(db: AsyncSession) -> list[str]:
"""Symbols to fetch sentiment for, budgeted to stay in the free search tier.
Scope: only tickers that matter — watchlist + open paper trades + top-N by
composite score. Skip any refreshed within ``sentiment_fresh_hours``. Cap the
run at ``sentiment_max_per_run``, oldest/missing first. Once the relevant set
is fresh, runs make zero grounded searches until it ages out.
composite score + the momentum leaders the activation gate qualifies on. Skip
any refreshed within ``sentiment_fresh_hours``. Cap the run at
``sentiment_max_per_run``, oldest/missing first. Once the relevant set is
fresh, runs make zero grounded searches until it ages out.
"""
from app.models.paper_trade import PaperTrade
from app.models.score import CompositeScore
@@ -244,6 +245,31 @@ async def _get_sentiment_priority_tickers(db: AsyncSession) -> list[str]:
)
relevant.update(r[0] for r in top.all())
# Momentum leaders: the tickers that can clear the activation gate, which
# selects the top ``min_momentum_percentile`` slice by 12-1 momentum — a
# different axis than composite score. The gate qualifies setups on this
# percentile, so without including them a freshly-qualifying ticker carries no
# sentiment and gets enhanced as neutral. Pre-fetching their sentiment here (in
# the daily pipeline, sentiment runs right after the OHLCV refresh) means the
# following R:R scan reads real sentiment for the setups it qualifies.
# Best-effort: a momentum/config failure must not stop sentiment collection.
try:
from app.services import momentum_service
from app.services.admin_service import get_activation_config
activation = await get_activation_config(db)
min_pct = float(activation.get("min_momentum_percentile", 0.0))
if min_pct > 0:
percentiles = await momentum_service.compute_momentum_percentiles(db)
leaders = [sym for sym, pct in percentiles.items() if pct >= min_pct]
if leaders:
rows = await db.execute(
select(Ticker.id).where(Ticker.symbol.in_(leaders))
)
relevant.update(r[0] for r in rows.all())
except Exception:
logger.exception("Sentiment momentum-leader scoping failed; using base relevant set")
if not relevant:
return []