from __future__ import annotations import asyncio import pickle from datetime import datetime from types import SimpleNamespace from zoneinfo import ZoneInfo import apps.intraday_bt.run as run_mod from apps.intraday_bt.run import ( _augment_momentum_seed_candidates_with_liquid_overlay, _chunk_trading_days_by_pairs, _fetch_vix_by_day, _latest_backtest_date, _latest_completed_trading_day, _load_vix_from_local_macro_snapshots, _normalize_candidate_map, _momentum_intraday_seed_candidates, _momentum_strategy_uses_candidate_stage_catalyst, _momentum_strategy_uses_daily_enrichment, _momentum_strategy_requires_regime_ticker_daily, _momentum_strategy_uses_attention, _momentum_strategy_uses_catalyst, _momentum_strategy_uses_sector_labels, _momentum_strategy_uses_sector_proxies, _retain_recent_intraday_shortlist, _recent_intraday_first_candidates, _strategy_for_recent_live_scan, _should_use_recent_live_scan, _should_use_recent_intraday_first_scan, apply_cli_overrides, get_trading_days, load_config, ) from libs.intraday.domain import StrategyParams def test_get_trading_days_uses_local_calendar_for_explicit_range() -> None: days = asyncio.run(get_trading_days(None, "2026-01-01", "2026-01-10", lookback=0)) assert days == [ "2026-01-02", "2026-01-05", "2026-01-06", "2026-01-07", "2026-01-08", "2026-01-09", ] def test_get_trading_days_trims_to_lookback_when_start_not_pinned() -> None: days = asyncio.run(get_trading_days(None, None, "2026-01-10", lookback=3)) assert days == [ "2026-01-07", "2026-01-08", "2026-01-09", ] def test_chunk_trading_days_by_pairs_keeps_days_contiguous_and_bounded() -> None: trading_days = [ "2026-01-05", "2026-01-06", "2026-01-07", "2026-01-08", ] candidates = { "2026-01-05": ["A"] * 2000, "2026-01-06": ["B"] * 2000, "2026-01-07": ["C"] * 1500, "2026-01-08": ["D"] * 2500, } chunks = _chunk_trading_days_by_pairs(trading_days, candidates, max_pairs_per_chunk=3500) assert chunks == [ ["2026-01-05"], ["2026-01-06", "2026-01-07"], ["2026-01-08"], ] def test_normalize_candidate_map_unwraps_tuple_payload() -> None: payload = ({"2026-01-05": ["A", "B"], "2026-01-06": []}, {"2026-01-05": {"A"}}) normalized = _normalize_candidate_map(payload) assert normalized == {"2026-01-05": ["A", "B"]} def test_latest_backtest_date_excludes_today_before_close() -> None: now_et = datetime(2026, 4, 14, 12, 0, tzinfo=ZoneInfo("America/New_York")) latest = _latest_backtest_date(now_et) assert latest.isoformat() == "2026-04-13" def test_latest_backtest_date_includes_today_after_close() -> None: now_et = datetime(2026, 4, 14, 16, 1, tzinfo=ZoneInfo("America/New_York")) latest = _latest_backtest_date(now_et) assert latest.isoformat() == "2026-04-14" def test_latest_backtest_date_uses_last_session_on_weekend() -> None: now_et = datetime(2026, 4, 18, 10, 0, tzinfo=ZoneInfo("America/New_York")) latest = _latest_backtest_date(now_et) assert latest.isoformat() == "2026-04-18" def test_latest_completed_trading_day_walks_back_on_weekend() -> None: now_et = datetime(2026, 4, 18, 10, 0, tzinfo=ZoneInfo("America/New_York")) latest = _latest_completed_trading_day(now_et) assert latest.isoformat() == "2026-04-17" def test_load_vix_from_local_macro_snapshots_uses_covering_file(tmp_path, monkeypatch) -> None: parquet_dir = tmp_path / "parquet" / "sample" parquet_dir.mkdir(parents=True, exist_ok=True) payload = { datetime(2025, 1, 2).date(): {"VIXCLS": 17.1}, datetime(2025, 1, 3).date(): {"VIXCLS": 18.2}, datetime(2025, 1, 6).date(): {"VIXCLS": 19.3}, } path = parquet_dir / "macro_window_2025-01-01_2025-01-10.pkl" with path.open("wb") as fh: pickle.dump(payload, fh, protocol=pickle.HIGHEST_PROTOCOL) monkeypatch.setattr( run_mod, "get_settings", lambda: SimpleNamespace(data_root=str(tmp_path)), ) result = _load_vix_from_local_macro_snapshots(["2025-01-02", "2025-01-03", "2025-01-06"]) assert result == { "2025-01-02": 17.1, "2025-01-03": 18.2, "2025-01-06": 19.3, } def test_fetch_vix_by_day_uses_local_snapshot_when_health_check_fails(tmp_path, monkeypatch) -> None: parquet_dir = tmp_path / "parquet" / "sample" parquet_dir.mkdir(parents=True, exist_ok=True) payload = { datetime(2025, 1, 2).date(): {"VIXCLS": 17.1}, datetime(2025, 1, 3).date(): {"VIXCLS": 18.2}, } path = parquet_dir / "macro_window_2025-01-01_2025-01-10.pkl" with path.open("wb") as fh: pickle.dump(payload, fh, protocol=pickle.HIGHEST_PROTOCOL) monkeypatch.setattr( run_mod, "get_settings", lambda: SimpleNamespace(data_root=str(tmp_path)), ) class _Client: async def health_check_fast(self, timeout: float = 3.0) -> bool: return False result = asyncio.run(_fetch_vix_by_day(_Client(), ["2025-01-02", "2025-01-03"])) assert result == { "2025-01-02": 17.1, "2025-01-03": 18.2, } def test_load_ticker_sectors_with_oracle_backfills_missing_cache(tmp_path, monkeypatch) -> None: cache_path = tmp_path / "cache" / "sector_cache.json" cache_path.parent.mkdir(parents=True, exist_ok=True) cache_path.write_text('{"TSLA": "Consumer Cyclical"}') monkeypatch.setattr( run_mod, "get_settings", lambda: SimpleNamespace(data_root=str(tmp_path)), ) class FakeCompanyService: def __init__(self, client) -> None: self.client = client async def get_company(self, symbol: str): if symbol == "CPRX": return SimpleNamespace( sector="Healthcare", industry="Biotechnology", exchange="NASDAQ", market_cap=2_979_108_098.0, ) raise AssertionError(f"unexpected symbol {symbol}") monkeypatch.setattr(run_mod, "CompanyService", FakeCompanyService) result = asyncio.run( run_mod._load_ticker_sectors_with_oracle(["TSLA", "CPRX"], client=object()) ) assert result == { "TSLA": "Consumer Cyclical", "CPRX": "Healthcare", } assert cache_path.exists() assert '"CPRX": "Healthcare"' in cache_path.read_text() def test_load_ticker_sectors_with_oracle_skips_placeholder_metadata(tmp_path, monkeypatch) -> None: cache_path = tmp_path / "cache" / "sector_cache.json" cache_path.parent.mkdir(parents=True, exist_ok=True) cache_path.write_text("{}") monkeypatch.setattr( run_mod, "get_settings", lambda: SimpleNamespace(data_root=str(tmp_path)), ) class FakeCompanyService: def __init__(self, client) -> None: self.client = client async def get_company(self, symbol: str): return SimpleNamespace( sector="Technology", industry="Software", exchange=None, market_cap=None, ) monkeypatch.setattr(run_mod, "CompanyService", FakeCompanyService) result = asyncio.run( run_mod._load_ticker_sectors_with_oracle(["AVGO"], client=object()) ) assert result == {"AVGO": "UNKNOWN"} assert cache_path.read_text() == "{}" def test_should_use_recent_live_scan_only_for_small_recent_windows(monkeypatch) -> None: monkeypatch.setattr( run_mod, "_latest_backtest_date", lambda now_et=None: datetime(2026, 4, 16, tzinfo=ZoneInfo("America/New_York")).date(), ) strategy = StrategyParams(recent_live_scan_days=5) assert _should_use_recent_live_scan(strategy, ["2026-04-15", "2026-04-16"]) is True assert _should_use_recent_live_scan( strategy, ["2026-04-09", "2026-04-10", "2026-04-13", "2026-04-14", "2026-04-15", "2026-04-16"], ) is False assert _should_use_recent_live_scan(strategy, ["2026-03-01"]) is False def test_momentum_strategy_uses_intraday_event_weight_for_fetch_activation() -> None: strategy = StrategyParams(candidate_intraday_weight_event_score=0.05) assert _momentum_strategy_uses_catalyst(strategy) is True def test_momentum_strategy_uses_seed_event_overlay_for_candidate_stage_catalyst() -> None: strategy = StrategyParams(candidate_seed_event_overlay_slots=2) assert _momentum_strategy_uses_catalyst(strategy) is True assert _momentum_strategy_uses_candidate_stage_catalyst(strategy) is True def test_momentum_strategy_uses_candidate_event_type_filter_for_fetch_activation() -> None: strategy = StrategyParams(candidate_allowed_event_types=["earnings_release"]) assert _momentum_strategy_uses_catalyst(strategy) is True assert _momentum_strategy_uses_candidate_stage_catalyst(strategy) is True def test_momentum_strategy_uses_event_reserve_and_event_sleeve_for_fetch_activation() -> None: reserve_strategy = StrategyParams(candidate_intraday_event_reserve_slots=1) sleeve_strategy = StrategyParams(use_event_sleeve=True, event_weight=0.1) event_day_liquid_strategy = StrategyParams( use_event_day_liquid_sleeve=True, event_day_liquid_capital_fraction=0.1, event_day_liquid_max_positions=1, ) assert _momentum_strategy_uses_catalyst(reserve_strategy) is True assert _momentum_strategy_uses_candidate_stage_catalyst(reserve_strategy) is False assert _momentum_strategy_uses_catalyst(sleeve_strategy) is True assert _momentum_strategy_uses_catalyst(event_day_liquid_strategy) is True def test_momentum_strategy_uses_intraday_attention_weight_for_fetch_activation() -> None: strategy = StrategyParams(candidate_intraday_weight_attention_news=0.03) assert _momentum_strategy_uses_attention(strategy) is True def test_momentum_strategy_requires_regime_ticker_daily_for_gap_meta_layer() -> None: strategy = StrategyParams(regime_size_scale_low=-0.01) assert _momentum_strategy_requires_regime_ticker_daily(strategy) is True def test_momentum_strategy_uses_sector_metadata_and_proxy_fetch_for_overlay_engines() -> None: cluster_strategy = StrategyParams(use_liquid_cluster_engine=True) etf_strategy = StrategyParams( use_sector_etf_sleeve=True, sector_etf_capital_fraction=0.2, sector_etf_max_positions=1, ) assert _momentum_strategy_uses_daily_enrichment(cluster_strategy) is True assert _momentum_strategy_uses_sector_labels(cluster_strategy) is True assert _momentum_strategy_uses_sector_proxies(cluster_strategy) is False assert _momentum_strategy_uses_sector_labels(etf_strategy) is True assert _momentum_strategy_uses_sector_proxies(etf_strategy) is True def test_momentum_intraday_seed_candidates_only_apply_signal_filters_in_final_pass() -> None: daily_bars = { "AAA": [ {"date": "2026-01-05", "open": 10.0}, ], "BBB": [ {"date": "2026-01-05", "open": 10.0}, ], } enrichment = { "AAA": { "2026-01-05": { "gap_pct": 0.03, "event_flag": True, "event_score": 1.0, "ret_5d": 0.01, "entropy_20d": 0.4, "avg_dollar_vol_30d": 1_000_000.0, "atr_14": 1.0, } }, "BBB": { "2026-01-05": { "gap_pct": 0.04, "event_flag": False, "event_score": 0.0, "ret_5d": 0.01, "entropy_20d": 0.4, "avg_dollar_vol_30d": 1_000_000.0, "atr_14": 1.0, } }, } strategy = StrategyParams( candidate_source_mode="intraday_first", candidate_seed_threshold=0.02, candidate_seed_max_per_day=1, candidate_require_event_flag=True, candidate_weight_event_score=1.0, ) preliminary = _momentum_intraday_seed_candidates( daily_bars, ["2026-01-05"], enrichment, strategy, default_threshold=0.02, use_signal_features=False, ) final_seed = _momentum_intraday_seed_candidates( daily_bars, ["2026-01-05"], enrichment, strategy, default_threshold=0.02, use_signal_features=True, ) assert preliminary == {"2026-01-05": ["BBB"]} assert final_seed == {"2026-01-05": ["AAA"]} def test_recent_intraday_first_candidates_uses_intraday_leaders_without_static_universe() -> None: strategy = StrategyParams( entry_minutes_after_open=10, confirmation_minutes_after_entry=5, min_entry_volume=250000, min_entry_dollar_volume=2_000_000, top_n=8, recent_live_scan_max_candidates_per_day=10, ) day = "2026-04-16" bars = { day: { "XNDU": [ {"timestamp": "2026-04-16T13:30:00+00:00", "open": 2.00, "high": 2.06, "low": 1.98, "close": 2.05, "volume": 150000}, {"timestamp": "2026-04-16T13:35:00+00:00", "open": 2.05, "high": 2.12, "low": 2.04, "close": 2.11, "volume": 175000}, {"timestamp": "2026-04-16T13:40:00+00:00", "open": 2.11, "high": 2.15, "low": 2.10, "close": 2.14, "volume": 180000}, {"timestamp": "2026-04-16T13:45:00+00:00", "open": 2.14, "high": 2.20, "low": 2.13, "close": 2.19, "volume": 200000}, {"timestamp": "2026-04-16T13:50:00+00:00", "open": 2.19, "high": 2.24, "low": 2.18, "close": 2.22, "volume": 210000}, ], "SLOW": [ {"timestamp": "2026-04-16T13:30:00+00:00", "open": 20.00, "high": 20.01, "low": 19.95, "close": 19.98, "volume": 50000}, {"timestamp": "2026-04-16T13:35:00+00:00", "open": 19.98, "high": 20.00, "low": 19.90, "close": 19.95, "volume": 50000}, {"timestamp": "2026-04-16T13:40:00+00:00", "open": 19.95, "high": 19.99, "low": 19.92, "close": 19.97, "volume": 50000}, {"timestamp": "2026-04-16T13:45:00+00:00", "open": 19.97, "high": 19.98, "low": 19.94, "close": 19.96, "volume": 50000}, {"timestamp": "2026-04-16T13:50:00+00:00", "open": 19.96, "high": 19.97, "low": 19.93, "close": 19.95, "volume": 50000}, {"timestamp": "2026-04-16T13:55:00+00:00", "open": 19.95, "high": 19.96, "low": 19.92, "close": 19.94, "volume": 50000}, ], "LIQUID": [ {"timestamp": "2026-04-16T13:30:00+00:00", "open": 400.00, "high": 401.00, "low": 399.00, "close": 400.20, "volume": 80000}, {"timestamp": "2026-04-16T13:35:00+00:00", "open": 400.20, "high": 401.20, "low": 400.10, "close": 400.80, "volume": 90000}, {"timestamp": "2026-04-16T13:40:00+00:00", "open": 400.80, "high": 402.50, "low": 400.70, "close": 402.20, "volume": 120000}, {"timestamp": "2026-04-16T13:45:00+00:00", "open": 402.20, "high": 403.20, "low": 401.90, "close": 402.80, "volume": 130000}, {"timestamp": "2026-04-16T13:50:00+00:00", "open": 402.80, "high": 404.00, "low": 402.70, "close": 403.60, "volume": 140000}, {"timestamp": "2026-04-16T13:55:00+00:00", "open": 403.60, "high": 404.20, "low": 403.40, "close": 404.00, "volume": 150000}, ], "MEGA": [ {"timestamp": "2026-04-16T13:30:00+00:00", "open": 500.00, "high": 500.80, "low": 499.50, "close": 500.10, "volume": 120000}, {"timestamp": "2026-04-16T13:35:00+00:00", "open": 500.10, "high": 500.90, "low": 500.00, "close": 500.40, "volume": 140000}, {"timestamp": "2026-04-16T13:40:00+00:00", "open": 500.40, "high": 501.10, "low": 500.20, "close": 500.70, "volume": 150000}, {"timestamp": "2026-04-16T13:45:00+00:00", "open": 500.70, "high": 501.30, "low": 500.50, "close": 500.90, "volume": 160000}, {"timestamp": "2026-04-16T13:50:00+00:00", "open": 500.90, "high": 501.50, "low": 500.80, "close": 501.20, "volume": 180000}, {"timestamp": "2026-04-16T13:55:00+00:00", "open": 501.20, "high": 501.70, "low": 501.00, "close": 501.40, "volume": 190000}, ], } } candidates = _recent_intraday_first_candidates(bars, [day], strategy) assert "XNDU" in candidates[day] assert "LIQUID" in candidates[day] assert "MEGA" in candidates[day] assert "SLOW" not in candidates[day] def test_recent_intraday_first_scan_only_applies_to_latest_safe_day(monkeypatch) -> None: assert _should_use_recent_intraday_first_scan(["2026-04-16"]) is True assert _should_use_recent_intraday_first_scan(["2026-04-15"]) is True def test_recent_intraday_first_scan_treats_latest_completed_weekday_as_recent(monkeypatch) -> None: assert _should_use_recent_intraday_first_scan(["2026-04-17"]) is True def test_recent_intraday_first_scan_false_for_empty_window() -> None: assert _should_use_recent_intraday_first_scan([]) is False def test_liquid_seed_overlay_adds_liquid_name_without_replacing_base_seed() -> None: strategy = StrategyParams( candidate_seed_liquid_overlay_slots=1, candidate_seed_liquid_min_gap_pct=0.005, candidate_seed_liquid_max_gap_pct=0.03, candidate_seed_liquid_min_avg_dollar_vol_30d=500_000_000.0, candidate_seed_liquid_min_ret_5d=0.0, candidate_seed_liquid_max_entropy_20d=0.90, ) daily_bars = { "BASE": [{"date": "2026-04-15", "open": 50.0}], "TSLA": [{"date": "2026-04-15", "open": 250.0}], "NOPE": [{"date": "2026-04-15", "open": 30.0}], } enrichment = { "BASE": {"2026-04-15": {"gap_pct": 0.03, "avg_dollar_vol_30d": 200_000_000.0, "ret_5d": 0.02, "entropy_20d": 0.40}}, "TSLA": {"2026-04-15": {"gap_pct": 0.007, "avg_dollar_vol_30d": 1_200_000_000.0, "ret_5d": 0.05, "entropy_20d": 0.70}}, "NOPE": {"2026-04-15": {"gap_pct": 0.009, "avg_dollar_vol_30d": 200_000_000.0, "ret_5d": 0.05, "entropy_20d": 0.50}}, } candidates = {"2026-04-15": ["BASE"]} augmented, overlay = _augment_momentum_seed_candidates_with_liquid_overlay( candidates, daily_bars, ["2026-04-15"], enrichment, strategy, ) assert augmented["2026-04-15"] == ["BASE", "TSLA"] def test_strategy_for_recent_live_scan_applies_only_recent_overrides() -> None: strategy = StrategyParams( top_n=8, min_morning_gain_pct=0.015, max_morning_gain_pct=0.06, min_confirmation_return_pct=0.005, max_gap_pct=0.055, use_slow_ignite_sleeve=False, recent_live_scan_top_n=10, recent_live_scan_min_morning_gain_pct=0.01, recent_live_scan_max_morning_gain_pct=0.05, recent_live_scan_min_confirmation_return_pct=0.001, recent_live_scan_max_gap_pct=0.04, recent_live_scan_max_entropy_20d=0.9, recent_live_scan_use_slow_ignite_sleeve=True, recent_live_scan_slow_ignite_weight=0.2, recent_live_scan_use_liquid_largecap_sleeve=True, recent_live_scan_liquid_largecap_weight=0.3, recent_live_scan_liquid_largecap_min_gain_pct=0.004, recent_live_scan_liquid_largecap_max_gain_pct=0.02, recent_live_scan_liquid_largecap_min_confirmation_return_pct=0.0005, recent_live_scan_liquid_largecap_min_entry_dollar_volume=50_000_000.0, recent_live_scan_liquid_largecap_min_avg_dollar_vol_30d=500_000_000.0, recent_live_scan_liquid_largecap_max_entropy_20d=0.9, ) unchanged = _strategy_for_recent_live_scan(strategy, recent_live_scan=False) recent = _strategy_for_recent_live_scan(strategy, recent_live_scan=True) assert unchanged.top_n == 8 assert unchanged.min_morning_gain_pct == 0.015 assert unchanged.max_morning_gain_pct == 0.06 assert unchanged.max_gap_pct == 0.055 assert unchanged.use_slow_ignite_sleeve is False assert recent.top_n == 10 assert recent.min_morning_gain_pct == 0.01 assert recent.max_morning_gain_pct == 0.05 assert recent.min_confirmation_return_pct == 0.001 assert recent.max_gap_pct == 0.04 assert recent.max_entropy_20d == 0.9 assert recent.use_slow_ignite_sleeve is True assert recent.use_liquid_largecap_sleeve is True assert recent.liquid_largecap_weight == 0.3 assert recent.liquid_largecap_min_gain_pct == 0.004 assert recent.liquid_largecap_max_gain_pct == 0.02 assert recent.liquid_largecap_min_confirmation_return_pct == 0.0005 assert recent.slow_ignite_weight == 0.2 def test_augment_momentum_seed_candidates_with_leader_overlay_adds_negative_gap_continuation_name() -> None: strategy = StrategyParams( candidate_seed_leader_overlay_slots=1, candidate_seed_leader_min_gap_pct=-0.01, candidate_seed_leader_max_gap_pct=0.01, candidate_seed_leader_min_avg_dollar_vol_30d=500_000_000.0, candidate_seed_leader_min_ret_5d=0.15, candidate_seed_leader_min_atr_pct=0.05, candidate_seed_leader_max_entropy_20d=0.75, ) daily_bars = { "BASE": [{"date": "2026-04-20", "open": 50.0}], "CAR": [{"date": "2026-04-20", "open": 491.26}], "NOPE": [{"date": "2026-04-20", "open": 300.0}], } enrichment = { "BASE": {"2026-04-20": {"gap_pct": 0.03, "avg_dollar_vol_30d": 200_000_000.0, "ret_5d": 0.02, "entropy_20d": 0.40, "atr_14": 2.0}}, "CAR": {"2026-04-20": {"gap_pct": -0.005, "avg_dollar_vol_30d": 730_000_000.0, "ret_5d": 0.33, "entropy_20d": 0.44, "atr_14": 51.8}}, "NOPE": {"2026-04-20": {"gap_pct": 0.0, "avg_dollar_vol_30d": 450_000_000.0, "ret_5d": 0.18, "entropy_20d": 0.55, "atr_14": 5.0}}, } candidates = {"2026-04-20": ["BASE"]} augmented, overlay = _augment_momentum_seed_candidates_with_liquid_overlay( candidates, daily_bars, ["2026-04-20"], enrichment, strategy, ) assert augmented["2026-04-20"] == ["BASE", "CAR"] def test_augment_momentum_seed_candidates_with_moderate_liquid_overlay_adds_followthrough_name() -> None: strategy = StrategyParams( candidate_seed_moderate_liquid_overlay_slots=1, candidate_seed_moderate_liquid_min_gap_pct=0.005, candidate_seed_moderate_liquid_max_gap_pct=0.025, candidate_seed_moderate_liquid_min_avg_dollar_vol_30d=250_000_000.0, candidate_seed_moderate_liquid_max_avg_dollar_vol_30d=2_000_000_000.0, candidate_seed_moderate_liquid_max_entropy_20d=0.86, ) daily_bars = { "BASE": [{"date": "2026-03-13", "open": 50.0}], "TER": [{"date": "2026-03-13", "open": 100.0}], "MEGA": [{"date": "2026-03-13", "open": 250.0}], } enrichment = { "BASE": {"2026-03-13": {"gap_pct": 0.04, "avg_dollar_vol_30d": 150_000_000.0, "ret_5d": 0.02, "entropy_20d": 0.40}}, "TER": {"2026-03-13": {"gap_pct": 0.012, "avg_dollar_vol_30d": 750_000_000.0, "ret_5d": -0.02, "entropy_20d": 0.79}}, "MEGA": {"2026-03-13": {"gap_pct": 0.010, "avg_dollar_vol_30d": 5_000_000_000.0, "ret_5d": 0.01, "entropy_20d": 0.50}}, } candidates = {"2026-03-13": ["BASE"]} augmented, overlay = _augment_momentum_seed_candidates_with_liquid_overlay( candidates, daily_bars, ["2026-03-13"], enrichment, strategy, ) assert augmented["2026-03-13"] == ["BASE", "TER"] assert overlay == {"2026-03-13": {"TER"}} assert enrichment["TER"]["2026-03-13"]["candidate_seed_moderate_liquid_overlay"] is True def test_augment_momentum_seed_candidates_with_event_overlay_adds_actual_catalyst_name() -> None: strategy = StrategyParams( candidate_seed_event_overlay_slots=1, candidate_seed_event_min_score=0.95, candidate_seed_event_min_gap_pct=-0.02, candidate_seed_event_max_gap_pct=0.08, candidate_seed_event_min_avg_dollar_vol_30d=100_000_000.0, candidate_seed_event_max_entropy_20d=0.80, ) daily_bars = { "BASE": [{"date": "2026-02-10", "open": 50.0}], "CAT": [{"date": "2026-02-10", "open": 42.0}], "WEAK": [{"date": "2026-02-10", "open": 30.0}], } enrichment = { "BASE": {"2026-02-10": {"gap_pct": 0.04, "avg_dollar_vol_30d": 200_000_000.0, "ret_5d": 0.03, "entropy_20d": 0.40}}, "CAT": {"2026-02-10": {"event_flag": True, "event_score": 1.0, "gap_pct": 0.01, "avg_dollar_vol_30d": 350_000_000.0, "ret_5d": 0.08, "entropy_20d": 0.55}}, "WEAK": {"2026-02-10": {"event_flag": True, "event_score": 0.60, "gap_pct": 0.015, "avg_dollar_vol_30d": 120_000_000.0, "ret_5d": 0.02, "entropy_20d": 0.50}}, } candidates = {"2026-02-10": ["BASE"]} augmented, overlay = _augment_momentum_seed_candidates_with_liquid_overlay( candidates, daily_bars, ["2026-02-10"], enrichment, strategy, ) assert augmented["2026-02-10"] == ["BASE", "CAT"] assert overlay == {"2026-02-10": {"CAT"}} def test_retain_recent_intraday_shortlist_preserves_intraday_candidates() -> None: candidates = {"2026-04-15": ["TSLA", "XNDU", "AXTI"]} daily_bars = {"TSLA": [{"date": "2026-04-15"}], "AXTI": [{"date": "2026-04-15"}]} retained = _retain_recent_intraday_shortlist( candidates, daily_bars, require_daily_features=True, ) assert retained == {"2026-04-15": ["TSLA", "AXTI"]} def test_momentum_strategy_defaults_to_simple_returns_and_cli_can_override() -> None: config = load_config( "configs/intraday/strategies/leader_intraday_momentum_high_wr_intraday_first.yaml" ) assert config.strategy.compound_returns is False args = SimpleNamespace( days=None, start=None, end=None, universe=None, top_n=None, stop_loss=None, entry_min=None, exit_min=None, min_gain=None, no_cache=False, verbose=False, output_dir=None, strategy="momentum", compound_returns=True, initial_capital=None, ) updated = apply_cli_overrides(config, args) assert updated.strategy.compound_returns is True