Files
signal-platform/tests/unit/test_rr_scanner_scan_all.py
T

169 lines
5.0 KiB
Python

"""Tests for scan_all_tickers orchestration: error isolation between phases."""
from __future__ import annotations
import pytest
from datetime import datetime, timezone
from sqlalchemy import select
from app.models.score import CompositeScore, DimensionScore
from app.models.ticker import Ticker
from app.services import rr_scanner_service, scoring_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_scan_proceeds_when_score_refresh_fails(session, monkeypatch):
"""A scoring failure must not skip setup detection for the ticker.
Qualification re-gates on live scores at alert time, so a stale score is
recoverable — a skipped scan is not.
"""
session.add(Ticker(symbol="AAA"))
await session.commit()
async def _boom(db, symbol):
raise RuntimeError("scoring unavailable")
scanned: list[str] = []
primary_floors: list[float] = []
async def _fake_scan_ticker(db, symbol, *args, **kwargs):
scanned.append(symbol)
primary_floors.append(kwargs["primary_min_rr"])
return []
monkeypatch.setattr(scoring_service, "compute_all_dimensions", _boom)
monkeypatch.setattr(rr_scanner_service, "scan_ticker", _fake_scan_ticker)
setups = await rr_scanner_service.scan_all_tickers(session)
assert scanned == ["AAA"]
assert primary_floors == [rr_scanner_service.PRIMARY_TARGET_MIN_RR]
assert setups == []
async def test_scan_marks_scores_stale_when_refresh_fails(session, monkeypatch):
ticker = Ticker(symbol="AAA")
session.add(ticker)
await session.flush()
now = datetime.now(timezone.utc)
session.add_all([
DimensionScore(
ticker_id=ticker.id,
dimension="technical",
score=70.0,
is_stale=False,
computed_at=now,
),
CompositeScore(
ticker_id=ticker.id,
score=70.0,
is_stale=False,
weights_json="{}",
computed_at=now,
),
])
await session.commit()
async def _boom(db, symbol):
raise RuntimeError("scoring unavailable")
async def _fake_scan_ticker(db, symbol, *args, **kwargs):
comp = (
await db.execute(select(CompositeScore).where(CompositeScore.ticker_id == ticker.id))
).scalar_one()
dimensions = (
await db.execute(select(DimensionScore).where(DimensionScore.ticker_id == ticker.id))
).scalars().all()
assert comp.is_stale is True
assert all(score.is_stale for score in dimensions)
return []
monkeypatch.setattr(scoring_service, "compute_all_dimensions", _boom)
monkeypatch.setattr(rr_scanner_service, "scan_ticker", _fake_scan_ticker)
await rr_scanner_service.scan_all_tickers(session)
async def test_scan_error_does_not_stop_later_tickers(session, monkeypatch):
session.add_all([Ticker(symbol="AAA"), Ticker(symbol="BBB")])
await session.commit()
scanned: list[str] = []
async def _fake_scan_ticker(db, symbol, *args, **kwargs):
scanned.append(symbol)
if symbol == "AAA":
raise RuntimeError("scan blew up")
return []
monkeypatch.setattr(rr_scanner_service, "scan_ticker", _fake_scan_ticker)
await rr_scanner_service.scan_all_tickers(session)
assert scanned == ["AAA", "BBB"]
async def test_scan_skips_ticker_with_incomplete_sec_fundamentals(
session, monkeypatch
):
ticker = Ticker(symbol="BLOCKED", cik="0000000001")
session.add(ticker)
await session.commit()
async def _blocked(db):
return {ticker.id}
async def _unexpected_scan(*args, **kwargs):
raise AssertionError("fundamentals-incomplete ticker was scanned")
monkeypatch.setattr(
rr_scanner_service.fundamentals_quality_service,
"blocked_ticker_ids",
_blocked,
)
monkeypatch.setattr(rr_scanner_service, "scan_ticker", _unexpected_scan)
assert await rr_scanner_service.scan_all_tickers(session) == []
async def test_scan_quality_failure_blocks_closed_and_emits_event(
session, monkeypatch
):
session.add(Ticker(symbol="BLOCKED"))
await session.commit()
async def _boom(db):
raise ValueError("bad quality metadata")
async def _unexpected_scan(*args, **kwargs):
raise AssertionError("ticker was scanned without a quality decision")
events: list[dict] = []
async def _capture_event(**kwargs):
events.append(kwargs)
monkeypatch.setattr(
rr_scanner_service.fundamentals_quality_service,
"blocked_ticker_ids",
_boom,
)
monkeypatch.setattr(rr_scanner_service, "scan_ticker", _unexpected_scan)
monkeypatch.setattr(
rr_scanner_service.system_event_service,
"log_event_standalone",
_capture_event,
)
assert await rr_scanner_service.scan_all_tickers(session) == []
assert [event["code"] for event in events] == [
"fundamentals_quality_unavailable"
]