Files
signal-platform/tests/unit/test_activation_rank_resolve.py
dennisthiessen 8db535b889
Deploy / lint (push) Successful in 9s
Deploy / test (push) Successful in 1m20s
Deploy / deploy (push) Successful in 39s
Fix manual refresh dropping qualified ranks and clarify trade UI.
Single-ticker fetch now attaches residual-momentum ranks so setups do not silently fail the activation gate. Exit plan is a timeline, chart labels move left of the price scale, and missing ranks surface explicitly.
2026-07-14 10:10:30 +02:00

152 lines
4.4 KiB
Python

"""Single-ticker activation ranks: manual refresh must attach residual ranks.
A daily ``scan_all`` path ranks the universe and stamps each setup. Manual
``/ingestion/fetch`` re-scans one symbol; without the same stamps the new
setup has ``momentum_percentile=None`` and fails the activation gate.
"""
from __future__ import annotations
from datetime import datetime, timezone
import pytest
from app.models.ticker import Ticker
from app.models.trade_setup import TradeSetup
from app.services import rr_scanner_service
from tests.conftest import _test_session_factory # type: ignore
@pytest.fixture
async def session():
async with _test_session_factory() as s:
yield s
async def test_resolve_ranks_uses_fresh_universe_rank(session, monkeypatch):
session.add(Ticker(symbol="AAA"))
await session.commit()
async def _fake_ranks(db):
return {
"AAA": {
"momentum_percentile": 91.0,
"strategy_rank": 88.5,
"volatility_percentile": 70.0,
}
}
monkeypatch.setattr(
"app.services.momentum_service.compute_activation_ranks",
_fake_ranks,
)
ranks = await rr_scanner_service.resolve_activation_ranks_for_symbol(session, "aaa")
assert ranks["momentum_percentile"] == 91.0
assert ranks["strategy_rank"] == 88.5
assert ranks["volatility_percentile"] == 70.0
async def test_resolve_ranks_falls_back_to_previous_setup(session, monkeypatch):
ticker = Ticker(symbol="BBB")
session.add(ticker)
await session.flush()
session.add(
TradeSetup(
ticker_id=ticker.id,
direction="long",
entry_price=100.0,
stop_loss=95.0,
target=110.0,
rr_ratio=2.0,
composite_score=60.0,
momentum_percentile=84.0,
strategy_rank=80.0,
volatility_percentile=55.0,
detected_at=datetime.now(timezone.utc),
)
)
await session.commit()
async def _empty_ranks(db):
return {}
monkeypatch.setattr(
"app.services.momentum_service.compute_activation_ranks",
_empty_ranks,
)
ranks = await rr_scanner_service.resolve_activation_ranks_for_symbol(session, "BBB")
assert ranks["momentum_percentile"] == 84.0
assert ranks["strategy_rank"] == 80.0
assert ranks["volatility_percentile"] == 55.0
async def test_resolve_ranks_skips_null_prior_setups(session, monkeypatch):
"""Broken prior setups (null percentile) must not block a still-older ranked row."""
ticker = Ticker(symbol="CCC")
session.add(ticker)
await session.flush()
now = datetime.now(timezone.utc)
session.add_all([
TradeSetup(
ticker_id=ticker.id,
direction="long",
entry_price=100.0,
stop_loss=95.0,
target=110.0,
rr_ratio=2.0,
composite_score=60.0,
momentum_percentile=93.0,
strategy_rank=90.0,
volatility_percentile=40.0,
detected_at=now.replace(year=now.year - 1) if now.year > 2000 else now,
),
TradeSetup(
ticker_id=ticker.id,
direction="long",
entry_price=101.0,
stop_loss=96.0,
target=111.0,
rr_ratio=2.1,
composite_score=61.0,
momentum_percentile=None,
strategy_rank=None,
volatility_percentile=None,
detected_at=now,
),
])
await session.commit()
async def _empty_ranks(db):
return {}
monkeypatch.setattr(
"app.services.momentum_service.compute_activation_ranks",
_empty_ranks,
)
ranks = await rr_scanner_service.resolve_activation_ranks_for_symbol(session, "CCC")
assert ranks["momentum_percentile"] == 93.0
assert ranks["strategy_rank"] == 90.0
async def test_resolve_ranks_returns_empty_when_unavailable(session, monkeypatch):
session.add(Ticker(symbol="DDD"))
await session.commit()
async def _boom(db):
raise RuntimeError("ranker down")
monkeypatch.setattr(
"app.services.momentum_service.compute_activation_ranks",
_boom,
)
ranks = await rr_scanner_service.resolve_activation_ranks_for_symbol(session, "DDD")
assert ranks == {
"momentum_percentile": None,
"strategy_rank": None,
"volatility_percentile": None,
}