"""Unit tests for apps.orb_trader.screener.""" from __future__ import annotations import pytest from apps.orb_trader.screener import live_pre_screen, load_universe from libs.intraday.domain import ORBStrategyParams def _enrich(ticker: str, *, prev_close: float, atr_14: float, avg_dollar_vol: float) -> dict: """Build minimal enrichment dict for a single ticker.""" return { ticker: { "2026-01-04": { "prev_close": prev_close, "atr_14": atr_14, "avg_dollar_vol_30d": avg_dollar_vol, } } } def _default_params(**overrides) -> ORBStrategyParams: defaults = dict( min_price=10.0, min_atr_14=0.50, min_avg_dollar_volume=25_000_000.0, min_atr_pct=None, max_atr_pct=None, ) defaults.update(overrides) return ORBStrategyParams(**defaults) _DATE = "2026-01-05" class TestBasicFilters: def test_passes_all_filters(self): enrich = _enrich("AAPL", prev_close=150.0, atr_14=3.0, avg_dollar_vol=100_000_000) result = live_pre_screen(enrich, _DATE, _default_params()) assert "AAPL" in result def test_fails_min_price(self): enrich = _enrich("AAPL", prev_close=9.0, atr_14=0.8, avg_dollar_vol=30_000_000) result = live_pre_screen(enrich, _DATE, _default_params(min_price=10.0)) assert "AAPL" not in result def test_fails_min_atr_14(self): enrich = _enrich("AAPL", prev_close=50.0, atr_14=0.3, avg_dollar_vol=50_000_000) result = live_pre_screen(enrich, _DATE, _default_params(min_atr_14=0.50)) assert "AAPL" not in result def test_fails_min_dollar_volume(self): enrich = _enrich("AAPL", prev_close=50.0, atr_14=1.0, avg_dollar_vol=10_000_000) result = live_pre_screen(enrich, _DATE, _default_params(min_avg_dollar_volume=25_000_000)) assert "AAPL" not in result def test_uses_latest_enrichment_date(self): # Two enrichment dates; the newer one passes filters, older one fails enrich = { "AAPL": { "2025-12-31": {"prev_close": 5.0, "atr_14": 0.1, "avg_dollar_vol_30d": 1_000_000}, "2026-01-04": {"prev_close": 150.0, "atr_14": 3.0, "avg_dollar_vol_30d": 100_000_000}, } } result = live_pre_screen(enrich, _DATE, _default_params()) assert "AAPL" in result def test_future_enrichment_dates_excluded(self): # date_str="2026-01-05"; enrichment only has "2026-01-06" → nothing qualifies enrich = { "AAPL": { "2026-01-06": {"prev_close": 150.0, "atr_14": 3.0, "avg_dollar_vol_30d": 100_000_000}, } } result = live_pre_screen(enrich, _DATE, _default_params()) assert "AAPL" not in result class TestAtrPctFilters: def test_min_atr_pct_passes(self): # atr_14=3.0, prev_close=100.0 → atr_ratio=3% ≥ min_atr_pct=2% → pass enrich = _enrich("AAPL", prev_close=100.0, atr_14=3.0, avg_dollar_vol=50_000_000) params = _default_params(min_atr_pct=0.02) assert "AAPL" in live_pre_screen(enrich, _DATE, params) def test_min_atr_pct_fails(self): # atr_14=1.0, prev_close=100.0 → atr_ratio=1% < min_atr_pct=2% → fail enrich = _enrich("AAPL", prev_close=100.0, atr_14=1.0, avg_dollar_vol=50_000_000) params = _default_params(min_atr_pct=0.02) assert "AAPL" not in live_pre_screen(enrich, _DATE, params) def test_max_atr_pct_passes(self): # atr_ratio=3% < max_atr_pct=5% → pass enrich = _enrich("AAPL", prev_close=100.0, atr_14=3.0, avg_dollar_vol=50_000_000) params = _default_params(max_atr_pct=0.05) assert "AAPL" in live_pre_screen(enrich, _DATE, params) def test_max_atr_pct_fails(self): # atr_14=8.0, prev_close=100.0 → atr_ratio=8% > max_atr_pct=5% → fail enrich = _enrich("AAPL", prev_close=100.0, atr_14=8.0, avg_dollar_vol=50_000_000) params = _default_params(max_atr_pct=0.05) assert "AAPL" not in live_pre_screen(enrich, _DATE, params) def test_both_bounds_respected(self): # Only ticker in [2%, 5%] range should pass; below and above both fail enrich = { "LOW": {"2026-01-04": {"prev_close": 100.0, "atr_14": 1.0, "avg_dollar_vol_30d": 50_000_000}}, "OK": {"2026-01-04": {"prev_close": 100.0, "atr_14": 3.0, "avg_dollar_vol_30d": 50_000_000}}, "HI": {"2026-01-04": {"prev_close": 100.0, "atr_14": 7.0, "avg_dollar_vol_30d": 50_000_000}}, } params = _default_params(min_atr_pct=0.02, max_atr_pct=0.05) result = live_pre_screen(enrich, _DATE, params) assert "OK" in result assert "LOW" not in result assert "HI" not in result def test_none_bounds_no_filter(self): # min_atr_pct=None, max_atr_pct=None → no ATR% filtering enrich = _enrich("AAPL", prev_close=100.0, atr_14=0.1, avg_dollar_vol=50_000_000) params = _default_params(min_atr_14=0.0, min_atr_pct=None, max_atr_pct=None) assert "AAPL" in live_pre_screen(enrich, _DATE, params) def test_load_universe_supports_broad_snapshot() -> None: symbols = load_universe("broad") assert "AAPL" in symbols assert "TSLA" in symbols assert len(symbols) > 3000