From 11d1bf6bfed1c946af0594a1c3bc521b10fedab9 Mon Sep 17 00:00:00 2001 From: I Luk Kim Date: Mon, 20 Apr 2026 16:04:25 -0700 Subject: [PATCH] Add live_pre_screen unit tests including min/max_atr_pct filters 12 tests covering all screener filters: min_price, min_atr_14, min_avg_dollar_volume, date selection (latest-before, future excluded), and the V23 quality filters min_atr_pct / max_atr_pct added last session but previously untested. Co-Authored-By: Claude Sonnet 4.6 --- tests/unit/orb_trader/test_screener.py | 123 +++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 tests/unit/orb_trader/test_screener.py diff --git a/tests/unit/orb_trader/test_screener.py b/tests/unit/orb_trader/test_screener.py new file mode 100644 index 0000000..77f2ca8 --- /dev/null +++ b/tests/unit/orb_trader/test_screener.py @@ -0,0 +1,123 @@ +"""Unit tests for apps.orb_trader.screener.live_pre_screen.""" +from __future__ import annotations + +import pytest + +from apps.orb_trader.screener import live_pre_screen +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)