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.
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
"""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,
|
||||
}
|
||||
Reference in New Issue
Block a user