from __future__ import annotations import asyncio from datetime import datetime, timedelta from types import SimpleNamespace from zoneinfo import ZoneInfo from libs.intraday.cache import DailyBarCache, IntradayCache, ReadOnlyDailyBarCache from libs.intraday import screener as screener_module from libs.intraday.domain import StrategyParams, UniverseParams from libs.intraday.screener import ( fetch_daily_bars_bulk, momentum_intraday_first_candidates, momentum_pre_screen_candidates, orb_pre_screen_candidates, pre_screen_candidates, resolve_universe, ) def test_pre_screen_candidates_uses_opening_gap_not_same_day_high() -> None: daily_bars = { "AAA": [ {"date": "2026-01-02", "open": 100.0, "high": 102.0, "low": 99.0, "close": 100.0, "volume": 1_000}, {"date": "2026-01-05", "open": 100.0, "high": 130.0, "low": 95.0, "close": 96.0, "volume": 2_000}, ], "BBB": [ {"date": "2026-01-02", "open": 50.0, "high": 51.0, "low": 49.0, "close": 50.0, "volume": 1_000}, {"date": "2026-01-05", "open": 51.5, "high": 53.0, "low": 51.0, "close": 52.0, "volume": 2_000}, ], } result = pre_screen_candidates( daily_bars, ["2026-01-05"], threshold=0.02, max_per_day=30, ) assert result == {"2026-01-05": ["BBB"]} def test_pre_screen_candidates_can_use_precomputed_gap_enrichment() -> None: daily_bars = { "AAA": [ {"date": "2026-01-02", "open": 100.0, "high": 102.0, "low": 99.0, "close": 100.0, "volume": 1_000}, {"date": "2026-01-05", "open": 100.0, "high": 130.0, "low": 95.0, "close": 96.0, "volume": 2_000}, ], "BBB": [ {"date": "2026-01-02", "open": 50.0, "high": 51.0, "low": 49.0, "close": 50.0, "volume": 1_000}, {"date": "2026-01-05", "open": 51.5, "high": 53.0, "low": 51.0, "close": 52.0, "volume": 2_000}, ], } enrichment = { "AAA": {"2026-01-05": {"gap_pct": 0.0}}, "BBB": {"2026-01-05": {"gap_pct": 0.03}}, } result = pre_screen_candidates( daily_bars, ["2026-01-05"], threshold=0.02, max_per_day=30, enrichment=enrichment, ) assert result == {"2026-01-05": ["BBB"]} def test_momentum_pre_screen_candidates_ranks_by_gap_then_prior_features() -> None: daily_bars = { "AAA": [ {"date": "2026-01-02", "open": 10.0, "high": 10.2, "low": 9.8, "close": 10.0, "volume": 1_000}, {"date": "2026-01-05", "open": 10.3, "high": 10.8, "low": 10.2, "close": 10.6, "volume": 2_000}, ], "BBB": [ {"date": "2026-01-02", "open": 10.0, "high": 10.1, "low": 9.9, "close": 10.0, "volume": 1_000}, {"date": "2026-01-05", "open": 10.3, "high": 10.5, "low": 10.1, "close": 10.2, "volume": 2_000}, ], "CCC": [ {"date": "2026-01-02", "open": 10.0, "high": 10.1, "low": 9.9, "close": 10.0, "volume": 1_000}, {"date": "2026-01-05", "open": 10.5, "high": 10.7, "low": 10.4, "close": 10.6, "volume": 2_000}, ], } enrichment = { "AAA": { "2026-01-05": { "gap_pct": 0.03, "ret_5d": 0.10, "entropy_20d": 0.50, "avg_dollar_vol_30d": 50_000_000.0, "atr_14": 1.5, } }, "BBB": { "2026-01-05": { "gap_pct": 0.03, "ret_5d": 0.02, "entropy_20d": 0.70, "avg_dollar_vol_30d": 30_000_000.0, "atr_14": 1.0, } }, "CCC": { "2026-01-05": { "gap_pct": 0.05, "ret_5d": -0.01, "entropy_20d": 0.80, "avg_dollar_vol_30d": 10_000_000.0, "atr_14": 0.8, } }, } result = momentum_pre_screen_candidates( daily_bars, ["2026-01-05"], enrichment, threshold=0.02, max_per_day=3, ) assert result == {"2026-01-05": ["CCC", "AAA", "BBB"]} def test_momentum_pre_screen_candidates_can_require_event_and_attention() -> None: daily_bars = { "AAA": [ {"date": "2026-01-02", "open": 10.0, "high": 10.2, "low": 9.8, "close": 10.0, "volume": 1_000}, {"date": "2026-01-05", "open": 10.3, "high": 10.8, "low": 10.2, "close": 10.6, "volume": 2_000}, ], "BBB": [ {"date": "2026-01-02", "open": 10.0, "high": 10.1, "low": 9.9, "close": 10.0, "volume": 1_000}, {"date": "2026-01-05", "open": 10.4, "high": 10.6, "low": 10.3, "close": 10.5, "volume": 2_000}, ], } enrichment = { "AAA": { "2026-01-05": { "gap_pct": 0.03, "ret_5d": 0.02, "entropy_20d": 0.70, "avg_dollar_vol_30d": 20_000_000.0, "atr_14": 1.0, "event_flag": True, "event_score": 1.0, "attention_wiki_spike_10d": 2.0, "attention_article_count_3d": 5, "attention_us_article_count_3d": 4, "attention_resolver_confidence": 0.9, } }, "BBB": { "2026-01-05": { "gap_pct": 0.04, "ret_5d": 0.04, "entropy_20d": 0.50, "avg_dollar_vol_30d": 25_000_000.0, "atr_14": 1.2, "event_flag": False, "event_score": 0.0, "attention_wiki_spike_10d": 0.2, "attention_article_count_3d": 0, "attention_us_article_count_3d": 0, "attention_resolver_confidence": 0.2, } }, } strategy = StrategyParams( candidate_require_event_flag=True, candidate_min_attention_wiki_spike_10d=1.0, candidate_weight_event_score=1.0, candidate_weight_attention_wiki=1.0, candidate_weight_attention_news=1.0, ) result = momentum_pre_screen_candidates( daily_bars, ["2026-01-05"], enrichment, threshold=0.02, max_per_day=5, strategy=strategy, ) assert result == {"2026-01-05": ["AAA"]} def test_momentum_pre_screen_candidates_can_filter_event_types() -> None: daily_bars = { "AAA": [ {"date": "2026-01-02", "open": 10.0, "high": 10.2, "low": 9.8, "close": 10.0, "volume": 1_000}, {"date": "2026-01-05", "open": 10.3, "high": 10.8, "low": 10.2, "close": 10.6, "volume": 2_000}, ], "BBB": [ {"date": "2026-01-02", "open": 10.0, "high": 10.2, "low": 9.8, "close": 10.0, "volume": 1_000}, {"date": "2026-01-05", "open": 10.4, "high": 10.9, "low": 10.3, "close": 10.7, "volume": 2_000}, ], } enrichment = { "AAA": { "2026-01-05": { "gap_pct": 0.03, "ret_5d": 0.03, "entropy_20d": 0.60, "avg_dollar_vol_30d": 20_000_000.0, "atr_14": 1.0, "event_flag": True, "event_score": 1.0, "event_types": ["earnings_release"], } }, "BBB": { "2026-01-05": { "gap_pct": 0.04, "ret_5d": 0.04, "entropy_20d": 0.50, "avg_dollar_vol_30d": 25_000_000.0, "atr_14": 1.2, "event_flag": True, "event_score": 1.0, "event_types": ["management_change"], } }, } strategy = StrategyParams( candidate_require_event_flag=True, candidate_allowed_event_types=["earnings_release"], ) result = momentum_pre_screen_candidates( daily_bars, ["2026-01-05"], enrichment, threshold=0.02, max_per_day=5, strategy=strategy, ) assert result == {"2026-01-05": ["AAA"]} def test_momentum_pre_screen_candidates_event_type_filter_does_not_block_non_event_names() -> None: daily_bars = { "AAA": [ {"date": "2026-01-02", "open": 10.0, "high": 10.2, "low": 9.8, "close": 10.0, "volume": 1_000}, {"date": "2026-01-05", "open": 10.3, "high": 10.8, "low": 10.2, "close": 10.6, "volume": 2_000}, ], "BBB": [ {"date": "2026-01-02", "open": 10.0, "high": 10.2, "low": 9.8, "close": 10.0, "volume": 1_000}, {"date": "2026-01-05", "open": 10.4, "high": 10.9, "low": 10.3, "close": 10.7, "volume": 2_000}, ], } enrichment = { "AAA": { "2026-01-05": { "gap_pct": 0.03, "ret_5d": 0.03, "entropy_20d": 0.60, "avg_dollar_vol_30d": 20_000_000.0, "atr_14": 1.0, "event_flag": True, "event_score": 1.0, "event_types": ["management_change"], } }, "BBB": { "2026-01-05": { "gap_pct": 0.04, "ret_5d": 0.04, "entropy_20d": 0.50, "avg_dollar_vol_30d": 25_000_000.0, "atr_14": 1.2, "event_flag": False, "event_score": 0.0, } }, } strategy = StrategyParams( candidate_allowed_event_types=["earnings_release"], ) result = momentum_pre_screen_candidates( daily_bars, ["2026-01-05"], enrichment, threshold=0.02, max_per_day=5, strategy=strategy, ) assert result == {"2026-01-05": ["BBB", "AAA"]} def test_momentum_intraday_first_candidates_uses_entry_time_info_only() -> None: strategy = StrategyParams( candidate_source_mode="intraday_first", entry_minutes_after_open=10, confirmation_minutes_after_entry=5, min_confirmation_return_pct=0.0, min_morning_gain_pct=0.01, min_entry_volume=50_000, candidate_final_max_per_day=2, candidate_require_event_flag=True, ) all_intraday = { "2026-01-05": { "AAA": [ {"timestamp": "2026-01-05T14:30:00+00:00", "open": 10.0, "high": 10.2, "low": 9.9, "close": 10.1, "volume": 30_000}, {"timestamp": "2026-01-05T14:35:00+00:00", "open": 10.1, "high": 10.3, "low": 10.0, "close": 10.2, "volume": 30_000}, {"timestamp": "2026-01-05T14:40:00+00:00", "open": 10.2, "high": 10.5, "low": 10.1, "close": 10.4, "volume": 30_000}, {"timestamp": "2026-01-05T14:45:00+00:00", "open": 10.4, "high": 10.7, "low": 10.3, "close": 10.6, "volume": 30_000}, {"timestamp": "2026-01-05T14:50:00+00:00", "open": 10.6, "high": 10.8, "low": 10.5, "close": 10.7, "volume": 30_000}, ], "BBB": [ {"timestamp": "2026-01-05T14:30:00+00:00", "open": 20.0, "high": 20.1, "low": 19.9, "close": 20.0, "volume": 40_000}, {"timestamp": "2026-01-05T14:35:00+00:00", "open": 20.0, "high": 20.2, "low": 19.9, "close": 20.1, "volume": 40_000}, {"timestamp": "2026-01-05T14:40:00+00:00", "open": 20.1, "high": 20.7, "low": 20.0, "close": 20.5, "volume": 40_000}, {"timestamp": "2026-01-05T14:45:00+00:00", "open": 20.5, "high": 21.0, "low": 20.4, "close": 20.9, "volume": 40_000}, {"timestamp": "2026-01-05T14:50:00+00:00", "open": 20.9, "high": 21.3, "low": 20.8, "close": 21.1, "volume": 40_000}, ], } } daily_enrichment = { "AAA": {"2026-01-05": {"event_flag": False, "gap_pct": 0.01, "avg_daily_vol_14d": 1_000_000.0}}, "BBB": {"2026-01-05": {"event_flag": True, "event_score": 1.0, "gap_pct": 0.01, "avg_daily_vol_14d": 1_000_000.0}}, } result = momentum_intraday_first_candidates( all_intraday, ["2026-01-05"], strategy, daily_enrichment=daily_enrichment, max_per_day=2, ) assert result == {"2026-01-05": ["BBB"]} def test_momentum_intraday_first_candidates_can_filter_event_types() -> None: strategy = StrategyParams( candidate_source_mode="intraday_first", entry_minutes_after_open=10, confirmation_minutes_after_entry=5, min_confirmation_return_pct=0.0, min_morning_gain_pct=0.01, min_entry_volume=50_000, candidate_final_max_per_day=2, candidate_require_event_flag=True, candidate_allowed_event_types=["earnings_release"], ) all_intraday = { "2026-01-05": { "AAA": [ {"timestamp": "2026-01-05T14:30:00+00:00", "open": 10.0, "high": 10.2, "low": 9.9, "close": 10.1, "volume": 30_000}, {"timestamp": "2026-01-05T14:35:00+00:00", "open": 10.1, "high": 10.3, "low": 10.0, "close": 10.2, "volume": 30_000}, {"timestamp": "2026-01-05T14:40:00+00:00", "open": 10.2, "high": 10.5, "low": 10.1, "close": 10.4, "volume": 30_000}, {"timestamp": "2026-01-05T14:45:00+00:00", "open": 10.4, "high": 10.7, "low": 10.3, "close": 10.6, "volume": 30_000}, {"timestamp": "2026-01-05T14:50:00+00:00", "open": 10.6, "high": 10.8, "low": 10.5, "close": 10.7, "volume": 30_000}, ], "BBB": [ {"timestamp": "2026-01-05T14:30:00+00:00", "open": 20.0, "high": 20.1, "low": 19.9, "close": 20.0, "volume": 40_000}, {"timestamp": "2026-01-05T14:35:00+00:00", "open": 20.0, "high": 20.2, "low": 19.9, "close": 20.1, "volume": 40_000}, {"timestamp": "2026-01-05T14:40:00+00:00", "open": 20.1, "high": 20.7, "low": 20.0, "close": 20.5, "volume": 40_000}, {"timestamp": "2026-01-05T14:45:00+00:00", "open": 20.5, "high": 21.0, "low": 20.4, "close": 20.9, "volume": 40_000}, {"timestamp": "2026-01-05T14:50:00+00:00", "open": 20.9, "high": 21.3, "low": 20.8, "close": 21.1, "volume": 40_000}, ], } } daily_enrichment = { "AAA": { "2026-01-05": { "event_flag": True, "event_score": 1.0, "event_types": ["management_change"], "gap_pct": 0.01, "avg_daily_vol_14d": 1_000_000.0, } }, "BBB": { "2026-01-05": { "event_flag": True, "event_score": 1.0, "event_types": ["earnings_release"], "gap_pct": 0.01, "avg_daily_vol_14d": 1_000_000.0, } }, } result = momentum_intraday_first_candidates( all_intraday, ["2026-01-05"], strategy, daily_enrichment=daily_enrichment, max_per_day=2, ) assert result == {"2026-01-05": ["BBB"]} def test_momentum_intraday_first_candidates_can_use_weighted_ranking() -> None: strategy = StrategyParams( candidate_source_mode="intraday_first", candidate_intraday_rank_mode="weighted", candidate_intraday_weight_gain=0.2, candidate_intraday_weight_confirmation=0.5, candidate_intraday_weight_volume_ratio=0.2, candidate_intraday_weight_entry_dollar_volume=0.1, entry_minutes_after_open=10, confirmation_minutes_after_entry=5, min_confirmation_return_pct=0.0, min_morning_gain_pct=0.01, candidate_final_max_per_day=1, ) all_intraday = { "2026-01-05": { "AAA": [ {"timestamp": "2026-01-05T14:30:00+00:00", "open": 10.0, "high": 10.1, "low": 9.9, "close": 10.0, "volume": 80_000}, {"timestamp": "2026-01-05T14:35:00+00:00", "open": 10.0, "high": 10.2, "low": 10.0, "close": 10.1, "volume": 80_000}, {"timestamp": "2026-01-05T14:40:00+00:00", "open": 10.1, "high": 10.4, "low": 10.0, "close": 10.2, "volume": 80_000}, {"timestamp": "2026-01-05T14:45:00+00:00", "open": 10.2, "high": 10.8, "low": 10.2, "close": 10.7, "volume": 80_000}, {"timestamp": "2026-01-05T14:50:00+00:00", "open": 10.7, "high": 10.9, "low": 10.6, "close": 10.8, "volume": 80_000}, ], "BBB": [ {"timestamp": "2026-01-05T14:30:00+00:00", "open": 20.0, "high": 20.2, "low": 19.9, "close": 20.1, "volume": 70_000}, {"timestamp": "2026-01-05T14:35:00+00:00", "open": 20.1, "high": 20.4, "low": 20.0, "close": 20.3, "volume": 70_000}, {"timestamp": "2026-01-05T14:40:00+00:00", "open": 20.3, "high": 21.0, "low": 20.2, "close": 20.9, "volume": 70_000}, {"timestamp": "2026-01-05T14:45:00+00:00", "open": 20.9, "high": 21.0, "low": 20.7, "close": 20.95, "volume": 70_000}, {"timestamp": "2026-01-05T14:50:00+00:00", "open": 20.95, "high": 21.1, "low": 20.8, "close": 21.0, "volume": 70_000}, ], } } daily_enrichment = { "AAA": { "2026-01-05": { "gap_pct": 0.01, "avg_daily_vol_14d": 1_000_000.0, } }, "BBB": { "2026-01-05": { "gap_pct": 0.01, "avg_daily_vol_14d": 1_000_000.0, } }, } result = momentum_intraday_first_candidates( all_intraday, ["2026-01-05"], strategy, daily_enrichment=daily_enrichment, max_per_day=1, ) assert result == {"2026-01-05": ["AAA"]} def test_momentum_intraday_first_candidates_weighted_ranking_can_use_prior_dollar_volume() -> None: strategy = StrategyParams( candidate_source_mode="intraday_first", candidate_intraday_rank_mode="weighted", candidate_intraday_weight_gain=0.2, candidate_intraday_weight_confirmation=0.4, candidate_intraday_weight_volume_ratio=0.2, candidate_intraday_weight_entry_dollar_volume=0.1, candidate_intraday_weight_avg_dollar_vol_30d=0.5, entry_minutes_after_open=10, confirmation_minutes_after_entry=5, min_confirmation_return_pct=0.0, min_morning_gain_pct=0.005, candidate_final_max_per_day=1, ) all_intraday = { "2026-01-05": { "AAA": [ {"timestamp": "2026-01-05T14:30:00+00:00", "open": 10.0, "high": 10.1, "low": 9.9, "close": 10.0, "volume": 400_000}, {"timestamp": "2026-01-05T14:35:00+00:00", "open": 10.0, "high": 10.2, "low": 10.0, "close": 10.1, "volume": 400_000}, {"timestamp": "2026-01-05T14:40:00+00:00", "open": 10.1, "high": 10.2, "low": 10.0, "close": 10.1, "volume": 400_000}, {"timestamp": "2026-01-05T14:45:00+00:00", "open": 10.1, "high": 10.3, "low": 10.1, "close": 10.2, "volume": 400_000}, {"timestamp": "2026-01-05T14:50:00+00:00", "open": 10.2, "high": 10.4, "low": 10.1, "close": 10.3, "volume": 400_000}, ], "BBB": [ {"timestamp": "2026-01-05T14:30:00+00:00", "open": 20.0, "high": 20.1, "low": 19.9, "close": 20.0, "volume": 200_000}, {"timestamp": "2026-01-05T14:35:00+00:00", "open": 20.0, "high": 20.2, "low": 20.0, "close": 20.1, "volume": 200_000}, {"timestamp": "2026-01-05T14:40:00+00:00", "open": 20.1, "high": 20.2, "low": 20.0, "close": 20.1, "volume": 200_000}, {"timestamp": "2026-01-05T14:45:00+00:00", "open": 20.1, "high": 20.3, "low": 20.1, "close": 20.2, "volume": 200_000}, {"timestamp": "2026-01-05T14:50:00+00:00", "open": 20.2, "high": 20.4, "low": 20.1, "close": 20.3, "volume": 200_000}, ], } } daily_enrichment = { "AAA": { "2026-01-05": { "gap_pct": 0.01, "avg_daily_vol_14d": 5_000_000.0, "avg_dollar_vol_30d": 200_000_000.0, } }, "BBB": { "2026-01-05": { "gap_pct": 0.01, "avg_daily_vol_14d": 5_000_000.0, "avg_dollar_vol_30d": 8_000_000_000.0, } }, } result = momentum_intraday_first_candidates( all_intraday, ["2026-01-05"], strategy, daily_enrichment=daily_enrichment, max_per_day=1, ) assert result == {"2026-01-05": ["BBB"]} def test_momentum_intraday_first_candidates_weighted_ranking_can_use_sector_thrust() -> None: strategy = StrategyParams( candidate_source_mode="intraday_first", candidate_intraday_rank_mode="weighted", candidate_intraday_weight_gain=0.2, candidate_intraday_weight_confirmation=0.2, candidate_intraday_weight_entry_dollar_volume=0.1, candidate_intraday_weight_sector_thrust=1.0, entry_minutes_after_open=10, confirmation_minutes_after_entry=5, min_confirmation_return_pct=0.0, min_morning_gain_pct=0.01, candidate_final_max_per_day=1, use_sector_thrust_sleeve=True, sector_thrust_min_members=2, sector_thrust_min_gain_pct=0.01, sector_thrust_min_confirmation_return_pct=0.003, sector_thrust_min_entry_dollar_volume=50_000_000.0, sector_thrust_min_avg_dollar_vol_30d=500_000_000.0, sector_thrust_min_sector_avg_confirmation_return_pct=0.003, sector_thrust_min_sector_total_entry_dollar_volume=120_000_000.0, ) all_intraday = { "2026-01-05": { "ALLY_A": [ {"timestamp": "2026-01-05T14:30:00+00:00", "open": 100.0, "high": 100.2, "low": 99.9, "close": 100.0, "volume": 180_000}, {"timestamp": "2026-01-05T14:35:00+00:00", "open": 100.0, "high": 100.8, "low": 99.9, "close": 100.6, "volume": 180_000}, {"timestamp": "2026-01-05T14:40:00+00:00", "open": 100.6, "high": 101.2, "low": 100.5, "close": 101.0, "volume": 180_000}, {"timestamp": "2026-01-05T14:45:00+00:00", "open": 101.0, "high": 101.7, "low": 100.9, "close": 101.5, "volume": 180_000}, {"timestamp": "2026-01-05T14:50:00+00:00", "open": 101.5, "high": 101.8, "low": 101.4, "close": 101.6, "volume": 180_000}, ], "ALLY_B": [ {"timestamp": "2026-01-05T14:30:00+00:00", "open": 80.0, "high": 80.1, "low": 79.9, "close": 80.0, "volume": 170_000}, {"timestamp": "2026-01-05T14:35:00+00:00", "open": 80.0, "high": 80.6, "low": 79.9, "close": 80.4, "volume": 170_000}, {"timestamp": "2026-01-05T14:40:00+00:00", "open": 80.4, "high": 80.9, "low": 80.3, "close": 80.8, "volume": 170_000}, {"timestamp": "2026-01-05T14:45:00+00:00", "open": 80.8, "high": 81.4, "low": 80.7, "close": 81.2, "volume": 170_000}, {"timestamp": "2026-01-05T14:50:00+00:00", "open": 81.2, "high": 81.5, "low": 81.1, "close": 81.3, "volume": 170_000}, ], "SOLO": [ {"timestamp": "2026-01-05T14:30:00+00:00", "open": 20.0, "high": 20.3, "low": 19.9, "close": 20.1, "volume": 300_000}, {"timestamp": "2026-01-05T14:35:00+00:00", "open": 20.1, "high": 20.8, "low": 20.0, "close": 20.6, "volume": 300_000}, {"timestamp": "2026-01-05T14:40:00+00:00", "open": 20.6, "high": 21.1, "low": 20.5, "close": 20.9, "volume": 300_000}, {"timestamp": "2026-01-05T14:45:00+00:00", "open": 20.9, "high": 21.3, "low": 20.8, "close": 21.1, "volume": 300_000}, {"timestamp": "2026-01-05T14:50:00+00:00", "open": 21.1, "high": 21.3, "low": 21.0, "close": 21.2, "volume": 300_000}, ], } } daily_enrichment = { "ALLY_A": {"2026-01-05": {"gap_pct": 0.01, "avg_daily_vol_14d": 5_000_000.0, "avg_dollar_vol_30d": 900_000_000.0}}, "ALLY_B": {"2026-01-05": {"gap_pct": 0.01, "avg_daily_vol_14d": 5_000_000.0, "avg_dollar_vol_30d": 850_000_000.0}}, "SOLO": {"2026-01-05": {"gap_pct": 0.01, "avg_daily_vol_14d": 10_000_000.0, "avg_dollar_vol_30d": 1_200_000_000.0}}, } result = momentum_intraday_first_candidates( all_intraday, ["2026-01-05"], strategy, daily_enrichment=daily_enrichment, ticker_sectors={ "ALLY_A": "Technology", "ALLY_B": "Technology", "SOLO": "Energy", }, max_per_day=1, ) assert result == {"2026-01-05": ["ALLY_A"]} def test_momentum_intraday_first_candidates_can_use_liquid_continuation_rank_mode() -> None: strategy = StrategyParams( candidate_source_mode="intraday_first", candidate_intraday_rank_mode="liquid_continuation", entry_minutes_after_open=10, confirmation_minutes_after_entry=5, min_confirmation_return_pct=0.0, min_morning_gain_pct=0.004, candidate_final_max_per_day=1, use_liquid_largecap_sleeve=True, liquid_largecap_min_gain_pct=0.004, liquid_largecap_max_gain_pct=0.03, liquid_largecap_min_confirmation_return_pct=0.0005, liquid_largecap_min_entry_dollar_volume=50_000_000.0, liquid_largecap_min_avg_dollar_vol_30d=2_000_000_000.0, liquid_largecap_max_entropy_20d=0.90, use_moderate_gap_liquid_sleeve=True, moderate_gap_liquid_min_gap_pct=0.002, moderate_gap_liquid_max_gap_pct=0.04, moderate_gap_liquid_min_gain_pct=0.005, moderate_gap_liquid_max_gain_pct=0.04, moderate_gap_liquid_min_confirmation_return_pct=0.001, moderate_gap_liquid_min_entry_dollar_volume=25_000_000.0, moderate_gap_liquid_min_avg_dollar_vol_30d=250_000_000.0, moderate_gap_liquid_max_avg_dollar_vol_30d=4_000_000_000.0, moderate_gap_liquid_min_volume_ratio_14d=0.02, moderate_gap_liquid_max_entropy_20d=0.88, ) all_intraday = { "2026-01-05": { "LIQ": [ {"timestamp": "2026-01-05T14:30:00+00:00", "open": 100.0, "high": 100.8, "low": 99.9, "close": 100.5, "volume": 220_000}, {"timestamp": "2026-01-05T14:35:00+00:00", "open": 100.5, "high": 101.2, "low": 100.4, "close": 101.0, "volume": 220_000}, {"timestamp": "2026-01-05T14:40:00+00:00", "open": 101.0, "high": 101.8, "low": 100.9, "close": 101.5, "volume": 220_000}, {"timestamp": "2026-01-05T14:45:00+00:00", "open": 101.5, "high": 102.2, "low": 101.4, "close": 102.0, "volume": 220_000}, {"timestamp": "2026-01-05T14:50:00+00:00", "open": 102.0, "high": 102.4, "low": 101.9, "close": 102.2, "volume": 220_000}, ], "HOT": [ {"timestamp": "2026-01-05T14:30:00+00:00", "open": 10.0, "high": 10.4, "low": 9.9, "close": 10.3, "volume": 120_000}, {"timestamp": "2026-01-05T14:35:00+00:00", "open": 10.3, "high": 10.7, "low": 10.2, "close": 10.6, "volume": 120_000}, {"timestamp": "2026-01-05T14:40:00+00:00", "open": 10.6, "high": 10.9, "low": 10.5, "close": 10.8, "volume": 120_000}, {"timestamp": "2026-01-05T14:45:00+00:00", "open": 10.8, "high": 11.0, "low": 10.7, "close": 10.9, "volume": 120_000}, {"timestamp": "2026-01-05T14:50:00+00:00", "open": 10.9, "high": 11.1, "low": 10.8, "close": 11.0, "volume": 120_000}, ], } } daily_enrichment = { "LIQ": { "2026-01-05": { "gap_pct": 0.01, "avg_daily_vol_14d": 5_000_000.0, "avg_dollar_vol_30d": 3_000_000_000.0, "entropy_20d": 0.70, } }, "HOT": { "2026-01-05": { "gap_pct": 0.02, "avg_daily_vol_14d": 4_000_000.0, "avg_dollar_vol_30d": 50_000_000.0, "entropy_20d": 0.82, } }, } result = momentum_intraday_first_candidates( all_intraday, ["2026-01-05"], strategy, daily_enrichment=daily_enrichment, max_per_day=2, ) assert result == {"2026-01-05": ["LIQ"]} def test_momentum_intraday_first_candidates_can_replace_tail_with_event_reserve() -> None: strategy = StrategyParams( candidate_source_mode="intraday_first", candidate_intraday_rank_mode="weighted", candidate_intraday_weight_gain=0.2, candidate_intraday_weight_confirmation=0.5, candidate_intraday_weight_volume_ratio=0.2, candidate_intraday_weight_entry_dollar_volume=0.1, candidate_intraday_event_reserve_slots=1, candidate_intraday_event_reserve_min_score=1.0, entry_minutes_after_open=10, confirmation_minutes_after_entry=5, min_confirmation_return_pct=0.0, min_morning_gain_pct=0.01, candidate_final_max_per_day=2, ) all_intraday = { "2026-01-05": { "AAA": [ {"timestamp": "2026-01-05T14:30:00+00:00", "open": 10.0, "high": 10.1, "low": 9.9, "close": 10.0, "volume": 120_000}, {"timestamp": "2026-01-05T14:35:00+00:00", "open": 10.0, "high": 10.2, "low": 10.0, "close": 10.1, "volume": 120_000}, {"timestamp": "2026-01-05T14:40:00+00:00", "open": 10.1, "high": 10.6, "low": 10.0, "close": 10.5, "volume": 120_000}, {"timestamp": "2026-01-05T14:45:00+00:00", "open": 10.5, "high": 10.9, "low": 10.4, "close": 10.8, "volume": 120_000}, {"timestamp": "2026-01-05T14:50:00+00:00", "open": 10.8, "high": 11.0, "low": 10.7, "close": 10.9, "volume": 120_000}, ], "BBB": [ {"timestamp": "2026-01-05T14:30:00+00:00", "open": 20.0, "high": 20.2, "low": 19.9, "close": 20.0, "volume": 110_000}, {"timestamp": "2026-01-05T14:35:00+00:00", "open": 20.0, "high": 20.3, "low": 20.0, "close": 20.2, "volume": 110_000}, {"timestamp": "2026-01-05T14:40:00+00:00", "open": 20.2, "high": 20.7, "low": 20.1, "close": 20.6, "volume": 110_000}, {"timestamp": "2026-01-05T14:45:00+00:00", "open": 20.6, "high": 20.8, "low": 20.5, "close": 20.7, "volume": 110_000}, {"timestamp": "2026-01-05T14:50:00+00:00", "open": 20.7, "high": 20.9, "low": 20.6, "close": 20.8, "volume": 110_000}, ], "CAT": [ {"timestamp": "2026-01-05T14:30:00+00:00", "open": 30.0, "high": 30.1, "low": 29.9, "close": 30.0, "volume": 100_000}, {"timestamp": "2026-01-05T14:35:00+00:00", "open": 30.0, "high": 30.2, "low": 30.0, "close": 30.1, "volume": 100_000}, {"timestamp": "2026-01-05T14:40:00+00:00", "open": 30.1, "high": 30.5, "low": 30.0, "close": 30.4, "volume": 100_000}, {"timestamp": "2026-01-05T14:45:00+00:00", "open": 30.4, "high": 30.6, "low": 30.3, "close": 30.45, "volume": 100_000}, {"timestamp": "2026-01-05T14:50:00+00:00", "open": 30.45, "high": 30.7, "low": 30.4, "close": 30.5, "volume": 100_000}, ], } } daily_enrichment = { "AAA": {"2026-01-05": {"gap_pct": 0.01, "avg_daily_vol_14d": 1_000_000.0}}, "BBB": {"2026-01-05": {"gap_pct": 0.01, "avg_daily_vol_14d": 1_000_000.0}}, "CAT": { "2026-01-05": { "gap_pct": 0.01, "avg_daily_vol_14d": 1_000_000.0, "event_flag": True, "event_score": 1.5, } }, } result = momentum_intraday_first_candidates( all_intraday, ["2026-01-05"], strategy, daily_enrichment=daily_enrichment, max_per_day=2, ) assert result == {"2026-01-05": ["AAA", "CAT"]} def test_momentum_intraday_first_candidates_can_reserve_moderate_liquid_followthrough() -> None: strategy = StrategyParams( candidate_source_mode="intraday_first", candidate_intraday_rank_mode="weighted", candidate_intraday_weight_gain=1.0, entry_minutes_after_open=10, confirmation_minutes_after_entry=5, min_confirmation_return_pct=0.01, min_morning_gain_pct=0.04, candidate_final_max_per_day=2, use_moderate_gap_liquid_sleeve=True, moderate_gap_liquid_min_gap_pct=0.005, moderate_gap_liquid_max_gap_pct=0.025, moderate_gap_liquid_min_gain_pct=0.015, moderate_gap_liquid_max_gain_pct=0.04, moderate_gap_liquid_min_confirmation_return_pct=0.005, moderate_gap_liquid_min_entry_dollar_volume=40_000_000.0, moderate_gap_liquid_min_avg_dollar_vol_30d=250_000_000.0, moderate_gap_liquid_max_avg_dollar_vol_30d=2_000_000_000.0, moderate_gap_liquid_max_entropy_20d=0.86, candidate_intraday_moderate_liquid_reserve_slots=1, ) all_intraday = { "2026-03-13": { "AAA": [ {"timestamp": "2026-03-13T13:30:00+00:00", "open": 10.0, "high": 10.1, "low": 9.9, "close": 10.0, "volume": 80_000}, {"timestamp": "2026-03-13T13:35:00+00:00", "open": 10.0, "high": 10.5, "low": 10.0, "close": 10.4, "volume": 80_000}, {"timestamp": "2026-03-13T13:40:00+00:00", "open": 10.4, "high": 10.9, "low": 10.3, "close": 10.8, "volume": 80_000}, {"timestamp": "2026-03-13T13:45:00+00:00", "open": 10.8, "high": 11.1, "low": 10.7, "close": 11.0, "volume": 80_000}, {"timestamp": "2026-03-13T13:50:00+00:00", "open": 11.0, "high": 11.1, "low": 10.9, "close": 11.0, "volume": 80_000}, ], "BBB": [ {"timestamp": "2026-03-13T13:30:00+00:00", "open": 20.0, "high": 20.1, "low": 19.9, "close": 20.0, "volume": 80_000}, {"timestamp": "2026-03-13T13:35:00+00:00", "open": 20.0, "high": 20.7, "low": 20.0, "close": 20.5, "volume": 80_000}, {"timestamp": "2026-03-13T13:40:00+00:00", "open": 20.5, "high": 21.2, "low": 20.4, "close": 21.0, "volume": 80_000}, {"timestamp": "2026-03-13T13:45:00+00:00", "open": 21.0, "high": 21.3, "low": 20.9, "close": 21.2, "volume": 80_000}, {"timestamp": "2026-03-13T13:50:00+00:00", "open": 21.2, "high": 21.3, "low": 21.1, "close": 21.2, "volume": 80_000}, ], "TER": [ {"timestamp": "2026-03-13T13:30:00+00:00", "open": 100.0, "high": 100.2, "low": 99.8, "close": 100.0, "volume": 150_000}, {"timestamp": "2026-03-13T13:35:00+00:00", "open": 100.0, "high": 100.8, "low": 100.0, "close": 100.4, "volume": 150_000}, {"timestamp": "2026-03-13T13:40:00+00:00", "open": 100.4, "high": 101.4, "low": 100.3, "close": 101.2, "volume": 150_000}, {"timestamp": "2026-03-13T13:45:00+00:00", "open": 101.2, "high": 102.0, "low": 101.1, "close": 101.9, "volume": 150_000}, {"timestamp": "2026-03-13T13:50:00+00:00", "open": 101.9, "high": 102.1, "low": 101.8, "close": 102.0, "volume": 150_000}, ], } } daily_enrichment = { "AAA": {"2026-03-13": {"gap_pct": 0.04, "avg_daily_vol_14d": 1_000_000.0, "avg_dollar_vol_30d": 100_000_000.0, "entropy_20d": 0.50}}, "BBB": {"2026-03-13": {"gap_pct": 0.04, "avg_daily_vol_14d": 1_000_000.0, "avg_dollar_vol_30d": 100_000_000.0, "entropy_20d": 0.50}}, "TER": {"2026-03-13": {"gap_pct": 0.012, "avg_daily_vol_14d": 8_000_000.0, "avg_dollar_vol_30d": 750_000_000.0, "entropy_20d": 0.79}}, } result = momentum_intraday_first_candidates( all_intraday, ["2026-03-13"], strategy, daily_enrichment=daily_enrichment, max_per_day=2, ) assert result == {"2026-03-13": ["AAA", "TER"]} def test_momentum_intraday_moderate_liquid_reserve_can_be_sparse_only() -> None: strategy = StrategyParams( candidate_source_mode="intraday_first", candidate_intraday_rank_mode="weighted", candidate_intraday_weight_gain=1.0, entry_minutes_after_open=10, confirmation_minutes_after_entry=5, min_confirmation_return_pct=0.0, min_morning_gain_pct=0.01, candidate_final_max_per_day=2, top_n=2, use_moderate_gap_liquid_sleeve=True, moderate_gap_liquid_min_gap_pct=0.005, moderate_gap_liquid_max_gap_pct=0.025, moderate_gap_liquid_min_gain_pct=0.015, moderate_gap_liquid_max_gain_pct=0.04, moderate_gap_liquid_min_confirmation_return_pct=0.005, moderate_gap_liquid_min_entry_dollar_volume=40_000_000.0, moderate_gap_liquid_min_avg_dollar_vol_30d=250_000_000.0, moderate_gap_liquid_max_avg_dollar_vol_30d=2_000_000_000.0, moderate_gap_liquid_max_entropy_20d=0.86, candidate_intraday_moderate_liquid_reserve_slots=1, candidate_intraday_moderate_liquid_reserve_trigger_below=2, ) bars = [ {"timestamp": "2026-03-13T13:30:00+00:00", "open": 100.0, "high": 100.2, "low": 99.8, "close": 100.0, "volume": 150_000}, {"timestamp": "2026-03-13T13:35:00+00:00", "open": 100.0, "high": 101.0, "low": 100.0, "close": 100.8, "volume": 150_000}, {"timestamp": "2026-03-13T13:40:00+00:00", "open": 100.8, "high": 102.0, "low": 100.7, "close": 101.7, "volume": 150_000}, {"timestamp": "2026-03-13T13:45:00+00:00", "open": 101.7, "high": 102.5, "low": 101.6, "close": 102.2, "volume": 150_000}, {"timestamp": "2026-03-13T13:50:00+00:00", "open": 102.2, "high": 102.4, "low": 102.0, "close": 102.3, "volume": 150_000}, ] all_intraday = { "2026-03-13": { "AAA": bars, "BBB": [ {**bar, "open": bar["open"] * 2, "high": bar["high"] * 2, "low": bar["low"] * 2, "close": bar["close"] * 2} for bar in bars ], "TER": [ {**bar, "open": bar["open"] * 3, "high": bar["high"] * 3, "low": bar["low"] * 3, "close": bar["close"] * 3} for bar in bars ], } } daily_enrichment = { "AAA": {"2026-03-13": {"gap_pct": 0.04, "avg_daily_vol_14d": 1_000_000.0, "avg_dollar_vol_30d": 100_000_000.0, "entropy_20d": 0.50}}, "BBB": {"2026-03-13": {"gap_pct": 0.04, "avg_daily_vol_14d": 1_000_000.0, "avg_dollar_vol_30d": 100_000_000.0, "entropy_20d": 0.50}}, "TER": { "2026-03-13": { "gap_pct": 0.012, "avg_daily_vol_14d": 8_000_000.0, "avg_dollar_vol_30d": 750_000_000.0, "entropy_20d": 0.79, "candidate_seed_moderate_liquid_overlay": True, } }, } result = momentum_intraday_first_candidates( all_intraday, ["2026-03-13"], strategy, daily_enrichment=daily_enrichment, max_per_day=2, ) assert set(result["2026-03-13"]) == {"AAA", "BBB"} class _StubOracleClient: def __init__(self, responses: dict[str, object], *, health_ok: bool = True) -> None: self._responses = responses self._health_ok = health_ok self.calls: list[tuple[str, dict | None]] = [] async def get(self, path: str, params: dict | None = None) -> object: self.calls.append((path, params)) response = self._responses[path] if isinstance(response, Exception): raise response if callable(response): return response(params) return response async def health_check_fast(self, timeout: float = 3.0) -> bool: return self._health_ok def test_orb_pre_screen_candidates_returns_full_ranked_universe_when_uncapped() -> None: daily_bars = { "AAA": [{"date": "2026-01-05", "open": 10.0}], "BBB": [{"date": "2026-01-05", "open": 20.0}], "CCC": [{"date": "2026-01-05", "open": 40.0}], } enrichment = { "AAA": {"2026-01-05": {"atr_14": 4.0, "avg_dollar_vol_30d": 50_000_000.0}}, "BBB": {"2026-01-05": {"atr_14": 3.0, "avg_dollar_vol_30d": 50_000_000.0}}, "CCC": {"2026-01-05": {"atr_14": 1.0, "avg_dollar_vol_30d": 50_000_000.0}}, } capped = orb_pre_screen_candidates( daily_bars, ["2026-01-05"], enrichment, max_per_day=2, ) uncapped = orb_pre_screen_candidates( daily_bars, ["2026-01-05"], enrichment, max_per_day=None, ) assert capped["2026-01-05"] == ["AAA", "BBB"] assert uncapped["2026-01-05"] == ["AAA", "BBB", "CCC"] def test_fetch_daily_bars_bulk_uses_bulk_endpoint_by_default() -> None: client = _StubOracleClient( { "/api/v1/price/data": { "bars": { "AAA": [ { "date": "2026-01-02", "open": 10, "high": 11, "low": 9, "close": 10.5, "volume": 1000, } ], "BBB": [ { "date": "2026-01-02", "open": 20, "high": 21, "low": 19, "close": 20.5, "volume": 2000, } ], } } } ) bars = asyncio.run( fetch_daily_bars_bulk( ["AAA", "BBB"], "2026-01-02", "2026-01-02", client, concurrency=2 ) ) assert list(bars) == ["AAA", "BBB"] assert len(client.calls) == 1 assert client.calls[0][0] == "/api/v1/price/data" assert client.calls[0][1] == { "tickers": "AAA,BBB", "start_date": "2026-01-02", "end_date": "2026-01-02", } def test_fetch_daily_bars_bulk_falls_back_to_single_ticker_on_chunk_failure() -> None: client = _StubOracleClient( { "/api/v1/price/data": RuntimeError("bulk failed"), "/api/v1/price/data/AAA": { "ticker": "AAA", "data": [ { "date": "2026-01-02", "open": 10, "high": 11, "low": 9, "close": 10.5, "volume": 1000, } ], }, "/api/v1/price/data/BBB": { "ticker": "BBB", "data": [ { "date": "2026-01-02", "open": 20, "high": 21, "low": 19, "close": 20.5, "volume": 2000, } ], }, } ) bars = asyncio.run( fetch_daily_bars_bulk( ["AAA", "BBB"], "2026-01-02", "2026-01-02", client, concurrency=2 ) ) assert list(bars) == ["AAA", "BBB"] assert [call[0] for call in client.calls] == [ "/api/v1/price/data", "/api/v1/price/data/AAA", "/api/v1/price/data/BBB", ] def test_fetch_daily_bars_bulk_repairs_suspiciously_short_bulk_responses() -> None: short_rows = [ { "date": f"2026-04-{6 + i:02d}", "open": 100 + i, "high": 101 + i, "low": 99 + i, "close": 100.5 + i, "volume": 1000 + i, } for i in range(11) ] full_row_dates = [ (datetime(2026, 1, 20) + timedelta(days=i * 3)).date().isoformat() for i in range(24) ] + ["2026-04-20"] full_rows = [ { "date": day, "open": 100 + i, "high": 101 + i, "low": 99 + i, "close": 100.5 + i, "volume": 1000 + i, } for i, day in enumerate(full_row_dates) ] client = _StubOracleClient( { "/api/v1/price/data": { "bars": { "AAA": short_rows, } }, "/api/v1/price/data/AAA": { "ticker": "AAA", "data": full_rows, }, } ) bars = asyncio.run( fetch_daily_bars_bulk( ["AAA"], "2026-01-20", "2026-04-20", client, concurrency=1, ) ) assert len(bars["AAA"]) == 25 assert [call[0] for call in client.calls] == [ "/api/v1/price/data", "/api/v1/price/data/AAA", ] def test_fetch_daily_bars_bulk_hits_daily_cache_on_repeat_range(tmp_path) -> None: cache = DailyBarCache(str(tmp_path)) client = _StubOracleClient( { "/api/v1/price/data": { "bars": { "AAA": [ { "date": "2026-01-02", "open": 10, "high": 11, "low": 9, "close": 10.5, "volume": 1000, } ], "BBB": [ { "date": "2026-01-02", "open": 20, "high": 21, "low": 19, "close": 20.5, "volume": 2000, } ], } } } ) first = asyncio.run( fetch_daily_bars_bulk( ["AAA", "BBB"], "2026-01-01", "2026-01-10", client, cache=cache, concurrency=2, ) ) assert list(first) == ["AAA", "BBB"] assert len(client.calls) == 1 client.calls.clear() second = asyncio.run( fetch_daily_bars_bulk( ["AAA", "BBB"], "2026-01-01", "2026-01-10", client, cache=cache, concurrency=2, ) ) assert second == first assert client.calls == [] def test_fetch_daily_bars_bulk_strict_snapshot_does_not_repair_misses(tmp_path) -> None: base_cache = DailyBarCache(str(tmp_path)) cache = ReadOnlyDailyBarCache(base_cache) base_cache.put( "AAA", "2026-01-01", "2026-01-10", [ { "date": "2026-01-02", "open": 10, "high": 11, "low": 9, "close": 10.5, "volume": 1000, } ], ) client = _StubOracleClient( { "/api/v1/price/data": { "bars": { "BBB": [ { "date": "2026-01-02", "open": 20, "high": 21, "low": 19, "close": 20.5, "volume": 2000, } ], } } } ) bars = asyncio.run( fetch_daily_bars_bulk( ["AAA", "BBB"], "2026-01-01", "2026-01-10", client, cache=cache, concurrency=2, ) ) assert list(bars) == ["AAA"] assert client.calls == [] assert base_cache.get("BBB", "2026-01-01", "2026-01-10") is None def test_fetch_daily_bars_bulk_drops_stale_tail_cache_when_oracle_has_no_tail(tmp_path) -> None: cache = DailyBarCache(str(tmp_path)) cached_rows = [ { "date": "2026-01-20", "open": 100.0, "high": 101.0, "low": 99.0, "close": 100.5, "volume": 1000.0, }, { "date": "2026-02-01", "open": 101.0, "high": 102.0, "low": 100.0, "close": 101.5, "volume": 1100.0, }, ] cache.put("AAA", "2026-01-20", "2026-02-01", cached_rows) client = _StubOracleClient({"/api/v1/price/data": {"bars": {"AAA": []}}}) bars = asyncio.run( fetch_daily_bars_bulk( ["AAA"], "2026-01-20", "2026-04-20", client, cache=cache, concurrency=1, ) ) assert bars == {} assert client.calls == [ ( "/api/v1/price/data", { "tickers": "AAA", "start_date": "2026-02-02", "end_date": "2026-04-20", }, ) ] def test_fetch_intraday_bulk_recovers_from_bulk_chunk_failure_by_splitting() -> None: from libs.intraday.screener import fetch_intraday_bulk def intraday_response(params: dict | None) -> dict: tickers = (params or {}).get("tickers", "") if "," in tickers: raise RuntimeError("chunk failed") ticker = tickers return { "bars": { ticker: [ { "timestamp": f"2026-01-02T14:{30 + i:02d}:00+00:00", "open": 10.0, "high": 11.0, "low": 9.5, "close": 10.5, "volume": 1000.0, "vwap": 10.4, } for i in range(10) ] } } client = _StubOracleClient({"/api/v1/alpaca/intraday": intraday_response}) bars = asyncio.run( fetch_intraday_bulk( {"2026-01-02": ["AAA", "BBB"]}, client, cache=None, concurrency=2, ) ) assert list(bars["2026-01-02"]) == ["AAA", "BBB"] assert [call[1]["tickers"] for call in client.calls] == ["AAA,BBB", "AAA", "BBB"] def test_fetch_intraday_bulk_negative_caches_sparse_responses(tmp_path) -> None: from libs.intraday.cache import IntradayCache from libs.intraday.screener import fetch_intraday_bulk def intraday_response(params: dict | None) -> dict: tickers = (params or {}).get("tickers", "") result = {"bars": {}} for ticker in tickers.split(","): if ticker == "AAA": result["bars"][ticker] = [ { "timestamp": "2026-01-02T14:30:00+00:00", "open": 10.0, "high": 11.0, "low": 9.5, "close": 10.5, "volume": 1000.0, "vwap": 10.4, } ] else: result["bars"][ticker] = [ { "timestamp": f"2026-01-02T14:{30 + i:02d}:00+00:00", "open": 20.0, "high": 21.0, "low": 19.5, "close": 20.5, "volume": 1000.0, "vwap": 20.4, } for i in range(10) ] return result client = _StubOracleClient({"/api/v1/alpaca/intraday": intraday_response}) cache = IntradayCache(str(tmp_path)) first = asyncio.run( fetch_intraday_bulk( {"2026-01-02": ["AAA", "BBB"]}, client, cache=cache, concurrency=2, ) ) assert list(first["2026-01-02"]) == ["BBB"] assert cache.has("AAA", "2026-01-02") is True assert cache.get("AAA", "2026-01-02") == [] client.calls.clear() second = asyncio.run( fetch_intraday_bulk( {"2026-01-02": ["AAA", "BBB"]}, client, cache=cache, concurrency=2, ) ) assert list(second["2026-01-02"]) == ["BBB"] assert client.calls == [] def test_fetch_intraday_bulk_uses_today_endpoint_without_caching_live_bars(tmp_path, monkeypatch) -> None: from libs.intraday.cache import IntradayCache from libs.intraday.screener import fetch_intraday_bulk today = "2026-05-01" class MarketHoursDateTime(datetime): @classmethod def now(cls, tz=None): value = datetime(2026, 5, 1, 10, 0, tzinfo=ZoneInfo("America/New_York")) return value.astimezone(tz) if tz else value monkeypatch.setattr(screener_module, "datetime", MarketHoursDateTime) def intraday_today_response(params: dict | None) -> dict: ticker = (params or {}).get("tickers", "") return { "bars": { ticker: [ { "timestamp": ( datetime.fromisoformat(f"{today}T13:30:00+00:00") + timedelta(minutes=5 * i) ).isoformat(), "open": 10.0 + i * 0.1, "high": 10.2 + i * 0.1, "low": 9.9 + i * 0.1, "close": 10.1 + i * 0.1, "volume": 1000.0, "vwap": 10.05 + i * 0.1, } for i in range(78) ] } } client = _StubOracleClient({"/api/v1/alpaca/intraday/today": intraday_today_response}) cache = IntradayCache(str(tmp_path)) bars = asyncio.run( fetch_intraday_bulk( {today: ["AAA"]}, client, cache=cache, concurrency=1, ) ) assert list(bars[today]) == ["AAA"] assert client.calls == [ ( "/api/v1/alpaca/intraday/today", {"tickers": "AAA", "interval": "5m"}, ) ] assert cache.has("AAA", today) is False def test_fetch_intraday_bulk_uses_historical_today_after_close_and_caches(tmp_path, monkeypatch) -> None: from libs.intraday.cache import IntradayCache from libs.intraday.screener import fetch_intraday_bulk today = "2026-05-01" class AfterCloseDateTime(datetime): @classmethod def now(cls, tz=None): value = datetime(2026, 5, 1, 21, 15, tzinfo=ZoneInfo("America/New_York")) return value.astimezone(tz) if tz else value monkeypatch.setattr(screener_module, "datetime", AfterCloseDateTime) def historical_response(params: dict | None) -> dict: ticker = (params or {}).get("tickers", "") return { "bars": { ticker: [ { "timestamp": ( datetime.fromisoformat(f"{today}T13:30:00+00:00") + timedelta(minutes=5 * i) ).isoformat(), "open": 10.0 + i * 0.1, "high": 10.2 + i * 0.1, "low": 9.9 + i * 0.1, "close": 10.1 + i * 0.1, "volume": 1000.0, "vwap": 10.05 + i * 0.1, } for i in range(78) ] } } client = _StubOracleClient({"/api/v1/alpaca/intraday": historical_response}) cache = IntradayCache(str(tmp_path)) bars = asyncio.run( fetch_intraday_bulk( {today: ["AAA"]}, client, cache=cache, concurrency=1, ) ) assert list(bars[today]) == ["AAA"] assert client.calls == [ ( "/api/v1/alpaca/intraday", {"tickers": "AAA", "interval": "5m", "start_date": today, "end_date": today}, ) ] assert cache.has("AAA", today) is True def test_fetch_intraday_bulk_ignores_existing_today_cache_after_close(tmp_path, monkeypatch) -> None: from libs.intraday.cache import IntradayCache from libs.intraday.screener import fetch_intraday_bulk today = "2026-05-01" class AfterCloseDateTime(datetime): @classmethod def now(cls, tz=None): value = datetime(2026, 5, 1, 21, 15, tzinfo=ZoneInfo("America/New_York")) return value.astimezone(tz) if tz else value monkeypatch.setattr(screener_module, "datetime", AfterCloseDateTime) cache = IntradayCache(str(tmp_path)) cache.put( "AAA", today, [ { "timestamp": ( datetime.fromisoformat(f"{today}T13:30:00+00:00") + timedelta(minutes=5 * i) ).isoformat(), "open": 10.0, "high": 10.1, "low": 9.9, "close": 10.0, "volume": 1000.0, "vwap": 10.0, } for i in range(10) ], ) def historical_response(params: dict | None) -> dict: ticker = (params or {}).get("tickers", "") return { "bars": { ticker: [ { "timestamp": ( datetime.fromisoformat(f"{today}T13:30:00+00:00") + timedelta(minutes=5 * i) ).isoformat(), "open": 20.0, "high": 20.2, "low": 19.9, "close": 20.1, "volume": 2000.0, "vwap": 20.05, } for i in range(78) ] } } client = _StubOracleClient({"/api/v1/alpaca/intraday": historical_response}) bars = asyncio.run( fetch_intraday_bulk( {today: ["AAA"]}, client, cache=cache, concurrency=1, ) ) assert client.calls == [ ( "/api/v1/alpaca/intraday", {"tickers": "AAA", "interval": "5m", "start_date": today, "end_date": today}, ) ] assert bars[today]["AAA"][0]["open"] == 20.0 def test_fetch_intraday_bulk_rejects_truncated_today_after_close(tmp_path, monkeypatch) -> None: from libs.intraday.cache import IntradayCache from libs.intraday.screener import fetch_intraday_bulk today = "2026-05-01" class AfterCloseDateTime(datetime): @classmethod def now(cls, tz=None): value = datetime(2026, 5, 1, 21, 15, tzinfo=ZoneInfo("America/New_York")) return value.astimezone(tz) if tz else value monkeypatch.setattr(screener_module, "datetime", AfterCloseDateTime) def truncated_response(params: dict | None) -> dict: ticker = (params or {}).get("tickers", "") return { "bars": { ticker: [ { "timestamp": ( datetime.fromisoformat(f"{today}T13:30:00+00:00") + timedelta(minutes=5 * i) ).isoformat(), "open": 10.0, "high": 10.2, "low": 9.9, "close": 10.1, "volume": 1000.0, "vwap": 10.05, } for i in range(20) ] } } client = _StubOracleClient( { "/api/v1/alpaca/intraday": truncated_response, "/api/v1/alpaca/intraday/today": truncated_response, } ) cache = IntradayCache(str(tmp_path)) bars = asyncio.run( fetch_intraday_bulk( {today: ["AAA"]}, client, cache=cache, concurrency=1, ) ) assert "AAA" not in bars.get(today, {}) assert cache.has("AAA", today) is False assert client.calls == [ ( "/api/v1/alpaca/intraday", {"tickers": "AAA", "interval": "5m", "start_date": today, "end_date": today}, ), ( "/api/v1/alpaca/intraday/today", {"tickers": "AAA", "interval": "5m"}, ), ] def test_fetch_daily_bars_bulk_rebuilds_from_intraday_cache_when_oracle_unavailable(tmp_path) -> None: daily_cache = DailyBarCache(str(tmp_path / "daily")) intraday_cache = IntradayCache(str(tmp_path / "intraday")) intraday_cache.put( "AAA", "2026-01-02", [ { "timestamp": "2026-01-02T14:30:00+00:00", "open": 10.0, "high": 10.5, "low": 9.9, "close": 10.4, "volume": 100.0, "vwap": 10.2, }, { "timestamp": "2026-01-02T14:35:00+00:00", "open": 10.4, "high": 11.0, "low": 10.2, "close": 10.8, "volume": 150.0, "vwap": 10.7, }, ] * 5, ) intraday_cache.put( "AAA", "2026-01-03", [ { "timestamp": "2026-01-03T14:30:00+00:00", "open": 11.0, "high": 11.2, "low": 10.8, "close": 11.1, "volume": 120.0, "vwap": 11.0, }, { "timestamp": "2026-01-03T14:35:00+00:00", "open": 11.1, "high": 11.4, "low": 11.0, "close": 11.3, "volume": 180.0, "vwap": 11.2, }, ] * 5, ) client = _StubOracleClient({"/api/v1/price/data": RuntimeError("oracle down")}) bars = asyncio.run( fetch_daily_bars_bulk( ["AAA"], "2026-01-02", "2026-01-03", client, cache=daily_cache, intraday_cache_fallback=intraday_cache, concurrency=1, ) ) assert list(bars) == ["AAA"] assert bars["AAA"] == [ { "date": "2026-01-02", "open": 10.0, "high": 11.0, "low": 9.9, "close": 10.8, "volume": 1250.0, }, { "date": "2026-01-03", "open": 11.0, "high": 11.4, "low": 10.8, "close": 11.3, "volume": 1500.0, }, ] assert daily_cache.get("AAA", "2026-01-02", "2026-01-03") == bars["AAA"] def test_fetch_daily_bars_bulk_prefers_intraday_fallback_without_oracle_calls(tmp_path) -> None: intraday_cache = IntradayCache(str(tmp_path / "intraday")) intraday_cache.put( "AAA", "2026-01-02", [ { "timestamp": f"2026-01-02T14:{30 + i:02d}:00+00:00", "open": 10.0, "high": 10.0 + i * 0.1, "low": 9.8, "close": 10.0 + i * 0.1, "volume": 100.0 + i, "vwap": 10.0 + i * 0.05, } for i in range(10) ], ) client = _StubOracleClient({"/api/v1/price/data": RuntimeError("should not call")}) bars = asyncio.run( fetch_daily_bars_bulk( ["AAA"], "2026-01-02", "2026-01-02", client, intraday_cache_fallback=intraday_cache, prefer_intraday_fallback=True, concurrency=1, ) ) assert list(bars) == ["AAA"] assert client.calls == [] def test_fetch_daily_bars_bulk_repairs_sparse_intraday_fallback_before_using_it(tmp_path) -> None: intraday_cache = IntradayCache(str(tmp_path / "intraday")) for day in [f"2026-04-{6 + i:02d}" for i in range(11)]: intraday_cache.put( "AAA", day, [ { "timestamp": f"{day}T14:{30 + i:02d}:00+00:00", "open": 10.0, "high": 10.2, "low": 9.8, "close": 10.1, "volume": 100.0, "vwap": 10.0, } for i in range(10) ], ) daily_cache = DailyBarCache(str(tmp_path / "daily")) row_dates = [ (datetime(2026, 1, 20) + timedelta(days=i * 3)).date().isoformat() for i in range(24) ] + ["2026-04-20"] full_rows = [ { "date": day, "open": 100 + i, "high": 101 + i, "low": 99 + i, "close": 100.5 + i, "volume": 1000 + i, } for i, day in enumerate(row_dates) ] client = _StubOracleClient( { "/api/v1/price/data": { "bars": { "AAA": full_rows, } } } ) bars = asyncio.run( fetch_daily_bars_bulk( ["AAA"], "2026-01-20", "2026-04-20", client, cache=daily_cache, intraday_cache_fallback=intraday_cache, prefer_intraday_fallback=True, concurrency=1, ) ) assert len(bars["AAA"]) == len(full_rows) assert [call[0] for call in client.calls] == ["/api/v1/price/data"] assert daily_cache.get("AAA", "2026-01-20", "2026-04-20") == [ { "date": row["date"], "open": float(row["open"]), "high": float(row["high"]), "low": float(row["low"]), "close": float(row["close"]), "volume": float(row["volume"]), } for row in full_rows ] def test_fetch_daily_bars_bulk_skips_oracle_misses_when_health_check_fails(tmp_path) -> None: intraday_cache = IntradayCache(str(tmp_path / "intraday")) intraday_cache.put( "AAA", "2026-01-02", [ { "timestamp": f"2026-01-02T14:{30 + i:02d}:00+00:00", "open": 10.0, "high": 10.2, "low": 9.8, "close": 10.1, "volume": 100.0, "vwap": 10.0, } for i in range(10) ], ) client = _StubOracleClient({"/api/v1/price/data": RuntimeError("should not call")}, health_ok=False) bars = asyncio.run( fetch_daily_bars_bulk( ["AAA", "BBB"], "2026-01-02", "2026-01-02", client, intraday_cache_fallback=intraday_cache, prefer_intraday_fallback=True, skip_oracle_when_unhealthy=True, concurrency=1, ) ) assert list(bars) == ["AAA"] assert client.calls == [] def test_fetch_intraday_bulk_skips_uncached_pairs_when_oracle_health_fails(tmp_path) -> None: from libs.intraday.screener import fetch_intraday_bulk cache = IntradayCache(str(tmp_path)) cache.put( "AAA", "2026-01-02", [ { "timestamp": f"2026-01-02T14:{30 + i:02d}:00+00:00", "open": 10.0, "high": 10.5, "low": 9.8, "close": 10.3, "volume": 1000.0, "vwap": 10.2, } for i in range(10) ], ) client = _StubOracleClient({"/api/v1/alpaca/intraday": RuntimeError("should not call")}, health_ok=False) bars = asyncio.run( fetch_intraday_bulk( {"2026-01-02": ["AAA", "BBB"]}, client, cache=cache, skip_oracle_when_unhealthy=True, concurrency=1, ) ) assert list(bars["2026-01-02"]) == ["AAA"] assert client.calls == [] def test_resolve_universe_screener_uses_snapshot_fallback(tmp_path, monkeypatch) -> None: monkeypatch.setattr( screener_module, "get_settings", lambda: SimpleNamespace(data_root=str(tmp_path)), ) params = UniverseParams( source="screener", market_cap_min=100_000_000.0, avg_volume_min=200_000, min_price=2.0, ) async def first_search(self, **kwargs): return [ SimpleNamespace(symbol="TSLA"), SimpleNamespace(symbol="NVDA"), SimpleNamespace(symbol="TSLA"), ] async def failing_search(self, **kwargs): raise RuntimeError("screener down") monkeypatch.setattr(screener_module.ScreenerService, "search_all_stocks", first_search) first = asyncio.run(resolve_universe(params, client=object())) assert first == ["NVDA", "TSLA"] monkeypatch.setattr(screener_module.ScreenerService, "search_all_stocks", failing_search) second = asyncio.run(resolve_universe(params, client=object())) assert second == ["NVDA", "TSLA"] def test_resolve_universe_broad_uses_named_yaml_snapshot() -> None: params = UniverseParams(source="broad") symbols = asyncio.run(resolve_universe(params, client=object())) assert "AAPL" in symbols assert "TSLA" in symbols assert len(symbols) > 3000