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
+81
View File
@@ -0,0 +1,81 @@
"""Tests for sentiment-collection scoping (``_get_sentiment_priority_tickers``).
The activation gate qualifies setups on 12-1 momentum percentile, a different
axis than composite score. These tests pin the fix that adds the gate's momentum
leaders to the sentiment relevant-set so a freshly-qualifying ticker isn't left
without sentiment.
"""
from __future__ import annotations
from datetime import date, datetime, timedelta, timezone
import pytest
from app import scheduler
from app.models.ohlcv import OHLCVRecord
from app.models.settings import SystemSetting
from app.models.ticker import Ticker
@pytest.fixture
async def session():
from tests.conftest import _test_session_factory
async with _test_session_factory() as s:
yield s
async def _seed_history(session, symbol: str, rate: float, n: int = 280) -> Ticker:
"""Seed a ticker with a full year+ of daily closes growing at ``rate``."""
t = Ticker(symbol=symbol)
session.add(t)
await session.flush()
base = date(2024, 1, 1)
for i in range(n):
close = 100.0 * (rate ** i)
session.add(OHLCVRecord(
ticker_id=t.id,
date=base + timedelta(days=i),
open=close, high=close, low=close, close=close,
volume=1_000_000,
))
await session.commit()
return t
async def _set_min_momentum(session, value: str) -> None:
session.add(SystemSetting(
key="activation_min_momentum_percentile",
value=value,
updated_at=datetime.now(timezone.utc),
))
await session.commit()
async def test_momentum_leader_is_included_without_composite_or_watchlist(session):
"""A top-percentile momentum ticker is fetched even when it has no composite
score, no watchlist entry, and no open trade — the case that previously left
qualifying setups with no sentiment."""
await _seed_history(session, "LEADER", rate=1.010) # strong uptrend → pct 100
await _seed_history(session, "LAGGARD", rate=0.999) # declining → pct 0
await _set_min_momentum(session, "80")
symbols = await scheduler._get_sentiment_priority_tickers(session)
assert "LEADER" in symbols
# Below the gate's percentile and not otherwise relevant → not fetched.
assert "LAGGARD" not in symbols
async def test_momentum_leaders_skipped_when_gate_disabled(session):
"""With the momentum gate off (min percentile 0), the leader is no longer
pulled in solely on momentum — scoping falls back to the base relevant set."""
await _seed_history(session, "LEADER", rate=1.010)
await _seed_history(session, "LAGGARD", rate=0.999)
await _set_min_momentum(session, "0")
symbols = await scheduler._get_sentiment_priority_tickers(session)
assert "LEADER" not in symbols
assert "LAGGARD" not in symbols