134 lines
4.0 KiB
Python
134 lines
4.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) == []
|