You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
132 lines
5.2 KiB
Python
132 lines
5.2 KiB
Python
"""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
|