from __future__ import annotations import datetime as dt import pytest from libs.intraday.domain import ORBStrategyParams from libs.intraday.orb_simulator import ( ORBSimulationState, _build_idle_sleeve_orders, _entry_market_guard_scale, _market_return_at_entry_open, _orb_form4_size_scale, _orb_liquid_leader_conviction_size_scale, _orb_opening_burst_liquid_size_scale, _orb_ownership_initial_size_scale, _orb_sector_confirmation_size_scale, _orb_soft_day_sector_confirmation_override_allows, _orb_soft_day_sector_confirmation_override_base_sizing, _orb_soft_day_setup_profile_allows, _orb_soft_day_vwap_min_score_pct, _orb_soft_day_vwap_reason_allowed, _orb_soft_day_vwap_reason_param, _orb_soft_day_vwap_size_scale, _orb_unboosted_primary_fragility_size_scale, _orb_unsupported_attention_size_scale, compute_orb_candidates, run_orb_simulation, run_orb_simulation_with_state, simulate_orb_day, simulate_orb_trade, ) def _enrichment_for(*tickers: str) -> dict[str, dict[str, dict]]: return { ticker: { "2026-01-05": { "atr_14": 1.0, "avg_dollar_vol_30d": 1_000_000_000.0, "avg_daily_vol_14d": 10_000.0, "prev_close": 99.0, "today_open": 100.0, "entropy_20d": 0.5, "atr_ratio_10_60": 0.8, "range_compression_10_60": 0.7, "gap_zscore_20d": 0.2, } } for ticker in tickers } def _parse_test_ts(value: str) -> dt.datetime: return dt.datetime.fromisoformat(value) def _idle_bars(open_price: float, close_price: float) -> list[dict]: return [ { "timestamp": "2026-01-05T09:30:00-05:00", "open": open_price, "high": max(open_price, close_price), "low": min(open_price, close_price), "close": open_price, "volume": 100_000, }, { "timestamp": "2026-01-05T15:55:00-05:00", "open": close_price, "high": close_price, "low": close_price, "close": close_price, "volume": 100_000, }, ] def _idle_reclaim_bars(open_price: float, checkpoint_close: float, close_price: float) -> list[dict]: return [ { "timestamp": "2026-01-05T09:30:00-05:00", "open": open_price, "high": max(open_price, checkpoint_close), "low": min(open_price, checkpoint_close), "close": open_price, "volume": 100_000, }, { "timestamp": "2026-01-05T10:30:00-05:00", "open": open_price, "high": max(open_price, checkpoint_close), "low": min(open_price, checkpoint_close), "close": checkpoint_close, "volume": 100_000, }, { "timestamp": "2026-01-05T15:55:00-05:00", "open": checkpoint_close, "high": max(open_price, checkpoint_close, close_price), "low": min(open_price, checkpoint_close, close_price), "close": close_price, "volume": 100_000, }, ] def test_pead_like_idle_parking_uses_tqqq_on_strong_market_close() -> None: params = ORBStrategyParams( orb_idle_sleeve_enabled=True, orb_idle_sleeve_parking_mode="pead_like", orb_idle_sleeve_parking_symbols=["QQQM", "TQQQ"], orb_idle_sleeve_weights={"parking": 1.0}, orb_idle_sleeve_market_ticker="QQQ", orb_idle_sleeve_min_market_day_return_pct=-0.005, orb_idle_sleeve_parking_overlay_min_market_return_pct=0.005, orb_idle_sleeve_parking_overlay_min_market_close_location=0.70, ) orders = _build_idle_sleeve_orders( { "QQQ": _idle_bars(100.0, 101.0), "QQQM": _idle_bars(100.0, 100.4), "TQQQ": _idle_bars(100.0, 103.0), }, "2026-01-05", params, {}, ) assert [(order.ticker, order.sleeve) for order in orders] == [("TQQQ", "parking")] def test_pead_like_idle_parking_uses_qqqm_on_ordinary_risk_on_close() -> None: params = ORBStrategyParams( orb_idle_sleeve_enabled=True, orb_idle_sleeve_parking_mode="pead_like", orb_idle_sleeve_parking_symbols=["QQQM", "TQQQ"], orb_idle_sleeve_weights={"parking": 1.0}, orb_idle_sleeve_market_ticker="QQQ", orb_idle_sleeve_min_market_day_return_pct=-0.005, orb_idle_sleeve_parking_overlay_min_market_return_pct=0.005, orb_idle_sleeve_parking_overlay_min_market_close_location=0.70, ) orders = _build_idle_sleeve_orders( { "QQQ": _idle_bars(100.0, 100.1), "QQQM": _idle_bars(100.0, 100.2), "TQQQ": _idle_bars(100.0, 100.6), }, "2026-01-05", params, {}, ) assert [(order.ticker, order.sleeve) for order in orders] == [("QQQM", "parking")] def test_pead_like_idle_parking_falls_back_to_sgov_when_risk_on_disallowed() -> None: params = ORBStrategyParams( orb_idle_sleeve_enabled=True, orb_idle_sleeve_parking_mode="pead_like", orb_idle_sleeve_parking_symbols=["QQQM", "TQQQ"], orb_idle_sleeve_risk_off_symbols=["GLD", "SGOV"], orb_idle_sleeve_weights={"parking": 1.0}, orb_idle_sleeve_market_ticker="QQQ", orb_idle_sleeve_min_market_day_return_pct=-0.005, orb_idle_sleeve_force_defensive_fallback=True, ) orders = _build_idle_sleeve_orders( { "QQQ": _idle_bars(100.0, 99.0), "QQQM": _idle_bars(100.0, 99.5), "TQQQ": _idle_bars(100.0, 97.0), "GLD": _idle_bars(100.0, 101.0), "SGOV": _idle_bars(100.0, 100.01), }, "2026-01-05", params, {}, ) assert [(order.ticker, order.sleeve) for order in orders] == [("SGOV", "parking")] def test_idle_sleeve_can_build_pead_like_five_sleeve_book() -> None: params = ORBStrategyParams( orb_idle_sleeve_enabled=True, orb_idle_sleeve_parking_mode="pead_like", orb_idle_sleeve_parking_symbols=["QQQM", "TQQQ"], orb_idle_sleeve_risk_off_symbols=["GLD", "SGOV"], orb_idle_sleeve_weights={ "parking": 0.35, "idle_alpha": 0.30, "form4": 0.15, "ownership": 0.10, "risk_off_alpha": 0.10, }, orb_idle_sleeve_max_positions=5, orb_idle_sleeve_market_ticker="QQQ", orb_idle_sleeve_min_market_day_return_pct=-0.005, orb_idle_sleeve_min_market_day_return_for_stock_sleeves_pct=-0.005, ) enrichment = _enrichment_for("ALPHA", "F4", "OWN") enrichment["F4"]["2026-01-05"].update( { "form4_flag": True, "form4_owner_count": 2, "form4_c_suite_count": 1, "form4_total_value": 1_000_000, } ) enrichment["OWN"]["2026-01-05"].update( { "ownership_13dg_flag": True, "ownership_13dg_initial_flag": True, "ownership_13dg_strength_score": 3.0, } ) orders = _build_idle_sleeve_orders( { "QQQ": _idle_bars(100.0, 99.8), "QQQM": _idle_bars(100.0, 100.1), "TQQQ": _idle_bars(100.0, 100.3), "GLD": _idle_bars(100.0, 100.4), "SGOV": _idle_bars(100.0, 100.01), "ALPHA": _idle_bars(100.0, 102.0), "F4": _idle_bars(100.0, 100.2), "OWN": _idle_bars(100.0, 100.3), }, "2026-01-05", params, enrichment, ) assert {(order.ticker, order.sleeve) for order in orders} == { ("QQQM", "parking"), ("ALPHA", "idle_alpha"), ("F4", "form4"), ("OWN", "ownership"), ("GLD", "risk_off_alpha"), } def test_idle_sleeve_can_build_late_close_reclaim_book() -> None: params = ORBStrategyParams( orb_idle_sleeve_enabled=True, orb_idle_sleeve_weights={"close_reclaim": 1.0}, orb_idle_sleeve_reclaim_checkpoint_minutes=60, orb_idle_sleeve_reclaim_min_day_return_pct=0.004, orb_idle_sleeve_reclaim_max_early_return_pct=0.004, orb_idle_sleeve_reclaim_min_late_return_pct=0.006, orb_idle_sleeve_reclaim_min_close_location=0.70, orb_idle_sleeve_reclaim_min_day_dollar_vol=20_000_000, orb_idle_sleeve_reclaim_min_avg_dollar_vol=80_000_000, ) orders = _build_idle_sleeve_orders( { "RECL": _idle_reclaim_bars(100.0, 99.2, 102.0), "MORN": _idle_reclaim_bars(100.0, 102.0, 103.0), }, "2026-01-05", params, _enrichment_for("RECL", "MORN"), ) assert [(order.ticker, order.sleeve) for order in orders] == [("RECL", "close_reclaim")] def test_idle_sleeve_can_build_sector_rotation_book() -> None: params = ORBStrategyParams( orb_idle_sleeve_enabled=True, orb_idle_sleeve_weights={"sector_rotation": 1.0}, orb_idle_sleeve_sector_rotation_symbols=["XLK", "XLE"], orb_idle_sleeve_sector_rotation_min_day_return_pct=0.002, orb_idle_sleeve_sector_rotation_min_close_location=0.60, orb_idle_sleeve_min_market_day_return_pct=None, ) orders = _build_idle_sleeve_orders( { "XLK": _idle_bars(100.0, 100.3), "XLE": _idle_bars(100.0, 101.0), }, "2026-01-05", params, {}, ) assert [(order.ticker, order.sleeve) for order in orders] == [("XLE", "sector_rotation")] def test_idle_sleeve_opens_on_streaming_chunk_boundary() -> None: params = ORBStrategyParams( min_candidates_to_trade=99, initial_capital=10_000.0, daily_budget_reset=True, compound_returns=False, orb_idle_sleeve_enabled=True, orb_idle_sleeve_parking_mode="pead_like", orb_idle_sleeve_parking_symbols=["QQQM"], orb_idle_sleeve_weights={"parking": 1.0}, orb_idle_sleeve_market_ticker="QQQ", orb_idle_sleeve_min_market_day_return_pct=-0.005, ) day1 = "2026-01-05" day2 = "2026-01-06" all_intraday = { day1: { "QQQ": _idle_bars(100.0, 100.2), "QQQM": _idle_bars(100.0, 100.5), }, day2: { "QQQM": [ { "timestamp": f"{day2}T09:30:00-05:00", "open": 101.0, "high": 101.1, "low": 100.9, "close": 101.0, "volume": 100_000, } ], }, } chunk1, state = run_orb_simulation_with_state( {day1: all_intraday[day1]}, [day1], params, {}, next_trading_day_after_window=day2, ) chunk2, _ = run_orb_simulation_with_state( {day2: all_intraday[day2]}, [day2], params, {}, state=state, ) assert chunk1[0].entry_diagnostics["orb_idle_sleeve_symbols_opened"] == ["QQQM"] assert len(chunk2[0].trades) == 1 assert chunk2[0].trades[0].ticker == "QQQM" assert chunk2[0].trades[0].exit_reason == "idle_sleeve_next_open" def test_idle_sleeve_can_enter_after_open_and_exit_same_day_close() -> None: params = ORBStrategyParams( min_candidates_to_trade=99, initial_capital=10_000.0, daily_budget_reset=True, compound_returns=False, orb_idle_sleeve_enabled=True, orb_idle_sleeve_entry_timing="minutes_after_open", orb_idle_sleeve_entry_minutes_after_open=60, orb_idle_sleeve_exit_timing="same_day_close", orb_idle_sleeve_parking_mode="pead_like", orb_idle_sleeve_parking_symbols=["QQQM"], orb_idle_sleeve_weights={"parking": 1.0}, orb_idle_sleeve_market_ticker="QQQ", orb_idle_sleeve_min_market_day_return_pct=-0.005, ) day = "2026-01-05" bars = [ { "timestamp": f"{day}T09:30:00-05:00", "open": 100.0, "high": 100.2, "low": 99.9, "close": 100.1, "volume": 100_000, }, { "timestamp": f"{day}T10:30:00-05:00", "open": 100.1, "high": 100.6, "low": 100.0, "close": 100.5, "volume": 100_000, }, { "timestamp": f"{day}T15:55:00-05:00", "open": 100.5, "high": 101.1, "low": 100.4, "close": 101.0, "volume": 100_000, }, ] results = run_orb_simulation( {day: {"QQQ": bars, "QQQM": bars}}, [day], params, {}, ) assert len(results[0].trades) == 1 trade = results[0].trades[0] assert trade.ticker == "QQQM" assert trade.entry_time == f"{day}T10:30:00-05:00" assert trade.exit_time == f"{day}T15:55:00-05:00" assert trade.exit_reason == "idle_sleeve_same_day_close" assert trade.orb_idle_sleeve_overnight is False def test_idle_sleeve_same_day_stop_loss_exits_before_close() -> None: params = ORBStrategyParams( min_candidates_to_trade=99, initial_capital=10_000.0, daily_budget_reset=True, compound_returns=False, orb_idle_sleeve_enabled=True, orb_idle_sleeve_entry_timing="minutes_after_open", orb_idle_sleeve_entry_minutes_after_open=60, orb_idle_sleeve_exit_timing="same_day_close", orb_idle_sleeve_same_day_stop_loss_pct=-0.03, orb_idle_sleeve_parking_mode="pead_like", orb_idle_sleeve_parking_symbols=["QQQM"], orb_idle_sleeve_weights={"parking": 1.0}, orb_idle_sleeve_market_ticker="QQQ", orb_idle_sleeve_min_market_day_return_pct=-0.005, ) day = "2026-01-05" bars = [ { "timestamp": f"{day}T09:30:00-05:00", "open": 100.0, "high": 100.2, "low": 99.8, "close": 100.0, "volume": 100_000, }, { "timestamp": f"{day}T10:30:00-05:00", "open": 100.0, "high": 102.2, "low": 99.9, "close": 102.0, "volume": 100_000, }, { "timestamp": f"{day}T11:00:00-05:00", "open": 102.0, "high": 102.1, "low": 98.5, "close": 99.0, "volume": 100_000, }, { "timestamp": f"{day}T15:55:00-05:00", "open": 99.0, "high": 105.0, "low": 98.9, "close": 105.0, "volume": 100_000, }, ] results = run_orb_simulation( {day: {"QQQ": bars, "QQQM": bars}}, [day], params, {}, ) trade = results[0].trades[0] assert trade.exit_reason == "idle_sleeve_same_day_stop_loss" assert trade.exit_time == f"{day}T11:00:00-05:00" assert trade.exit_price == 98.94 assert trade.orb_idle_sleeve_overnight is False def test_soft_day_setup_profile_rejects_market_regime_only_fallback() -> None: params = ORBStrategyParams(soft_day_setup_profile="skip_day_reclaim_v1") cand = { "score": 1.05, "rvol": 10.0, "gap_pct": 0.03, "premarket_dollar_vol": 100_000_000, "body_ratio": 0.8, "close_location": 0.9, } enrich = {"ret_5d": 0.3} assert not _orb_soft_day_setup_profile_allows( params, "market_regime", cand, enrich ) def test_soft_day_setup_profile_allows_breadth_gap_down_momentum() -> None: params = ORBStrategyParams(soft_day_setup_profile="skip_day_reclaim_v1") cand = { "score": 0.70, "rvol": 18.0, "gap_pct": -0.04, "premarket_dollar_vol": 50_000_000, "body_ratio": 0.5, "close_location": 0.65, } enrich = {"ret_5d": 0.20} assert _orb_soft_day_setup_profile_allows(params, "breadth", cand, enrich) def test_soft_day_vwap_reason_overrides_default_score_and_size() -> None: params = ORBStrategyParams( soft_day_vwap_reclaim_min_score_pct=0.85, soft_day_vwap_reclaim_min_score_pct_market_regime=0.70, soft_day_vwap_reclaim_min_score_pct_breadth=0.80, soft_day_vwap_reclaim_min_score_pct_joint=0.90, soft_day_vwap_reclaim_size_scale=0.03, soft_day_vwap_reclaim_size_scale_market_regime=0.05, soft_day_vwap_reclaim_size_scale_joint=0.02, ) assert _orb_soft_day_vwap_min_score_pct(params, "market_regime") == 0.70 assert _orb_soft_day_vwap_min_score_pct(params, "breadth") == 0.80 assert _orb_soft_day_vwap_min_score_pct(params, "market_regime+breadth") == 0.90 assert _orb_soft_day_vwap_min_score_pct(params, None) == 0.85 assert _orb_soft_day_vwap_size_scale(params, "market_regime") == 0.05 assert _orb_soft_day_vwap_size_scale(params, "market_regime+breadth") == 0.02 assert _orb_soft_day_vwap_size_scale(params, "breadth") == 0.03 def test_soft_day_vwap_reason_allowlist_limits_auxiliary_sleeve() -> None: params = ORBStrategyParams( soft_day_vwap_reclaim_allowed_reason_parts=["hard_breadth"] ) assert _orb_soft_day_vwap_reason_allowed(params, "market_regime+hard_breadth") assert not _orb_soft_day_vwap_reason_allowed(params, "market_regime+breadth") assert not _orb_soft_day_vwap_reason_allowed(params, "breadth") def test_soft_day_vwap_reason_quality_overrides_default_gates() -> None: params = ORBStrategyParams( soft_day_vwap_reclaim_min_premarket_dollar_vol=20_000_000, soft_day_vwap_reclaim_min_premarket_dollar_vol_hard_breadth=5_000_000, soft_day_vwap_reclaim_min_body_ratio=0.15, soft_day_vwap_reclaim_min_body_ratio_hard_breadth=0.0, soft_day_vwap_reclaim_min_close_location=0.65, soft_day_vwap_reclaim_min_close_location_hard_breadth=0.32, soft_day_vwap_reclaim_min_rvol=4.0, soft_day_vwap_reclaim_min_rvol_joint=5.0, soft_day_vwap_reclaim_max_rvol=12.0, soft_day_vwap_reclaim_max_rvol_breadth=35.0, ) assert ( _orb_soft_day_vwap_reason_param( params, "market_regime+hard_breadth", "soft_day_vwap_reclaim_min_premarket_dollar_vol", ) == 5_000_000 ) assert ( _orb_soft_day_vwap_reason_param( params, "market_regime", "soft_day_vwap_reclaim_min_premarket_dollar_vol", ) == 20_000_000 ) assert ( _orb_soft_day_vwap_reason_param( params, "market_regime+hard_breadth", "soft_day_vwap_reclaim_min_body_ratio", ) == 0.0 ) assert ( _orb_soft_day_vwap_reason_param( params, "market_regime+hard_breadth", "soft_day_vwap_reclaim_min_close_location", ) == 0.32 ) assert ( _orb_soft_day_vwap_reason_param( params, "breadth", "soft_day_vwap_reclaim_min_rvol", ) == 4.0 ) assert ( _orb_soft_day_vwap_reason_param( params, "market_regime+breadth", "soft_day_vwap_reclaim_min_rvol", ) == 5.0 ) assert ( _orb_soft_day_vwap_reason_param( params, "breadth", "soft_day_vwap_reclaim_max_rvol", ) == 35.0 ) def test_broad_gapup_continuation_admits_bounded_high_gap_candidate() -> None: bars = { "HIGH": [ {"timestamp": "2026-01-05T08:00:00-05:00", "open": 100.0, "high": 100.2, "low": 99.8, "close": 100.0, "volume": 300_000}, {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.9, "volume": 10_000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.9, "high": 101.5, "low": 100.7, "close": 101.3, "volume": 5_000}, ], } enrichment = _enrichment_for("HIGH") enrichment["HIGH"]["2026-01-05"]["prev_close"] = 90.0 enrichment["HIGH"]["2026-01-05"]["ret_5d"] = 0.08 enrichment["HIGH"]["2026-01-05"]["avg_daily_vol_14d"] = 1_000_000.0 base_params = ORBStrategyParams( engine_family="gainers_leader", min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=1000.0, min_abs_gap_pct=0.02, max_gap_pct=0.04, max_candidates=10, ) assert compute_orb_candidates(bars, "2026-01-05", base_params, enrichment) == [] params = base_params.model_copy( update={ "broad_gapup_continuation_enabled": True, "broad_gapup_continuation_min_gap_pct": 0.04, "broad_gapup_continuation_max_gap_pct": 0.20, "broad_gapup_continuation_min_rvol": None, "broad_gapup_continuation_min_premarket_dollar_vol": 10_000_000, "broad_gapup_continuation_min_first_bar_dollar_vol": 500_000, "broad_gapup_continuation_min_body_ratio": 0.50, "broad_gapup_continuation_min_close_location": 0.80, } ) candidates = compute_orb_candidates(bars, "2026-01-05", params, enrichment) assert len(candidates) == 1 assert candidates[0]["ticker"] == "HIGH" assert candidates[0]["broad_gapup_continuation"] is True assert candidates[0]["rvol"] < base_params.min_rvol liquid_only_params = params.model_copy( update={"broad_gapup_continuation_min_avg_dollar_vol": 2_000_000_000} ) assert compute_orb_candidates(bars, "2026-01-05", liquid_only_params, enrichment) == [] def test_broad_gapup_continuation_trades_with_dedicated_size_scale() -> None: bars = { "HIGH": [ {"timestamp": "2026-01-05T08:00:00-05:00", "open": 100.0, "high": 100.2, "low": 99.8, "close": 100.0, "volume": 300_000}, {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.9, "volume": 10_000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 5_000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.8, "high": 102.0, "low": 101.5, "close": 101.9, "volume": 5_000}, ], } enrichment = _enrichment_for("HIGH") enrichment["HIGH"]["2026-01-05"]["prev_close"] = 90.0 enrichment["HIGH"]["2026-01-05"]["ret_5d"] = 0.08 params = ORBStrategyParams( engine_family="gainers_leader", min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.02, max_gap_pct=0.04, max_candidates=10, min_candidates_to_trade=1, atr_stop_multiplier=10.0, trailing_at_r=99.0, breakeven_at_r=99.0, risk_per_trade_pct=0.01, max_position_pct=1.0, slippage_bps=0.0, broad_gapup_continuation_enabled=True, broad_gapup_continuation_min_gap_pct=0.04, broad_gapup_continuation_max_gap_pct=0.20, broad_gapup_continuation_min_rvol=1.0, broad_gapup_continuation_min_premarket_dollar_vol=10_000_000, broad_gapup_continuation_min_first_bar_dollar_vol=500_000, broad_gapup_continuation_min_body_ratio=0.50, broad_gapup_continuation_min_close_location=0.80, broad_gapup_continuation_size_scale=0.25, broad_gapup_continuation_max_trades=1, ) result = simulate_orb_day(bars, "2026-01-05", params, enrichment, equity=10_000.0) assert len(result.trades) == 1 trade = result.trades[0] assert trade.trigger_type == "broad_gapup_continuation" assert trade.broad_gapup_continuation is True assert trade.broad_gapup_continuation_size_scale == 0.25 assert trade.shares == 2 def test_broad_gapup_continuation_opening_burst_enters_next_bar_open() -> None: bars = { "HIGH": [ {"timestamp": "2026-01-05T08:00:00-05:00", "open": 100.0, "high": 100.2, "low": 99.8, "close": 100.0, "volume": 300_000}, {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.9, "volume": 10_000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.7, "high": 100.9, "low": 100.2, "close": 100.8, "volume": 5_000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 100.8, "high": 101.4, "low": 100.7, "close": 101.2, "volume": 5_000}, ], } enrichment = _enrichment_for("HIGH") enrichment["HIGH"]["2026-01-05"]["prev_close"] = 90.0 enrichment["HIGH"]["2026-01-05"]["ret_5d"] = 0.08 params = ORBStrategyParams( engine_family="gainers_leader", min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.02, max_gap_pct=0.04, max_candidates=10, min_candidates_to_trade=1, atr_stop_multiplier=10.0, trailing_at_r=99.0, breakeven_at_r=99.0, risk_per_trade_pct=0.01, max_position_pct=1.0, slippage_bps=0.0, broad_gapup_continuation_enabled=True, broad_gapup_continuation_entry_mode="opening_burst", broad_gapup_continuation_min_gap_pct=0.04, broad_gapup_continuation_max_gap_pct=0.20, broad_gapup_continuation_min_rvol=1.0, broad_gapup_continuation_min_premarket_dollar_vol=10_000_000, broad_gapup_continuation_min_first_bar_dollar_vol=500_000, broad_gapup_continuation_min_body_ratio=0.50, broad_gapup_continuation_min_close_location=0.80, broad_gapup_continuation_size_scale=0.25, broad_gapup_continuation_max_trades=1, ) result = simulate_orb_day(bars, "2026-01-05", params, enrichment, equity=10_000.0) assert len(result.trades) == 1 assert result.trades[0].trigger_type == "broad_gapup_continuation" assert result.trades[0].entry_price == 100.7 def test_entry_tie_break_rank_by_score_prioritizes_auxiliary_leader_on_same_bar() -> None: bars = { "BASE": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 10_000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 10_000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.8, "high": 102.0, "low": 101.5, "close": 101.9, "volume": 10_000}, ], "HIGH": [ {"timestamp": "2026-01-05T08:00:00-05:00", "open": 100.0, "high": 100.2, "low": 99.8, "close": 100.0, "volume": 300_000}, {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.9, "volume": 100_000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 50_000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.8, "high": 102.0, "low": 101.5, "close": 101.9, "volume": 50_000}, ], } enrichment = _enrichment_for("BASE", "HIGH") enrichment["BASE"]["2026-01-05"]["prev_close"] = 98.0 enrichment["BASE"]["2026-01-05"]["ret_5d"] = 0.03 enrichment["HIGH"]["2026-01-05"]["prev_close"] = 90.0 enrichment["HIGH"]["2026-01-05"]["ret_5d"] = 0.08 params = ORBStrategyParams( engine_family="gainers_leader", min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.02, max_gap_pct=0.04, max_candidates=10, min_candidates_to_trade=1, atr_stop_multiplier=1.0, trailing_at_r=99.0, breakeven_at_r=99.0, risk_per_trade_pct=1.0, max_position_pct=1.0, max_trades_per_day=2, slippage_bps=0.0, broad_gapup_continuation_enabled=True, broad_gapup_continuation_min_gap_pct=0.04, broad_gapup_continuation_max_gap_pct=0.20, broad_gapup_continuation_min_rvol=1.0, broad_gapup_continuation_min_premarket_dollar_vol=10_000_000, broad_gapup_continuation_min_first_bar_dollar_vol=500_000, broad_gapup_continuation_min_body_ratio=0.50, broad_gapup_continuation_min_close_location=0.80, broad_gapup_continuation_size_scale=1.0, broad_gapup_continuation_max_trades=1, ) legacy = simulate_orb_day( bars, "2026-01-05", params, enrichment, equity=10_000.0, available_cash=10_000.0, ) ranked = simulate_orb_day( bars, "2026-01-05", params.model_copy(update={"entry_tie_break_rank_by_score": True}), enrichment, equity=10_000.0, available_cash=10_000.0, ) assert legacy.trades[0].ticker == "BASE" assert ranked.trades[0].ticker == "HIGH" assert ranked.trades[0].trigger_type == "broad_gapup_continuation" def test_broad_gapup_continuation_bypasses_generic_soft_day_profile() -> None: bars = { "HIGH": [ {"timestamp": "2026-01-05T08:00:00-05:00", "open": 100.0, "high": 100.2, "low": 99.8, "close": 100.0, "volume": 300_000}, {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.9, "volume": 10_000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 5_000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.8, "high": 102.0, "low": 101.5, "close": 101.9, "volume": 5_000}, ], } enrichment = _enrichment_for("HIGH", "QQQ") enrichment["HIGH"]["2026-01-05"]["prev_close"] = 90.0 enrichment["HIGH"]["2026-01-05"]["ret_5d"] = 0.08 enrichment["QQQ"]["2026-01-05"]["prev_close"] = 100.0 enrichment["QQQ"]["2026-01-05"]["today_open"] = 100.0 params = ORBStrategyParams( engine_family="gainers_leader", min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.02, max_gap_pct=0.04, max_candidates=10, min_candidates_to_trade=1, atr_stop_multiplier=10.0, trailing_at_r=99.0, breakeven_at_r=99.0, risk_per_trade_pct=0.01, max_position_pct=1.0, slippage_bps=0.0, market_regime_spy_threshold=0.01, market_regime_ticker="QQQ", soft_day_fallback_on_regime_skip=True, soft_day_regime_skip_size_scale=0.1, soft_day_combined_size_scale_floor=1.0, soft_day_setup_profile="skip_day_reclaim_v1", soft_day_min_ret_5d=0.50, broad_gapup_continuation_enabled=True, broad_gapup_continuation_min_gap_pct=0.04, broad_gapup_continuation_max_gap_pct=0.20, broad_gapup_continuation_min_rvol=1.0, broad_gapup_continuation_min_premarket_dollar_vol=10_000_000, broad_gapup_continuation_min_first_bar_dollar_vol=500_000, broad_gapup_continuation_min_body_ratio=0.50, broad_gapup_continuation_min_close_location=0.80, broad_gapup_continuation_size_scale=0.25, ) result = simulate_orb_day(bars, "2026-01-05", params, enrichment, equity=10_000.0) assert result.is_soft_day is True assert result.soft_day_reason == "market_regime" assert len(result.trades) == 1 assert result.trades[0].trigger_type == "broad_gapup_continuation" def test_broad_gapup_continuation_scan_is_not_blocked_by_primary_sector_cap() -> None: bars = { "BASE": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 100.8, "low": 99.8, "close": 100.7, "volume": 20_000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.2, "low": 100.7, "close": 101.1, "volume": 10_000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.1, "high": 101.3, "low": 100.9, "close": 101.2, "volume": 10_000}, ], "HIGH": [ {"timestamp": "2026-01-05T08:00:00-05:00", "open": 100.0, "high": 100.2, "low": 99.8, "close": 100.0, "volume": 300_000}, {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.9, "volume": 10_000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 5_000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.8, "high": 102.0, "low": 101.5, "close": 101.9, "volume": 5_000}, ], } enrichment = _enrichment_for("BASE", "HIGH") enrichment["BASE"]["2026-01-05"]["prev_close"] = 98.0 enrichment["BASE"]["2026-01-05"]["ret_5d"] = 0.03 enrichment["HIGH"]["2026-01-05"]["prev_close"] = 90.0 enrichment["HIGH"]["2026-01-05"]["ret_5d"] = 0.08 params = ORBStrategyParams( engine_family="gainers_leader", min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.02, max_gap_pct=0.04, max_candidates=1, max_candidates_per_sector=1, min_candidates_to_trade=1, atr_stop_multiplier=10.0, trailing_at_r=99.0, breakeven_at_r=99.0, risk_per_trade_pct=0.01, max_position_pct=1.0, slippage_bps=0.0, broad_gapup_continuation_enabled=True, broad_gapup_continuation_min_gap_pct=0.04, broad_gapup_continuation_max_gap_pct=0.20, broad_gapup_continuation_min_rvol=1.0, broad_gapup_continuation_min_premarket_dollar_vol=10_000_000, broad_gapup_continuation_min_first_bar_dollar_vol=500_000, broad_gapup_continuation_min_body_ratio=0.50, broad_gapup_continuation_min_close_location=0.80, broad_gapup_continuation_size_scale=0.25, broad_gapup_continuation_max_candidates=1, broad_gapup_continuation_max_trades=1, ) result = simulate_orb_day( bars, "2026-01-05", params, enrichment, equity=10_000.0, ticker_sectors={"BASE": "TECH", "HIGH": "TECH"}, ) assert "broad_gapup_continuation" in {trade.trigger_type for trade in result.trades} def test_intraday_continuation_can_use_dedicated_low_atr_pct_gate() -> None: bars = { "LIQ": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 100.2, "low": 99.8, "close": 100.1, "volume": 10_000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.1, "high": 100.5, "low": 100.0, "close": 100.4, "volume": 10_000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 100.4, "high": 100.9, "low": 100.3, "close": 100.8, "volume": 10_000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 100.8, "high": 101.2, "low": 100.7, "close": 101.1, "volume": 10_000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 101.1, "high": 101.6, "low": 101.0, "close": 101.5, "volume": 10_000}, {"timestamp": "2026-01-05T09:55:00-05:00", "open": 101.5, "high": 102.1, "low": 101.4, "close": 102.0, "volume": 10_000}, {"timestamp": "2026-01-05T10:00:00-05:00", "open": 102.1, "high": 103.0, "low": 102.0, "close": 102.8, "volume": 10_000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.8, "high": 103.2, "low": 102.6, "close": 103.0, "volume": 10_000}, ], } enrichment = _enrichment_for("LIQ") enrichment["LIQ"]["2026-01-05"]["atr_14"] = 2.0 enrichment["LIQ"]["2026-01-05"]["prev_close"] = 99.0 base_params = ORBStrategyParams( engine_family="gainers_leader", min_price=10.0, min_avg_dollar_volume=100_000_000.0, min_atr_14=0.1, min_atr_pct=0.04, min_rvol=1000.0, min_abs_gap_pct=0.02, max_gap_pct=0.04, max_candidates=10, ) assert compute_orb_candidates(bars, "2026-01-05", base_params, enrichment) == [] params = base_params.model_copy( update={ "intraday_continuation_reclaim_enabled": True, "intraday_continuation_reclaim_signal_minutes": 30, "intraday_continuation_reclaim_min_signal_return_pct": 0.015, "intraday_continuation_reclaim_min_signal_dollar_vol": 1_000_000, "intraday_continuation_reclaim_min_signal_close_location": 0.80, "intraday_continuation_reclaim_require_signal_above_vwap": False, "intraday_continuation_reclaim_min_gap_pct": -0.02, "intraday_continuation_reclaim_max_gap_pct": 0.02, "intraday_continuation_reclaim_min_avg_dollar_vol": 100_000_000, "intraday_continuation_reclaim_min_atr_pct": 0.01, "intraday_continuation_reclaim_max_atr_pct": 0.03, } ) candidates = compute_orb_candidates(bars, "2026-01-05", params, enrichment) assert len(candidates) == 1 assert candidates[0]["ticker"] == "LIQ" assert candidates[0]["intraday_continuation_reclaim"] is True assert candidates[0]["intraday_continuation_entry_price"] == 102.1 too_low_volatility_ceiling = params.model_copy( update={"intraday_continuation_reclaim_max_atr_pct": 0.015} ) assert compute_orb_candidates( bars, "2026-01-05", too_low_volatility_ceiling, enrichment, ) == [] def test_market_thrust_liquid_opening_burst_enters_without_breakout() -> None: bars = { "BURST": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.8, "close": 100.9, "volume": 10_000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.2, "high": 101.2, "low": 100.8, "close": 101.0, "volume": 5_000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.0, "high": 102.2, "low": 100.9, "close": 102.0, "volume": 5_000}, ], } enrichment = _enrichment_for("BURST") enrichment["BURST"]["2026-01-05"]["prev_close"] = 98.0 enrichment["BURST"]["2026-01-05"]["ret_5d"] = 0.03 params = ORBStrategyParams( engine_family="gainers_leader", min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=1000.0, min_abs_gap_pct=0.01, max_gap_pct=0.04, max_candidates=10, min_candidates_to_trade=1, min_candidate_breadth=1.1, soft_day_fallback_on_breadth_skip=True, soft_day_breadth_skip_size_scale=0.1, soft_day_combined_size_scale_floor=1.0, atr_stop_multiplier=10.0, trailing_at_r=99.0, breakeven_at_r=99.0, risk_per_trade_pct=0.01, max_position_pct=1.0, slippage_bps=0.0, market_thrust_breadth_override_enabled=True, market_thrust_liquid_continuation_enabled=True, market_thrust_liquid_continuation_entry_mode="opening_burst", market_thrust_liquid_continuation_min_gap_pct=0.02, market_thrust_liquid_continuation_max_gap_pct=0.04, market_thrust_liquid_continuation_min_first_bar_return_pct=0.007, market_thrust_liquid_continuation_min_first_bar_dollar_vol=100_000, market_thrust_liquid_continuation_min_avg_dollar_vol=100_000, market_thrust_liquid_continuation_min_body_ratio=0.5, market_thrust_liquid_continuation_min_close_location=0.8, market_thrust_liquid_continuation_max_candidates=1, market_thrust_liquid_continuation_max_trades=1, market_thrust_liquid_continuation_size_scale=0.2, ) result = simulate_orb_day(bars, "2026-01-05", params, enrichment, equity=10_000.0) assert result.entry_diagnostics["market_thrust_opening_burst_candidates"] == 1 assert len(result.trades) == 1 trade = result.trades[0] assert trade.trigger_type == "market_thrust_opening_burst" assert trade.entry_time == "2026-01-05T09:35:00-05:00" assert trade.entry_price == 101.2 assert trade.market_thrust_opening_burst is True def test_iex_live_volume_multiplier_rescales_market_thrust_first_bar_gate() -> None: bars = { "IEX": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.8, "close": 100.9, "volume": 500}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.2, "high": 101.5, "low": 100.8, "close": 101.3, "volume": 500}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.3, "high": 102.0, "low": 101.0, "close": 101.8, "volume": 500}, ], } enrichment = _enrichment_for("IEX") enrichment["IEX"]["2026-01-05"]["prev_close"] = 98.0 enrichment["IEX"]["2026-01-05"]["avg_dollar_vol_30d"] = 300_000_000.0 params = ORBStrategyParams( engine_family="gainers_leader", min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=None, min_abs_gap_pct=0.01, max_gap_pct=0.04, max_candidates=10, market_thrust_liquid_continuation_enabled=True, market_thrust_liquid_continuation_min_gap_pct=0.02, market_thrust_liquid_continuation_max_gap_pct=0.04, market_thrust_liquid_continuation_min_first_bar_return_pct=0.007, market_thrust_liquid_continuation_min_first_bar_dollar_vol=1_000_000, market_thrust_liquid_continuation_min_avg_dollar_vol=100_000_000, market_thrust_liquid_continuation_min_body_ratio=0.5, market_thrust_liquid_continuation_min_close_location=0.8, ) unscaled = compute_orb_candidates( bars, "2026-01-05", params, enrichment, iex_live_mode=True, ) scaled = compute_orb_candidates( bars, "2026-01-05", params.model_copy(update={"iex_live_intraday_volume_multiplier": 30.0}), enrichment, iex_live_mode=True, ) assert len(unscaled) == 1 assert unscaled[0]["market_thrust_liquid_continuation"] is False assert len(scaled) == 1 assert scaled[0]["market_thrust_liquid_continuation"] is True assert scaled[0]["first_bar_dollar_vol"] == 50_000.0 assert scaled[0]["filter_first_bar_dollar_vol"] == 1_500_000.0 def test_no_thrust_liquid_repair_can_rank_by_opening_impulse() -> None: bars = { "SCORE": [ {"timestamp": "2026-01-05T08:00:00-05:00", "open": 100.0, "high": 100.0, "low": 100.0, "close": 100.0, "volume": 1_000_000}, {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.2, "low": 99.9, "close": 101.0, "volume": 20_000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.1, "high": 101.5, "low": 100.8, "close": 101.2, "volume": 10_000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.2, "high": 101.4, "low": 101.0, "close": 101.3, "volume": 10_000}, ], "IMPULSE": [ {"timestamp": "2026-01-05T08:00:00-05:00", "open": 100.0, "high": 100.0, "low": 100.0, "close": 100.0, "volume": 10_000}, {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 103.8, "low": 99.9, "close": 103.5, "volume": 2_000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 103.6, "high": 104.2, "low": 103.3, "close": 104.0, "volume": 2_000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 104.0, "high": 104.5, "low": 103.8, "close": 104.4, "volume": 2_000}, ], } enrichment = _enrichment_for("SCORE", "IMPULSE") for ticker in enrichment: enrichment[ticker]["2026-01-05"]["prev_close"] = 99.0 enrichment[ticker]["2026-01-05"]["ret_5d"] = 0.02 params = ORBStrategyParams( engine_family="gainers_leader", min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=1000.0, min_abs_gap_pct=0.02, max_gap_pct=0.04, max_candidates=10, min_candidates_to_trade=1, min_candidate_breadth=0.0, weight_rvol=0.0, weight_gap=0.0, weight_dollar_vol=0.0, weight_premarket_dollar_vol=1.0, weight_momentum=0.0, weight_obv_slope=0.0, weight_event_catalyst=0.0, weight_close_location=0.0, atr_stop_multiplier=10.0, trailing_at_r=99.0, breakeven_at_r=99.0, risk_per_trade_pct=0.01, max_position_pct=1.0, slippage_bps=0.0, market_thrust_liquid_continuation_enabled=True, market_thrust_liquid_continuation_require_market_thrust=False, market_thrust_liquid_continuation_entry_mode="opening_burst", market_thrust_liquid_continuation_no_thrust_min_gap_pct=0.0, market_thrust_liquid_continuation_no_thrust_max_gap_pct=0.04, market_thrust_liquid_continuation_no_thrust_min_first_bar_return_pct=0.005, market_thrust_liquid_continuation_no_thrust_min_first_bar_dollar_vol=100_000, market_thrust_liquid_continuation_no_thrust_min_avg_dollar_vol=100_000, market_thrust_liquid_continuation_no_thrust_max_candidates=1, market_thrust_liquid_continuation_no_thrust_max_trades=1, market_thrust_liquid_continuation_no_thrust_rank_mode="opening_impulse", market_thrust_liquid_continuation_no_thrust_size_scale=0.2, ) result = simulate_orb_day(bars, "2026-01-05", params, enrichment, equity=10_000.0) assert result.entry_diagnostics["market_thrust_opening_burst_candidates"] == 1 assert len(result.trades) == 1 assert result.trades[0].ticker == "IMPULSE" assert result.trades[0].trigger_type == "market_thrust_opening_burst" priority_params = params.model_copy( update={ "market_thrust_liquid_continuation_no_thrust_rank_mode": None, "min_candidate_breadth": 1.1, "soft_day_fallback_on_breadth_skip": True, "soft_day_combined_size_scale_floor": 1.0, "soft_day_rank_before_time": True, ( "market_thrust_liquid_continuation_no_thrust_" "priority_min_first_bar_return_pct" ): 0.03, } ) priority_result = simulate_orb_day( bars, "2026-01-05", priority_params, enrichment, equity=10_000.0, ) assert len(priority_result.trades) == 1 assert priority_result.trades[0].ticker == "IMPULSE" def test_nofill_vwap_reclaim_adds_trade_when_orb_never_breaks() -> None: bars = { "VWAP": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 98.0, "close": 99.0, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 99.0, "high": 99.2, "low": 98.5, "close": 98.8, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 98.8, "high": 99.5, "low": 98.7, "close": 99.4, "volume": 1000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 99.4, "high": 99.9, "low": 99.2, "close": 99.8, "volume": 1000}, {"timestamp": "2026-01-05T10:00:00-05:00", "open": 99.8, "high": 100.5, "low": 99.7, "close": 100.4, "volume": 2000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 100.4, "high": 100.9, "low": 100.2, "close": 100.8, "volume": 1000}, ] } enrichment = _enrichment_for("VWAP") enrichment["VWAP"]["2026-01-05"]["prev_close"] = 98.0 params = ORBStrategyParams( engine_family="gainers_leader", allow_red_to_green_breakout=True, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.0, max_candidates=10, min_candidates_to_trade=1, atr_stop_multiplier=10.0, trailing_at_r=99.0, breakeven_at_r=99.0, risk_per_trade_pct=0.01, max_position_pct=1.0, slippage_bps=0.0, nofill_vwap_reclaim_enabled=True, nofill_vwap_reclaim_min_score_pct=0.0, nofill_vwap_reclaim_size_scale=1.0, vwap_reclaim_window_start_min=30, vwap_reclaim_window_end_min=120, vwap_reclaim_require_prior_dip=True, ) day_result = simulate_orb_day( bars, "2026-01-05", params, enrichment, equity=10_000.0, ) assert len(day_result.trades) == 1 assert day_result.trades[0].trigger_type == "vwap_reclaim" assert day_result.trades[0].entry_time == "2026-01-05T10:00:00-05:00" def test_reclaim_entry_attention_can_gate_weak_nofill_vwap_reclaim() -> None: bars = { "VWAP": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 98.0, "close": 99.0, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 99.0, "high": 99.2, "low": 98.5, "close": 98.8, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 98.8, "high": 99.5, "low": 98.7, "close": 99.4, "volume": 1000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 99.4, "high": 99.9, "low": 99.2, "close": 99.8, "volume": 1000}, {"timestamp": "2026-01-05T10:00:00-05:00", "open": 99.8, "high": 100.5, "low": 99.7, "close": 100.4, "volume": 1200}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 100.4, "high": 100.9, "low": 100.2, "close": 100.8, "volume": 1000}, ] } enrichment = _enrichment_for("VWAP") enrichment["VWAP"]["2026-01-05"]["prev_close"] = 98.0 base_params = ORBStrategyParams( engine_family="gainers_leader", allow_red_to_green_breakout=True, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.0, max_candidates=10, min_candidates_to_trade=1, atr_stop_multiplier=10.0, trailing_at_r=99.0, breakeven_at_r=99.0, risk_per_trade_pct=0.01, max_position_pct=1.0, slippage_bps=0.0, nofill_vwap_reclaim_enabled=True, nofill_vwap_reclaim_min_score_pct=0.0, nofill_vwap_reclaim_size_scale=1.0, vwap_reclaim_window_start_min=30, vwap_reclaim_window_end_min=120, vwap_reclaim_require_prior_dip=True, reclaim_entry_attention_enabled=True, reclaim_entry_attention_trigger_types=["vwap_reclaim"], ) pass_result = simulate_orb_day( bars, "2026-01-05", base_params.model_copy( update={"reclaim_entry_attention_min_entry_rel_volume": 1.0} ), enrichment, equity=10_000.0, ) assert len(pass_result.trades) == 1 assert pass_result.trades[0].entry_rel_volume is not None assert pass_result.trades[0].entry_rel_volume > 1.0 reject_result = simulate_orb_day( bars, "2026-01-05", base_params.model_copy( update={"reclaim_entry_attention_min_entry_rel_volume": 2.0} ), enrichment, equity=10_000.0, ) assert reject_result.trades == [] assert ( reject_result.entry_diagnostics["entry_reject_stats"][ "reclaim_entry_attention_relvol" ] == 1 ) def test_late_breakout_adds_trade_after_primary_timeout() -> None: bars = { "LATE": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.6, "close": 100.6, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.6, "high": 100.8, "low": 100.0, "close": 100.2, "volume": 800}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 100.2, "high": 100.7, "low": 99.9, "close": 100.5, "volume": 800}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 100.5, "high": 100.9, "low": 100.2, "close": 100.8, "volume": 800}, {"timestamp": "2026-01-05T10:00:00-05:00", "open": 100.8, "high": 101.6, "low": 95.0, "close": 101.4, "volume": 3000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.4, "high": 102.0, "low": 101.1, "close": 101.8, "volume": 1000}, ] } enrichment = _enrichment_for("LATE") enrichment["LATE"]["2026-01-05"]["prev_close"] = 98.0 params = ORBStrategyParams( engine_family="gainers_leader", allow_red_to_green_breakout=True, order_timeout_minutes=25, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.0, max_candidates=10, min_candidates_to_trade=1, atr_stop_multiplier=0.5, trailing_at_r=99.0, breakeven_at_r=99.0, risk_per_trade_pct=0.01, max_position_pct=1.0, slippage_bps=0.0, late_breakout_enabled=True, late_breakout_min_score_pct=0.0, late_breakout_size_scale=1.0, late_breakout_window_start_min=30, late_breakout_window_end_min=120, late_breakout_confirm_rel_vol=1.2, ) day_result = simulate_orb_day( bars, "2026-01-05", params, enrichment, equity=10_000.0, ) assert day_result.entry_diagnostics["primary_timed_candidates"] == 0 assert day_result.entry_diagnostics["late_breakout_candidates"] == 1 assert len(day_result.trades) == 1 assert day_result.trades[0].trigger_type == "late_breakout" assert day_result.trades[0].entry_time == "2026-01-05T10:00:00-05:00" assert day_result.trades[0].exit_reason == "close" def test_soft_day_vwap_reclaim_bypasses_market_regime_profile_block() -> None: bars = { "VWAP": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 98.0, "close": 99.0, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 99.0, "high": 99.2, "low": 98.5, "close": 98.8, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 98.8, "high": 99.5, "low": 98.7, "close": 99.4, "volume": 1000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 99.4, "high": 99.9, "low": 99.2, "close": 99.8, "volume": 1000}, {"timestamp": "2026-01-05T10:00:00-05:00", "open": 99.8, "high": 100.5, "low": 99.7, "close": 100.4, "volume": 2000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 100.4, "high": 100.9, "low": 100.2, "close": 100.8, "volume": 1000}, ] } enrichment = _enrichment_for("VWAP", "QQQ") enrichment["VWAP"]["2026-01-05"]["prev_close"] = 98.0 enrichment["QQQ"]["2026-01-05"]["prev_close"] = 100.0 enrichment["QQQ"]["2026-01-05"]["today_open"] = 99.0 params = ORBStrategyParams( engine_family="gainers_leader", allow_red_to_green_breakout=True, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.0, max_candidates=10, min_candidates_to_trade=1, market_regime_spy_threshold=0.001, market_regime_ticker="QQQ", soft_day_fallback_on_regime_skip=True, soft_day_regime_skip_size_scale=0.1, soft_day_setup_profile="skip_day_reclaim_v1", atr_stop_multiplier=10.0, trailing_at_r=99.0, breakeven_at_r=99.0, risk_per_trade_pct=0.01, max_position_pct=1.0, slippage_bps=0.0, soft_day_vwap_reclaim_enabled=True, soft_day_vwap_reclaim_min_score_pct=0.0, soft_day_vwap_reclaim_min_premarket_dollar_vol=0.0, soft_day_vwap_reclaim_size_scale=1.0, vwap_reclaim_window_start_min=30, vwap_reclaim_window_end_min=120, vwap_reclaim_require_prior_dip=True, ) day_result = simulate_orb_day( bars, "2026-01-05", params, enrichment, equity=10_000.0, ) assert day_result.is_soft_day assert day_result.soft_day_reason == "market_regime" assert len(day_result.trades) == 1 assert day_result.trades[0].trigger_type == "soft_day_vwap_reclaim" def test_soft_day_vwap_weak_participation_guard_rejects_low_rank_body() -> None: bars = { "ANCHOR": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 104.0, "low": 99.5, "close": 103.5, "volume": 20_000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 103.5, "high": 104.0, "low": 103.0, "close": 103.7, "volume": 5_000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 103.7, "high": 104.2, "low": 103.5, "close": 103.9, "volume": 5_000}, {"timestamp": "2026-01-05T10:00:00-05:00", "open": 103.9, "high": 104.5, "low": 103.8, "close": 104.1, "volume": 5_000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 104.1, "high": 104.5, "low": 103.9, "close": 104.2, "volume": 5_000}, ], "VWAP": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 98.0, "close": 99.0, "volume": 1_000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 99.0, "high": 99.2, "low": 98.5, "close": 98.8, "volume": 1_000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 98.8, "high": 99.5, "low": 98.7, "close": 99.4, "volume": 1_000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 99.4, "high": 99.9, "low": 99.2, "close": 99.8, "volume": 1_000}, {"timestamp": "2026-01-05T10:00:00-05:00", "open": 99.8, "high": 100.5, "low": 99.7, "close": 100.4, "volume": 2_000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 100.4, "high": 100.9, "low": 100.2, "close": 100.8, "volume": 1_000}, ], } enrichment = _enrichment_for("ANCHOR", "VWAP", "QQQ") enrichment["ANCHOR"]["2026-01-05"]["prev_close"] = 98.0 enrichment["VWAP"]["2026-01-05"]["prev_close"] = 98.0 enrichment["QQQ"]["2026-01-05"]["prev_close"] = 100.0 enrichment["QQQ"]["2026-01-05"]["today_open"] = 99.0 params = ORBStrategyParams( engine_family="gainers_leader", allow_red_to_green_breakout=True, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.0, max_candidates=10, min_candidates_to_trade=1, market_regime_spy_threshold=0.001, market_regime_ticker="QQQ", soft_day_fallback_on_regime_skip=True, soft_day_regime_skip_size_scale=0.1, soft_day_setup_profile="skip_day_reclaim_v1", atr_stop_multiplier=10.0, trailing_at_r=99.0, breakeven_at_r=99.0, risk_per_trade_pct=0.01, max_position_pct=1.0, slippage_bps=0.0, soft_day_vwap_reclaim_enabled=True, soft_day_vwap_reclaim_min_score_pct=0.0, soft_day_vwap_reclaim_min_premarket_dollar_vol=0.0, soft_day_vwap_reclaim_size_scale=1.0, vwap_reclaim_window_start_min=30, vwap_reclaim_window_end_min=120, vwap_reclaim_require_prior_dip=True, ) unguarded = simulate_orb_day(bars, "2026-01-05", params, enrichment, equity=10_000.0) guarded = simulate_orb_day( bars, "2026-01-05", params.model_copy( update={ "soft_day_vwap_reclaim_weak_participation_max_rvol_rank_pct": 0.65, "soft_day_vwap_reclaim_weak_participation_max_body_ratio": 0.75, } ), enrichment, equity=10_000.0, ) assert len(unguarded.trades) == 1 assert unguarded.trades[0].ticker == "VWAP" assert unguarded.trades[0].trigger_type == "soft_day_vwap_reclaim" assert unguarded.trades[0].rvol_rank_pct == 0.0 assert len(guarded.trades) == 0 assert guarded.entry_diagnostics["entry_reject_stats"][ "soft_day_vwap_weak_participation" ] == 1 def test_soft_day_vwap_reclaim_can_be_limited_to_no_primary_trades() -> None: bars = { "ORB": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.8, "high": 102.0, "low": 101.6, "close": 101.9, "volume": 1000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 101.9, "high": 102.1, "low": 101.7, "close": 102.0, "volume": 1000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.8, "high": 102.0, "low": 101.6, "close": 101.9, "volume": 1000}, ], "VWAP": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 98.0, "close": 99.0, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 99.0, "high": 99.2, "low": 98.5, "close": 98.8, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 98.8, "high": 99.5, "low": 98.7, "close": 99.4, "volume": 1000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 99.4, "high": 99.9, "low": 99.2, "close": 99.8, "volume": 1000}, {"timestamp": "2026-01-05T10:00:00-05:00", "open": 99.8, "high": 100.5, "low": 99.7, "close": 100.4, "volume": 2000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 100.4, "high": 100.9, "low": 100.2, "close": 100.8, "volume": 1000}, ], } enrichment = _enrichment_for("ORB", "VWAP", "QQQ") enrichment["ORB"]["2026-01-05"]["prev_close"] = 98.0 enrichment["VWAP"]["2026-01-05"]["prev_close"] = 98.0 enrichment["QQQ"]["2026-01-05"]["prev_close"] = 100.0 enrichment["QQQ"]["2026-01-05"]["today_open"] = 99.0 params = ORBStrategyParams( engine_family="gainers_leader", allow_red_to_green_breakout=True, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.0, max_candidates=10, min_candidates_to_trade=1, max_trades_per_day=10, market_regime_spy_threshold=0.001, market_regime_ticker="QQQ", soft_day_fallback_on_regime_skip=True, soft_day_regime_skip_size_scale=0.1, soft_day_setup_profile="none", atr_stop_multiplier=10.0, trailing_at_r=99.0, breakeven_at_r=99.0, risk_per_trade_pct=0.01, max_position_pct=1.0, slippage_bps=0.0, soft_day_vwap_reclaim_enabled=True, soft_day_vwap_reclaim_only_when_no_primary_trades=True, soft_day_vwap_reclaim_min_score_pct=0.0, soft_day_vwap_reclaim_min_premarket_dollar_vol=0.0, soft_day_vwap_reclaim_size_scale=1.0, vwap_reclaim_window_start_min=30, vwap_reclaim_window_end_min=120, vwap_reclaim_require_prior_dip=True, ) day_result = simulate_orb_day( bars, "2026-01-05", params, enrichment, equity=10_000.0, ) assert day_result.is_soft_day assert day_result.entry_diagnostics["primary_timed_candidates"] == 1 assert day_result.entry_diagnostics["soft_day_vwap_candidates"] == 1 assert day_result.entry_diagnostics["timed_candidates"] == 2 assert day_result.entry_diagnostics["soft_day_vwap_trades"] == 0 assert day_result.entry_diagnostics["entry_reject_stats"] == { "soft_day_vwap_primary_trade_exists": 1 } assert len(day_result.trades) == 1 assert day_result.trades[0].trigger_type == "orb" no_existing_params = params.model_copy( update={ "soft_day_vwap_reclaim_only_when_no_primary_trades": False, "soft_day_vwap_reclaim_only_when_no_existing_trades": True, } ) no_existing_result = simulate_orb_day( bars, "2026-01-05", no_existing_params, enrichment, equity=10_000.0, ) assert no_existing_result.entry_diagnostics["soft_day_vwap_trades"] == 0 assert no_existing_result.entry_diagnostics["entry_reject_stats"] == { "soft_day_vwap_existing_trade": 1 } def test_hard_breadth_soft_fallback_converts_breadth_skip_to_soft_day() -> None: bars = { ticker: [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 1000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.8, "high": 102.2, "low": 101.6, "close": 102.0, "volume": 1000}, ] for ticker in ("AAA", "BBB") } enrichment = _enrichment_for("AAA", "BBB") enrichment["AAA"]["2026-01-05"]["prev_close"] = 98.0 enrichment["AAA"]["2026-01-05"]["today_open"] = 100.0 enrichment["BBB"]["2026-01-05"]["prev_close"] = 102.0 enrichment["BBB"]["2026-01-05"]["today_open"] = 100.0 params = ORBStrategyParams( engine_family="gainers_leader", allow_red_to_green_breakout=True, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.0, max_candidates=10, min_candidates_to_trade=1, min_candidate_breadth=0.6, breadth_skip_below=0.6, hard_breadth_soft_fallback_enabled=True, hard_breadth_soft_fallback_min_breadth=0.4, hard_breadth_soft_fallback_size_scale=0.05, soft_day_setup_profile="none", atr_stop_multiplier=10.0, trailing_at_r=99.0, breakeven_at_r=99.0, risk_per_trade_pct=0.01, max_position_pct=1.0, slippage_bps=0.0, ) day_result = simulate_orb_day( bars, "2026-01-05", params, enrichment, equity=10_000.0, ) assert day_result.skip_reason != "breadth" assert day_result.is_soft_day assert day_result.soft_day_reason == "hard_breadth" assert day_result.breadth_scaler == 0.05 def test_synthetic_today_daily_breadth_uses_intraday_open() -> None: bars = { ticker: [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 101.0, "high": 102.0, "low": 100.5, "close": 101.8, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.8, "high": 103.0, "low": 101.8, "close": 102.8, "volume": 1000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.8, "high": 103.2, "low": 102.6, "close": 103.0, "volume": 1000}, ] for ticker in ("AAA", "BBB", "CCC") } enrichment = _enrichment_for("AAA", "BBB", "CCC") for ticker in enrichment: enrichment[ticker]["2026-01-05"]["prev_close"] = 100.0 enrichment[ticker]["2026-01-05"]["today_open"] = 100.0 enrichment[ticker]["2026-01-05"]["synthetic_today_daily"] = True params = ORBStrategyParams( engine_family="gainers_leader", min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.0, max_candidates=10, min_candidates_to_trade=1, min_candidate_breadth=0.6, breadth_skip_below=0.25, hard_breadth_soft_fallback_enabled=True, hard_breadth_soft_fallback_min_breadth=0.0, hard_breadth_soft_fallback_size_scale=0.05, atr_stop_multiplier=10.0, trailing_at_r=99.0, breakeven_at_r=99.0, risk_per_trade_pct=0.01, max_position_pct=1.0, slippage_bps=0.0, ) day_result = simulate_orb_day( bars, "2026-01-05", params, enrichment, equity=10_000.0, ) assert day_result.skip_reason is None assert day_result.soft_day_reason is None assert day_result.breadth_scaler == 1.0 assert day_result.breadth_ratio == 1.0 assert day_result.breadth_positive_count == 3 assert day_result.breadth_total_count == 3 def test_market_thrust_override_clears_breadth_only_soft_day() -> None: bars = { "SPY": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.9, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.9, "high": 102.5, "low": 101.7, "close": 102.2, "volume": 1000}, ], "QQQ": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 101.0, "high": 103.0, "low": 100.8, "close": 102.8, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 102.8, "high": 103.4, "low": 102.7, "close": 103.2, "volume": 1000}, ], "AAA": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 99.0, "high": 99.2, "low": 98.0, "close": 98.2, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 98.2, "high": 98.4, "low": 97.8, "close": 98.0, "volume": 1000}, ], "BBB": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 99.0, "high": 99.1, "low": 98.1, "close": 98.3, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 98.3, "high": 98.5, "low": 98.0, "close": 98.1, "volume": 1000}, ], "CCC": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 99.0, "high": 99.1, "low": 98.0, "close": 98.4, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 98.4, "high": 98.6, "low": 98.0, "close": 98.2, "volume": 1000}, ], } enrichment = _enrichment_for("SPY", "QQQ", "AAA", "BBB", "CCC") for ticker in ("SPY", "QQQ"): enrichment[ticker]["2026-01-05"]["prev_close"] = 100.0 enrichment[ticker]["2026-01-05"]["today_open"] = 101.0 for ticker in ("AAA", "BBB", "CCC"): enrichment[ticker]["2026-01-05"]["prev_close"] = 100.0 enrichment[ticker]["2026-01-05"]["today_open"] = 99.0 params = ORBStrategyParams( engine_family="gainers_leader", market_regime_ticker="QQQ", market_regime_spy_threshold=0.001, market_orb_quality_ticker="SPY", market_orb_quality_secondary_ticker="QQQ", min_candidate_breadth=0.6, breadth_skip_below=0.25, soft_day_fallback_on_breadth_skip=True, soft_day_breadth_skip_size_scale=0.1, market_thrust_breadth_override_enabled=True, market_thrust_breadth_override_min_primary_close_location=0.85, market_thrust_breadth_override_min_secondary_close_location=0.85, market_thrust_breadth_override_min_primary_return_pct=0.001, market_thrust_breadth_override_min_secondary_return_pct=0.001, market_thrust_breadth_override_min_regime_gap_pct=0.001, market_thrust_breadth_override_min_breadth_ratio=0.35, market_thrust_breadth_override_size_scale_floor=1.0, market_thrust_breadth_override_clear_soft_day=True, ) day_result = simulate_orb_day( bars, "2026-01-05", params, enrichment, equity=10_000.0, ) assert day_result.breadth_ratio == pytest.approx(0.4) assert day_result.market_thrust_breadth_override_active assert day_result.breadth_scaler == 1.0 assert day_result.is_soft_day is False assert day_result.soft_day_reason is None def test_max_trades_per_day_caps_later_orb_fills() -> None: bars = { ticker: [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.8, "high": 102.4, "low": 101.5, "close": 102.2, "volume": 1000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.3, "volume": 1000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.3, "high": 102.6, "low": 102.1, "close": 102.4, "volume": 1000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.4, "volume": 1000}, ] for ticker in ("AAA", "BBB", "CCC") } enrichment = _enrichment_for("AAA", "BBB", "CCC") params = ORBStrategyParams( engine_family="gainers_leader", allow_red_to_green_breakout=True, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.0, max_candidates=10, min_candidates_to_trade=1, atr_stop_multiplier=10.0, trailing_at_r=99.0, breakeven_at_r=99.0, risk_per_trade_pct=0.01, max_position_pct=1.0, slippage_bps=0.0, max_trades_per_day=2, ) day_result = simulate_orb_day( bars, "2026-01-05", params, enrichment, equity=10_000.0, ) assert len(day_result.trades) == 2 def test_late_trade_size_scale_reduces_later_fills_without_skipping() -> None: bars = { ticker: [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.8, "high": 102.4, "low": 101.5, "close": 102.2, "volume": 1000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.3, "volume": 1000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.3, "high": 102.6, "low": 102.1, "close": 102.4, "volume": 1000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.4, "volume": 1000}, ] for ticker in ("AAA", "BBB", "CCC") } enrichment = _enrichment_for("AAA", "BBB", "CCC") params = ORBStrategyParams( engine_family="gainers_leader", allow_red_to_green_breakout=True, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.0, max_candidates=10, min_candidates_to_trade=1, atr_stop_multiplier=10.0, trailing_at_r=99.0, breakeven_at_r=99.0, risk_per_trade_pct=0.01, max_position_pct=1.0, slippage_bps=0.0, late_trade_size_scale_after_n=2, late_trade_size_scale=0.5, ) day_result = simulate_orb_day( bars, "2026-01-05", params, enrichment, equity=10_000.0, ) assert len(day_result.trades) == 3 assert day_result.trades[2].shares < day_result.trades[1].shares assert day_result.trades[2].late_trade_size_scale == 0.5 def test_rank_rvol_pressure_size_scale_reduces_mid_rank_high_rvol_fills() -> None: bars = { ticker: [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.8, "high": 102.4, "low": 101.5, "close": 102.2, "volume": 1000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.3, "volume": 1000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.3, "high": 102.6, "low": 102.1, "close": 102.4, "volume": 1000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.4, "volume": 1000}, ] for ticker in ("AAA", "BBB", "CCC") } enrichment = _enrichment_for("AAA", "BBB", "CCC") for ticker in enrichment: enrichment[ticker]["2026-01-05"]["ret_5d"] = -0.01 params = ORBStrategyParams( engine_family="gainers_leader", allow_red_to_green_breakout=True, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.0, max_candidates=10, min_candidates_to_trade=1, atr_stop_multiplier=10.0, trailing_at_r=99.0, breakeven_at_r=99.0, risk_per_trade_pct=0.01, max_position_pct=1.0, slippage_bps=0.0, rank_rvol_pressure_min_rvol=0.1, rank_rvol_pressure_max_score_rank_pct=0.5, rank_rvol_pressure_max_ret_5d=0.0, rank_rvol_pressure_size_scale=0.4, ) day_result = simulate_orb_day( bars, "2026-01-05", params, enrichment, equity=10_000.0, ) assert len(day_result.trades) == 3 assert day_result.trades[0].rank_rvol_pressure_size_scale is None assert day_result.trades[1].rank_rvol_pressure_size_scale == 0.4 assert day_result.trades[2].rank_rvol_pressure_size_scale == 0.4 assert day_result.trades[1].shares < day_result.trades[0].shares def test_mid_liquidity_fragility_scales_only_fragile_mid_premarket_profiles() -> None: def bars_for(premarket_dollar_vol: float) -> list[dict]: premarket_volume = int(premarket_dollar_vol / 100.0) return [ {"timestamp": "2026-01-05T08:00:00-05:00", "open": 100.0, "high": 100.0, "low": 100.0, "close": 100.0, "volume": premarket_volume}, {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.8, "high": 102.4, "low": 101.5, "close": 102.2, "volume": 1000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.3, "volume": 1000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.4, "volume": 1000}, ] bars = { "FRAGILE": bars_for(30_000_000), "LEADER": bars_for(90_000_000), } enrichment = _enrichment_for("FRAGILE", "LEADER") params = ORBStrategyParams( engine_family="gainers_leader", allow_red_to_green_breakout=True, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.0, min_abs_gap_pct=0.0, max_candidates=10, max_trades_per_day=2, min_candidates_to_trade=1, atr_stop_multiplier=10.0, trailing_at_r=99.0, breakeven_at_r=99.0, risk_per_trade_pct=0.01, max_position_pct=1.0, slippage_bps=0.0, mid_liquidity_fragility_size_scale=0.25, mid_liquidity_fragility_mid_min_premarket_dollar_vol=20_000_000, mid_liquidity_fragility_mid_max_premarket_dollar_vol=75_000_000, mid_liquidity_fragility_mid_min_body_ratio=0.5, mid_liquidity_fragility_mid_max_body_ratio=0.75, ) day_result = simulate_orb_day( bars, "2026-01-05", params, enrichment, equity=10_000.0, ) trades = {trade.ticker: trade for trade in day_result.trades} assert trades["FRAGILE"].mid_liquidity_fragility_active is True assert trades["FRAGILE"].mid_liquidity_fragility_size_scale == 0.25 assert trades["LEADER"].mid_liquidity_fragility_active is False assert trades["LEADER"].mid_liquidity_fragility_size_scale is None assert trades["FRAGILE"].shares < trades["LEADER"].shares def test_gap_exhaustion_pressure_scales_stretched_weak_orb_close() -> None: bars = { "STRONG": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.9, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.9, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.8, "high": 102.4, "low": 101.5, "close": 102.2, "volume": 1000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.3, "volume": 1000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.3, "high": 102.6, "low": 102.1, "close": 102.4, "volume": 1000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.4, "volume": 1000}, ], "WEAK": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.2, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.2, "high": 102.0, "low": 100.1, "close": 101.8, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.8, "high": 102.4, "low": 101.5, "close": 102.2, "volume": 1000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.3, "volume": 1000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.3, "high": 102.6, "low": 102.1, "close": 102.4, "volume": 1000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.4, "volume": 1000}, ], } enrichment = _enrichment_for("STRONG", "WEAK") for ticker in enrichment: enrichment[ticker]["2026-01-05"]["prev_close"] = 98.0 enrichment[ticker]["2026-01-05"]["ret_5d"] = 0.05 enrichment[ticker]["2026-01-05"]["gap_zscore_20d"] = 1.25 params = ORBStrategyParams( engine_family="gainers_leader", allow_red_to_green_breakout=True, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.0, max_candidates=10, min_candidates_to_trade=1, atr_stop_multiplier=10.0, trailing_at_r=99.0, breakeven_at_r=99.0, risk_per_trade_pct=0.01, max_position_pct=1.0, slippage_bps=0.0, gap_exhaustion_pressure_min_gap_zscore=1.0, gap_exhaustion_pressure_max_close_location=0.65, gap_exhaustion_pressure_min_ret_5d=0.0, gap_exhaustion_pressure_min_gap_pct=0.02, gap_exhaustion_pressure_size_scale=0.25, ) day_result = simulate_orb_day( bars, "2026-01-05", params, enrichment, equity=10_000.0, ) trades = {trade.ticker: trade for trade in day_result.trades} assert trades["STRONG"].gap_exhaustion_pressure_size_scale is None assert trades["WEAK"].gap_exhaustion_pressure_size_scale == 0.25 assert trades["WEAK"].shares < trades["STRONG"].shares def test_stale_obv_rvol_pressure_scales_low_rvol_stale_obv_fill() -> None: bars = { ticker: [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.8, "high": 102.4, "low": 101.5, "close": 102.2, "volume": 1000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.3, "volume": 1000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.3, "high": 102.6, "low": 102.1, "close": 102.4, "volume": 1000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.4, "volume": 1000}, ] for ticker in ("FRESH", "STALE") } enrichment = _enrichment_for("FRESH", "STALE") enrichment["FRESH"]["2026-01-05"]["obv_slope_20"] = 0.2 enrichment["STALE"]["2026-01-05"]["obv_slope_20"] = -0.3 for ticker in enrichment: enrichment[ticker]["2026-01-05"]["ret_5d"] = -0.01 params = ORBStrategyParams( engine_family="gainers_leader", allow_red_to_green_breakout=True, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.0, max_candidates=10, min_candidates_to_trade=1, atr_stop_multiplier=10.0, trailing_at_r=99.0, breakeven_at_r=99.0, risk_per_trade_pct=0.01, max_position_pct=1.0, slippage_bps=0.0, stale_obv_rvol_pressure_max_rvol=8.0, stale_obv_rvol_pressure_max_obv_slope_20d=-0.2, stale_obv_rvol_pressure_max_ret_5d=0.05, stale_obv_rvol_pressure_max_score_rank_pct=1.0, stale_obv_rvol_pressure_size_scale=0.4, ) day_result = simulate_orb_day( bars, "2026-01-05", params, enrichment, equity=10_000.0, ) trades = {trade.ticker: trade for trade in day_result.trades} assert trades["FRESH"].stale_obv_rvol_pressure_size_scale is None assert trades["STALE"].stale_obv_rvol_pressure_size_scale == 0.4 assert trades["STALE"].shares < trades["FRESH"].shares def test_red_to_green_acceleration_boosts_only_clean_downside_reclaims() -> None: bars = { ticker: [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.8, "high": 102.4, "low": 101.5, "close": 102.2, "volume": 1000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.3, "volume": 1000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.3, "high": 102.6, "low": 102.1, "close": 102.4, "volume": 1000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.4, "volume": 1000}, ] for ticker in ("BASE", "BOOST", "RISK") } enrichment = _enrichment_for("BASE", "BOOST", "RISK") enrichment["BASE"]["2026-01-05"]["prev_close"] = 98.0 enrichment["BOOST"]["2026-01-05"]["prev_close"] = 106.0 enrichment["BOOST"]["2026-01-05"]["ret_5d"] = 0.0 enrichment["RISK"]["2026-01-05"]["prev_close"] = 106.0 enrichment["RISK"]["2026-01-05"]["ret_5d"] = 0.5 params = ORBStrategyParams( engine_family="gainers_leader", allow_red_to_green_breakout=True, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.0, max_candidates=10, min_candidates_to_trade=1, atr_stop_multiplier=10.0, trailing_at_r=99.0, breakeven_at_r=99.0, risk_per_trade_pct=0.01, max_position_pct=1.0, slippage_bps=0.0, hot_reclaim_min_abs_gap_pct=0.04, hot_reclaim_min_ret_5d=0.30, hot_reclaim_max_premarket_dollar_vol=10_000_000_000, hot_reclaim_min_body_ratio=0.50, hot_reclaim_min_close_location=0.80, hot_reclaim_action="scale", hot_reclaim_size_scale=0.35, red_to_green_acceleration_min_abs_gap_pct=0.04, red_to_green_acceleration_min_orb_return=0.005, red_to_green_acceleration_size_scale=1.5, ) day_result = simulate_orb_day( bars, "2026-01-05", params, enrichment, equity=10_000.0, ) trades = {trade.ticker: trade for trade in day_result.trades} assert trades["BASE"].red_to_green_acceleration_size_scale is None assert trades["BOOST"].red_to_green_acceleration_size_scale == 1.5 assert trades["RISK"].hot_reclaim_size_scale == 0.35 assert trades["RISK"].red_to_green_acceleration_size_scale is None assert trades["BOOST"].shares > trades["BASE"].shares assert trades["RISK"].shares < trades["BASE"].shares def test_liquid_leader_conviction_boosts_only_unscaled_liquid_leaders() -> None: bars = {} for ticker, premarket_volume in ( ("THIN", 1_000), ("LIQUID", 1_000_000), ("RISK", 1_000_000), ): bars[ticker] = [ {"timestamp": "2026-01-05T08:00:00-05:00", "open": 100.0, "high": 100.1, "low": 99.9, "close": 100.0, "volume": premarket_volume}, {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.8, "high": 102.4, "low": 101.5, "close": 102.2, "volume": 1000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.3, "volume": 1000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.3, "high": 102.6, "low": 102.1, "close": 102.4, "volume": 1000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.4, "volume": 1000}, ] enrichment = _enrichment_for("THIN", "LIQUID", "RISK") enrichment["THIN"]["2026-01-05"]["prev_close"] = 98.0 enrichment["LIQUID"]["2026-01-05"]["prev_close"] = 98.0 enrichment["RISK"]["2026-01-05"]["prev_close"] = 106.0 enrichment["RISK"]["2026-01-05"]["ret_5d"] = 0.5 params = ORBStrategyParams( engine_family="gainers_leader", allow_red_to_green_breakout=True, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.0, min_premarket_dollar_vol=0.0, max_candidates=10, min_candidates_to_trade=1, atr_stop_multiplier=10.0, trailing_at_r=99.0, breakeven_at_r=99.0, risk_per_trade_pct=0.01, max_position_pct=1.0, slippage_bps=0.0, hot_reclaim_min_abs_gap_pct=0.04, hot_reclaim_min_ret_5d=0.30, hot_reclaim_max_premarket_dollar_vol=200_000_000, hot_reclaim_min_body_ratio=0.50, hot_reclaim_min_close_location=0.80, hot_reclaim_action="scale", hot_reclaim_size_scale=0.35, liquid_leader_conviction_min_premarket_dollar_vol=50_000_000, liquid_leader_conviction_size_scale=1.5, ) day_result = simulate_orb_day( bars, "2026-01-05", params, enrichment, equity=10_000.0, ) trades = {trade.ticker: trade for trade in day_result.trades} assert trades["THIN"].liquid_leader_conviction_size_scale is None assert trades["LIQUID"].liquid_leader_conviction_size_scale == 1.5 assert trades["RISK"].hot_reclaim_size_scale == 0.35 assert trades["RISK"].liquid_leader_conviction_size_scale is None assert trades["LIQUID"].shares > trades["THIN"].shares assert trades["RISK"].shares < trades["THIN"].shares trigger_blocked = simulate_orb_day( bars, "2026-01-05", params.model_copy( update={"liquid_leader_conviction_allowed_trigger_types": ["vwap_reclaim"]} ), enrichment, equity=10_000.0, ) trigger_blocked_trades = {trade.ticker: trade for trade in trigger_blocked.trades} assert trigger_blocked_trades["LIQUID"].liquid_leader_conviction_size_scale is None def test_liquid_leader_secondary_boost_can_require_sector_confirmation() -> None: params = ORBStrategyParams( liquid_leader_conviction_size_scale=2.0, liquid_leader_conviction_secondary_requires_sector_confirmation=True, liquid_leader_conviction_secondary_min_body_ratio=0.20, ) cand = {"sector_confirmation_active": False, "body_ratio": 0.50} confirmed = {"sector_confirmation_active": True, "body_ratio": 0.50} weak_body = {"sector_confirmation_active": True, "body_ratio": 0.10} assert ( _orb_liquid_leader_conviction_size_scale( params, cand, score_rank_pct=1.0, trigger_type="soft_day_vwap_reclaim", direction_str="long", risk_size_scales=(1.0,), ) == 1.0 ) assert ( _orb_liquid_leader_conviction_size_scale( params, confirmed, score_rank_pct=1.0, trigger_type="soft_day_vwap_reclaim", direction_str="long", risk_size_scales=(1.0,), ) == 2.0 ) assert ( _orb_liquid_leader_conviction_size_scale( params, weak_body, score_rank_pct=1.0, trigger_type="soft_day_vwap_reclaim", direction_str="long", risk_size_scales=(1.0,), ) == 1.0 ) assert ( _orb_liquid_leader_conviction_size_scale( params, cand, score_rank_pct=1.0, trigger_type="orb", direction_str="long", risk_size_scales=(1.0,), ) == 2.0 ) def test_unsupported_attention_governor_scales_unconfirmed_liquid_laggards() -> None: params = ORBStrategyParams( unsupported_attention_size_scale=0.35, unsupported_attention_allowed_trigger_types=["orb"], unsupported_attention_liquid_min_premarket_dollar_vol=50_000_000, unsupported_attention_liquid_max_candidate_score=0.65, ) weak = { "premarket_dollar_vol": 75_000_000.0, "score": 0.60, "sector_confirmation_active": False, } confirmed = {**weak, "sector_confirmation_active": True} strong_score = {**weak, "score": 0.80} assert ( _orb_unsupported_attention_size_scale( params, weak, trigger_type="orb", direction_str="long", high_conviction_size_scales=(1.0,), ) == 0.35 ) assert ( _orb_unsupported_attention_size_scale( params, confirmed, trigger_type="orb", direction_str="long", high_conviction_size_scales=(1.0,), ) == 1.0 ) assert ( _orb_unsupported_attention_size_scale( params, strong_score, trigger_type="orb", direction_str="long", high_conviction_size_scales=(1.0,), ) == 1.0 ) def test_unsupported_attention_governor_scales_thin_positive_gap_spikes() -> None: params = ORBStrategyParams( unsupported_attention_size_scale=0.25, unsupported_attention_thin_max_premarket_dollar_vol=10_000_000, unsupported_attention_thin_min_gap_pct=0.025, unsupported_attention_thin_min_rvol=20.0, unsupported_attention_thin_max_rvol=25.0, ) thin_spike = { "premarket_dollar_vol": 8_000_000.0, "gap_pct": 0.03, "rvol": 22.0, } downside_reclaim = {**thin_spike, "gap_pct": -0.04} high_conviction = {**thin_spike} assert ( _orb_unsupported_attention_size_scale( params, thin_spike, trigger_type="orb", direction_str="long", high_conviction_size_scales=(1.0,), ) == 0.25 ) assert ( _orb_unsupported_attention_size_scale( params, downside_reclaim, trigger_type="orb", direction_str="long", high_conviction_size_scales=(1.0,), ) == 1.0 ) assert ( _orb_unsupported_attention_size_scale( params, high_conviction, trigger_type="orb", direction_str="long", high_conviction_size_scales=(1.5,), ) == 1.0 ) def test_thin_gap_up_loss_cap_marks_only_low_premarket_gap_ups() -> None: bars = {} for ticker, premarket_volume in ( ("THIN", 1_000), ("LIQUID", 2_000_000), ): bars[ticker] = [ {"timestamp": "2026-01-05T08:00:00-05:00", "open": 100.0, "high": 100.1, "low": 99.9, "close": 100.0, "volume": premarket_volume}, {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.8, "high": 102.4, "low": 101.5, "close": 102.2, "volume": 1000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.3, "volume": 1000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.3, "high": 102.6, "low": 102.1, "close": 102.4, "volume": 1000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.2, "high": 102.5, "low": 102.0, "close": 102.4, "volume": 1000}, ] enrichment = _enrichment_for("THIN", "LIQUID") for ticker in ("THIN", "LIQUID"): enrichment[ticker]["2026-01-05"]["prev_close"] = 98.0 enrichment[ticker]["2026-01-05"]["ret_5d"] = 0.03 params = ORBStrategyParams( engine_family="gainers_leader", allow_red_to_green_breakout=True, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.0, min_premarket_dollar_vol=0.0, max_candidates=10, min_candidates_to_trade=1, atr_stop_multiplier=10.0, trailing_at_r=99.0, breakeven_at_r=99.0, risk_per_trade_pct=0.01, max_position_pct=1.0, slippage_bps=0.0, thin_gap_up_loss_cap_min_gap_pct=0.02, thin_gap_up_loss_cap_min_ret_5d=-0.10, thin_gap_up_loss_cap_max_ret_5d=0.10, thin_gap_up_loss_cap_max_premarket_dollar_vol=10_000_000, thin_gap_up_loss_cap_min_body_ratio=0.50, thin_gap_up_loss_cap_min_close_location=0.80, thin_gap_up_loss_cap_pct=0.02, ) day_result = simulate_orb_day( bars, "2026-01-05", params, enrichment, equity=10_000.0, ) trades = {trade.ticker: trade for trade in day_result.trades} assert trades["THIN"].thin_gap_up_loss_cap_active assert not trades["LIQUID"].thin_gap_up_loss_cap_active def test_moderate_downside_loss_cap_marks_only_moderate_weak_reclaims() -> None: bars = {} for ticker in ("MOD", "DEEP", "STRONG"): bars[ticker] = [ {"timestamp": "2026-01-05T08:00:00-05:00", "open": 100.0, "high": 100.1, "low": 99.9, "close": 100.0, "volume": 1000}, {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.2, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 101.8, "low": 100.8, "close": 101.4, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.4, "high": 102.0, "low": 101.0, "close": 101.6, "volume": 1000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 101.6, "high": 102.1, "low": 101.4, "close": 101.8, "volume": 1000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.8, "high": 102.0, "low": 101.5, "close": 101.7, "volume": 1000}, ] enrichment = _enrichment_for("MOD", "DEEP", "STRONG") enrichment["MOD"]["2026-01-05"]["prev_close"] = 106.0 enrichment["MOD"]["2026-01-05"]["ret_5d"] = 0.10 enrichment["DEEP"]["2026-01-05"]["prev_close"] = 116.0 enrichment["DEEP"]["2026-01-05"]["ret_5d"] = 0.10 enrichment["STRONG"]["2026-01-05"]["prev_close"] = 106.0 enrichment["STRONG"]["2026-01-05"]["ret_5d"] = 0.35 params = ORBStrategyParams( engine_family="gainers_leader", allow_red_to_green_breakout=True, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.0, min_premarket_dollar_vol=0.0, max_candidates=10, min_candidates_to_trade=1, atr_stop_multiplier=10.0, trailing_at_r=99.0, breakeven_at_r=99.0, risk_per_trade_pct=0.01, max_position_pct=1.0, slippage_bps=0.0, moderate_downside_loss_cap_min_abs_gap_pct=0.05, moderate_downside_loss_cap_max_abs_gap_pct=0.10, moderate_downside_loss_cap_min_ret_5d=0.0, moderate_downside_loss_cap_max_ret_5d=0.25, moderate_downside_loss_cap_max_premarket_dollar_vol=10_000_000, moderate_downside_loss_cap_max_body_ratio=0.30, moderate_downside_loss_cap_max_close_location=0.75, moderate_downside_loss_cap_pct=0.01, ) day_result = simulate_orb_day( bars, "2026-01-05", params, enrichment, equity=10_000.0, ) trades = {trade.ticker: trade for trade in day_result.trades} assert trades["MOD"].moderate_downside_loss_cap_active assert not trades["DEEP"].moderate_downside_loss_cap_active assert not trades["STRONG"].moderate_downside_loss_cap_active def test_stale_obv_reversal_extra_gates_target_positive_gap_weak_open() -> None: bars = {} for ticker in ("WEAK", "NEG_GAP", "HIGH_RVOL", "STRONG_BODY"): first_volume = 500_000 if ticker == "HIGH_RVOL" else 1_000 first_close = 100.8 if ticker == "STRONG_BODY" else 98.9 bars[ticker] = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 99.0, "high": 101.0, "low": 98.0, "close": first_close, "volume": first_volume}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": first_close, "high": 101.6, "low": first_close - 0.2, "close": 101.2, "volume": 1_000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.2, "high": 102.0, "low": 101.0, "close": 101.6, "volume": 1_000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.5, "high": 101.8, "low": 101.2, "close": 101.7, "volume": 1_000}, ] enrichment = _enrichment_for("WEAK", "NEG_GAP", "HIGH_RVOL", "STRONG_BODY") for ticker in enrichment: enrichment[ticker]["2026-01-05"]["prev_close"] = 96.0 enrichment[ticker]["2026-01-05"]["ret_5d"] = 0.10 enrichment[ticker]["2026-01-05"]["obv_slope_20"] = 0.20 enrichment["NEG_GAP"]["2026-01-05"]["prev_close"] = 103.0 params = ORBStrategyParams( engine_family="gainers_leader", allow_red_to_green_breakout=True, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.0, min_abs_gap_pct=0.0, min_premarket_dollar_vol=0.0, max_candidates=10, min_candidates_to_trade=1, atr_stop_multiplier=10.0, trailing_at_r=99.0, breakeven_at_r=99.0, risk_per_trade_pct=0.001, max_position_pct=0.25, slippage_bps=0.0, stale_obv_reversal_max_obv_slope_20d=0.35, stale_obv_reversal_min_ret_5d=0.0, stale_obv_reversal_max_ret_5d=0.35, stale_obv_reversal_min_gap_pct=0.02, stale_obv_reversal_max_rvol=15.0, stale_obv_reversal_max_body_ratio=0.20, stale_obv_reversal_max_close_location=0.40, stale_obv_reversal_max_orb_return=0.0, stale_obv_reversal_action="confirm_scale", stale_obv_reversal_size_scale=0.35, stale_obv_reversal_loss_cap_pct=0.015, ) day_result = simulate_orb_day( bars, "2026-01-05", params, enrichment, equity=10_000.0, ) trades = {trade.ticker: trade for trade in day_result.trades} assert trades["WEAK"].stale_obv_reversal_requires_confirmation assert trades["WEAK"].stale_obv_reversal_size_scale == 0.35 assert not trades["NEG_GAP"].stale_obv_reversal_requires_confirmation assert not trades["HIGH_RVOL"].stale_obv_reversal_requires_confirmation assert not trades["STRONG_BODY"].stale_obv_reversal_requires_confirmation def test_fixed_loss_can_preserve_trailing_stop_management() -> None: bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 99.0, "high": 100.0, "low": 98.5, "close": 99.5, "volume": 1_000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 101.0, "volume": 2_000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.0, "high": 103.0, "low": 100.8, "close": 102.5, "volume": 2_000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.2, "low": 101.0, "close": 101.2, "volume": 2_000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 104.0, "high": 104.2, "low": 103.8, "close": 104.0, "volume": 1_000}, ] base_params = ORBStrategyParams( orb_minutes=5, sim_bar_minutes=5, order_timeout_minutes=30, atr_stop_multiplier=0.1, breakeven_at_r=99.0, trailing_at_r=1.0, trailing_stop_atr_multiplier=0.15, trailing_tighten_at_r=None, fixed_loss_pct=0.05, risk_per_trade_pct=0.01, max_position_pct=1.0, slippage_bps=0.0, ) legacy_trade = simulate_orb_trade( bars, bars[0], "long", atr=10.0, rvol=2.0, gap_pct=0.03, params=base_params, equity=10_000.0, date_str="2026-01-05", ticker="LEGACY", ) preserve_trade = simulate_orb_trade( bars, bars[0], "long", atr=10.0, rvol=2.0, gap_pct=0.03, params=base_params.model_copy(update={"fixed_loss_preserve_trailing": True}), equity=10_000.0, date_str="2026-01-05", ticker="PRESERVE", ) assert legacy_trade is not None assert preserve_trade is not None assert legacy_trade.exit_reason == "close" assert preserve_trade.exit_reason == "trailing_stop" assert preserve_trade.exit_price == pytest.approx(101.5) def test_conviction_runner_trail_only_delays_qualified_orb_tightening() -> None: bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1_000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 101.2, "low": 101.0, "close": 101.1, "volume": 2_000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.6, "high": 102.7, "low": 102.3, "close": 102.6, "volume": 2_000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.4, "high": 102.6, "low": 102.4, "close": 102.5, "volume": 2_000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 104.0, "high": 104.1, "low": 103.9, "close": 104.0, "volume": 1_000}, ] base_params = ORBStrategyParams( orb_minutes=5, sim_bar_minutes=5, order_timeout_minutes=30, atr_stop_multiplier=1.0, breakeven_at_r=1.0, trailing_at_r=1.0, trailing_stop_atr_multiplier=0.6, trailing_tighten_at_r=1.5, trailing_stop_atr_multiplier_tight=0.2, risk_per_trade_pct=0.01, max_position_pct=1.0, slippage_bps=0.0, ) runner_params = base_params.model_copy( update={ "conviction_runner_trail_allowed_trigger_types": ["orb"], "conviction_runner_trail_tighten_at_r": 2.0, "conviction_runner_trail_gap_atr_multiplier": 0.8, "conviction_runner_trail_min_abs_gap_pct": 0.035, "conviction_runner_trail_min_candidate_score": 0.9, "conviction_runner_trail_min_score_rank_pct": 0.9, } ) base_trade = simulate_orb_trade( bars, bars[0], "long", atr=1.0, rvol=8.0, gap_pct=0.04, params=base_params, equity=10_000.0, date_str="2026-01-05", ticker="BASE", score_rank_pct=1.0, candidate_score=0.95, ) runner_trade = simulate_orb_trade( bars, bars[0], "long", atr=1.0, rvol=8.0, gap_pct=0.04, params=runner_params, equity=10_000.0, date_str="2026-01-05", ticker="RUNNER", score_rank_pct=1.0, candidate_score=0.95, ) blocked_trade = simulate_orb_trade( bars, bars[0], "long", atr=1.0, rvol=8.0, gap_pct=0.04, params=runner_params, equity=10_000.0, date_str="2026-01-05", ticker="BLOCKED", score_rank_pct=1.0, candidate_score=0.50, ) assert base_trade is not None assert runner_trade is not None assert blocked_trade is not None assert base_trade.exit_reason == "trailing_stop" assert base_trade.exit_price == pytest.approx(102.4) assert runner_trade.exit_reason == "close" assert runner_trade.exit_price == pytest.approx(104.0) assert blocked_trade.exit_reason == "trailing_stop" assert blocked_trade.exit_price == pytest.approx(base_trade.exit_price) def test_aggregated_bar_entry_fills_on_next_raw_bar_after_signal() -> None: bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 103.0, "high": 103.2, "low": 102.8, "close": 103.0, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 103.0, "high": 103.0, "low": 102.9, "close": 102.95, "volume": 1000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.95, "high": 103.1, "low": 102.9, "close": 103.0, "volume": 1000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 103.0, "high": 103.1, "low": 102.8, "close": 102.9, "volume": 1000}, {"timestamp": "2026-01-05T09:55:00-05:00", "open": 102.9, "high": 103.0, "low": 102.7, "close": 102.8, "volume": 1000}, {"timestamp": "2026-01-05T10:00:00-05:00", "open": 102.8, "high": 103.3, "low": 102.7, "close": 103.1, "volume": 1000}, {"timestamp": "2026-01-05T10:05:00-05:00", "open": 104.5, "high": 104.8, "low": 104.4, "close": 104.7, "volume": 1000}, {"timestamp": "2026-01-05T10:10:00-05:00", "open": 104.7, "high": 104.9, "low": 104.6, "close": 104.8, "volume": 1000}, ] params = ORBStrategyParams( sim_bar_minutes=30, orb_minutes=5, order_timeout_minutes=45, atr_stop_multiplier=10.0, trailing_at_r=99.0, breakeven_at_r=99.0, slippage_bps=0.0, ) trade = simulate_orb_trade( bars, bars[0], "long", atr=1.0, rvol=2.0, gap_pct=0.01, params=params, equity=10_000.0, date_str="2026-01-05", ticker="TEST", ) assert trade is not None assert trade.entry_time == "2026-01-05T10:05:00-05:00" assert trade.entry_price == 104.5 def test_same_bar_stop_confirmation_delays_ambiguous_entry_bar_stop() -> None: bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.2, "low": 100.4, "close": 101.1, "volume": 1500}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.1, "high": 102.0, "low": 101.0, "close": 101.8, "volume": 1600}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.1, "high": 101.4, "low": 100.9, "close": 101.2, "volume": 1000}, ] base_params = ORBStrategyParams( orb_minutes=5, sim_bar_minutes=5, order_timeout_minutes=30, atr_stop_multiplier=0.5, trailing_at_r=99.0, breakeven_at_r=99.0, slippage_bps=0.0, risk_per_trade_pct=0.01, max_position_pct=1.0, ) default_trade = simulate_orb_trade( bars, bars[0], "long", atr=1.0, rvol=8.0, gap_pct=0.03, params=base_params, equity=10_000.0, date_str="2026-01-05", ticker="STOP", ) delayed_trade = simulate_orb_trade( bars, bars[0], "long", atr=1.0, rvol=8.0, gap_pct=0.03, params=base_params.model_copy( update={ "same_bar_stop_confirmation_enabled": True, "same_bar_stop_confirmation_allowed_trigger_types": ["orb"], } ), equity=10_000.0, date_str="2026-01-05", ticker="DELAY", ) blocked_by_trigger = simulate_orb_trade( bars, bars[0], "long", atr=1.0, rvol=8.0, gap_pct=0.03, params=base_params.model_copy( update={ "same_bar_stop_confirmation_enabled": True, "same_bar_stop_confirmation_allowed_trigger_types": ["vwap_reclaim"], } ), equity=10_000.0, date_str="2026-01-05", ticker="ALLOWLIST", ) assert default_trade is not None assert default_trade.exit_reason == "stop_loss" assert default_trade.entry_time == default_trade.exit_time assert default_trade.total_capital_deployed > 0 assert delayed_trade is not None assert delayed_trade.entry_time == "2026-01-05T09:40:00-05:00" assert delayed_trade.entry_price == 101.8 assert blocked_by_trigger is not None assert blocked_by_trigger.exit_reason == "stop_loss" assert blocked_by_trigger.total_capital_deployed > 0 def test_early_failure_exit_cuts_failed_breakout() -> None: bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.2, "low": 100.7, "close": 101.0, "volume": 1200}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.0, "high": 101.1, "low": 100.6, "close": 100.8, "volume": 1500}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.0, "high": 102.2, "low": 101.8, "close": 102.0, "volume": 1000}, ] params = ORBStrategyParams( orb_minutes=5, sim_bar_minutes=5, order_timeout_minutes=30, atr_stop_multiplier=10.0, trailing_at_r=99.0, breakeven_at_r=99.0, slippage_bps=0.0, risk_per_trade_pct=0.01, max_position_pct=1.0, early_failure_exit_minutes=15, early_failure_exit_level="breakout", early_failure_exit_max_peak_r=0.50, ) trade = simulate_orb_trade( bars, bars[0], "long", atr=1.0, rvol=8.0, gap_pct=0.03, params=params, equity=10_000.0, date_str="2026-01-05", ticker="FAIL", ) assert trade is not None assert trade.exit_reason == "early_failure" assert trade.exit_time == "2026-01-05T09:40:00-05:00" assert trade.exit_price == 100.8 def test_early_failure_exit_allows_trade_after_positive_close_excursion() -> None: bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.2, "low": 100.7, "close": 101.0, "volume": 1200}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.0, "high": 102.2, "low": 100.9, "close": 102.0, "volume": 1500}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.1, "low": 100.5, "close": 100.8, "volume": 1500}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.0, "high": 102.2, "low": 101.8, "close": 102.0, "volume": 1000}, ] params = ORBStrategyParams( orb_minutes=5, sim_bar_minutes=5, order_timeout_minutes=30, atr_stop_multiplier=1.0, trailing_at_r=99.0, breakeven_at_r=99.0, slippage_bps=0.0, risk_per_trade_pct=0.01, max_position_pct=1.0, early_failure_exit_minutes=15, early_failure_exit_level="breakout", early_failure_exit_max_peak_r=0.50, ) trade = simulate_orb_trade( bars, bars[0], "long", atr=1.0, rvol=8.0, gap_pct=0.03, params=params, equity=10_000.0, date_str="2026-01-05", ticker="HOLD", ) assert trade is not None assert trade.exit_reason == "close" assert trade.exit_price == 102.0 def test_pyramid_quality_gates_control_add_on() -> None: bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.2, "low": 100.7, "close": 101.0, "volume": 1200}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.0, "high": 102.4, "low": 101.0, "close": 102.2, "volume": 1500}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 103.8, "high": 104.1, "low": 103.7, "close": 104.0, "volume": 1000}, ] params = ORBStrategyParams( orb_minutes=5, sim_bar_minutes=5, order_timeout_minutes=30, atr_stop_multiplier=1.0, trailing_at_r=99.0, breakeven_at_r=99.0, slippage_bps=0.0, risk_per_trade_pct=0.02, max_position_pct=0.50, pyramid_at_r=1.0, pyramid_add_pct=0.50, pyramid_max_adds=1, pyramid_allowed_trigger_types=["orb"], pyramid_min_score_rank_pct=0.80, pyramid_min_rvol=6.0, ) allowed_trade = simulate_orb_trade( bars, bars[0], "long", atr=1.0, rvol=8.0, gap_pct=0.03, params=params, equity=10_000.0, date_str="2026-01-05", ticker="ALLOW", score_rank_pct=0.90, ) low_rank_trade = simulate_orb_trade( bars, bars[0], "long", atr=1.0, rvol=8.0, gap_pct=0.03, params=params, equity=10_000.0, date_str="2026-01-05", ticker="LOWRANK", score_rank_pct=0.70, ) low_rvol_trade = simulate_orb_trade( bars, bars[0], "long", atr=1.0, rvol=5.0, gap_pct=0.03, params=params, equity=10_000.0, date_str="2026-01-05", ticker="LOWRVOL", score_rank_pct=0.90, ) blocked_trigger_trade = simulate_orb_trade( bars, bars[0], "long", atr=1.0, rvol=8.0, gap_pct=0.03, params=params.model_copy(update={"pyramid_allowed_trigger_types": ["vwap_reclaim"]}), equity=10_000.0, date_str="2026-01-05", ticker="BLOCKED", score_rank_pct=0.90, ) blocked_context_trade = simulate_orb_trade( bars, bars[0], "long", atr=1.0, rvol=8.0, gap_pct=0.03, params=params, equity=10_000.0, date_str="2026-01-05", ticker="CTXBLOCK", score_rank_pct=0.90, pyramid_allowed=False, ) assert allowed_trade is not None assert low_rank_trade is not None assert low_rvol_trade is not None assert blocked_trigger_trade is not None assert blocked_context_trade is not None assert allowed_trade.pyramid_adds == 1 assert allowed_trade.pyramid_pnl > 0 assert low_rank_trade.pyramid_adds == 0 assert low_rvol_trade.pyramid_adds == 0 assert blocked_trigger_trade.pyramid_adds == 0 assert blocked_context_trade.pyramid_adds == 0 def test_pyramid_add_on_respects_available_cash() -> None: bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.2, "low": 100.7, "close": 101.0, "volume": 1200}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.0, "high": 102.4, "low": 101.0, "close": 102.2, "volume": 1500}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 103.8, "high": 104.1, "low": 103.7, "close": 104.0, "volume": 1000}, ] base_params = ORBStrategyParams( orb_minutes=5, sim_bar_minutes=5, order_timeout_minutes=30, atr_stop_multiplier=1.0, trailing_at_r=99.0, breakeven_at_r=99.0, slippage_bps=0.0, risk_per_trade_pct=0.02, pyramid_at_r=1.0, pyramid_add_pct=0.50, pyramid_max_adds=1, ) capped_trade = simulate_orb_trade( bars, bars[0], "long", atr=1.0, rvol=8.0, gap_pct=0.03, params=base_params.model_copy(update={"max_position_pct": 0.50}), equity=10_000.0, date_str="2026-01-05", ticker="CAPPED", available_cash=6_000.0, score_rank_pct=0.90, ) no_room_trade = simulate_orb_trade( bars, bars[0], "long", atr=1.0, rvol=8.0, gap_pct=0.03, params=base_params.model_copy(update={"max_position_pct": 1.00}), equity=10_000.0, date_str="2026-01-05", ticker="NOROOM", available_cash=6_000.0, score_rank_pct=0.90, ) assert capped_trade is not None assert no_room_trade is not None assert capped_trade.pyramid_adds == 1 assert capped_trade.total_capital_deployed <= 6_000.0 assert no_room_trade.pyramid_adds == 0 assert no_room_trade.total_capital_deployed <= 6_000.0 def test_entry_market_guard_uses_entry_bar_open_without_close_lookahead() -> None: bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.8, "close": 100.8, "volume": 10_000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.6, "high": 100.7, "low": 99.5, "close": 99.6, "volume": 12_000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 99.4, "high": 99.6, "low": 99.0, "close": 99.2, "volume": 12_000}, ] entry_ts = _parse_test_ts("2026-01-05T09:35:00-05:00") market_return = _market_return_at_entry_open(bars, entry_ts, "2026-01-05") assert market_return == pytest.approx(0.006) def test_entry_market_guard_scales_when_market_is_below_threshold() -> None: bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 100.2, "low": 99.7, "close": 99.8, "volume": 10_000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 99.7, "high": 99.8, "low": 99.0, "close": 99.1, "volume": 12_000}, ] params = ORBStrategyParams( entry_market_guard_enabled=True, entry_market_guard_min_return_pct=-0.001, entry_market_guard_size_scale=0.4, ) active, market_return, size_scale = _entry_market_guard_scale( params, bars, _parse_test_ts("2026-01-05T09:35:00-05:00"), "2026-01-05", is_soft_day=False, ) assert active is True assert market_return == pytest.approx(-0.003) assert size_scale == pytest.approx(0.4) def test_daily_loss_limit_counts_only_losses_realized_before_next_breakout() -> None: params = ORBStrategyParams( orb_minutes=5, sim_bar_minutes=5, order_timeout_minutes=45, atr_stop_multiplier=1.0, trailing_at_r=99.0, breakeven_at_r=99.0, slippage_bps=0.0, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, max_candidates=20, min_candidates_to_trade=1, risk_per_trade_pct=1.0, max_position_pct=1.0, daily_max_loss_pct=0.005, max_stops_per_day=99, ) early_close_loser = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.2, "low": 100.5, "close": 101.1, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 100.7, "high": 100.8, "low": 100.5, "close": 100.6, "volume": 1000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 100.6, "high": 100.7, "low": 100.4, "close": 100.6, "volume": 1000}, {"timestamp": "2026-01-05T10:00:00-05:00", "open": 100.6, "high": 100.7, "low": 100.4, "close": 100.5, "volume": 1000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 100.5, "high": 100.6, "low": 100.4, "close": 100.4, "volume": 1000}, ] late_winner = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 100.9, "low": 100.5, "close": 100.7, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 100.7, "high": 100.8, "low": 100.5, "close": 100.6, "volume": 1000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 100.6, "high": 100.7, "low": 100.4, "close": 100.6, "volume": 1000}, {"timestamp": "2026-01-05T10:00:00-05:00", "open": 100.6, "high": 101.3, "low": 100.5, "close": 101.2, "volume": 1000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.2, "high": 102.0, "low": 101.0, "close": 101.8, "volume": 1000}, ] day_result = simulate_orb_day( { "EARLY_CLOSE_LOSER": early_close_loser, "LATE_WINNER": late_winner, }, "2026-01-05", params, _enrichment_for("EARLY_CLOSE_LOSER", "LATE_WINNER"), equity=10_000.0, ) assert [trade.ticker for trade in day_result.trades] == ["EARLY_CLOSE_LOSER", "LATE_WINNER"] assert day_result.trades[0].exit_time == "2026-01-05T15:55:00-05:00" assert day_result.trades[1].entry_time == "2026-01-05T10:00:00-05:00" def test_crowded_gap_reject_filters_extended_positive_gap_candidates() -> None: bars = { "CROWDED": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 105.0, "low": 99.0, "close": 104.0, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 104.0, "high": 106.0, "low": 103.0, "close": 105.0, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 105.0, "high": 106.0, "low": 104.0, "close": 105.5, "volume": 1000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 105.5, "high": 106.0, "low": 105.0, "close": 105.7, "volume": 1000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 105.5, "high": 106.0, "low": 105.0, "close": 105.8, "volume": 1000}, ], "FRESH": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.0, "close": 101.0, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 103.0, "low": 100.8, "close": 102.5, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.5, "high": 103.0, "low": 102.0, "close": 102.8, "volume": 1000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.8, "high": 103.0, "low": 102.5, "close": 102.9, "volume": 1000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.8, "high": 103.0, "low": 102.5, "close": 102.9, "volume": 1000}, ], } enrichment = _enrichment_for("CROWDED", "FRESH") enrichment["CROWDED"]["2026-01-05"]["prev_close"] = 97.0 enrichment["CROWDED"]["2026-01-05"]["ret_5d"] = 0.30 enrichment["FRESH"]["2026-01-05"]["prev_close"] = 97.0 enrichment["FRESH"]["2026-01-05"]["ret_5d"] = 0.05 stats: dict[str, int] = {} params = ORBStrategyParams( min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=None, max_candidates=10, crowded_gap_reject_min_gap_pct=0.02, crowded_gap_reject_min_ret_5d=0.20, crowded_gap_reject_min_body_ratio=0.60, crowded_gap_reject_min_close_location=0.65, ) candidates = compute_orb_candidates( bars, "2026-01-05", params, enrichment, _stats_out=stats ) assert [cand["ticker"] for cand in candidates] == ["FRESH"] assert stats["crowded_gap"] == 1 def test_crowded_gap_confirm_keeps_candidate_with_confirmation_flag() -> None: bars = { "CROWDED": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 105.0, "low": 99.0, "close": 104.0, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 104.0, "high": 106.0, "low": 103.0, "close": 105.0, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 105.0, "high": 106.0, "low": 104.0, "close": 105.5, "volume": 1000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 105.5, "high": 106.0, "low": 105.0, "close": 105.7, "volume": 1000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 105.5, "high": 106.0, "low": 105.0, "close": 105.8, "volume": 1000}, ], } enrichment = _enrichment_for("CROWDED") enrichment["CROWDED"]["2026-01-05"]["prev_close"] = 97.0 enrichment["CROWDED"]["2026-01-05"]["ret_5d"] = 0.30 stats: dict[str, int] = {} params = ORBStrategyParams( min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=None, max_candidates=10, crowded_gap_reject_min_gap_pct=0.02, crowded_gap_reject_min_ret_5d=0.20, crowded_gap_reject_min_body_ratio=0.60, crowded_gap_reject_min_close_location=0.65, crowded_gap_action="confirm", ) candidates = compute_orb_candidates( bars, "2026-01-05", params, enrichment, _stats_out=stats ) assert [cand["ticker"] for cand in candidates] == ["CROWDED"] assert candidates[0]["crowded_gap_requires_confirmation"] is True assert stats["crowded_gap"] == 1 def test_crowded_gap_scale_keeps_candidate_with_size_scale() -> None: bars = { "CROWDED": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 105.0, "low": 99.0, "close": 104.0, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 104.0, "high": 106.0, "low": 103.0, "close": 105.0, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 105.0, "high": 106.0, "low": 104.0, "close": 105.5, "volume": 1000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 105.5, "high": 106.0, "low": 105.0, "close": 105.7, "volume": 1000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 105.5, "high": 106.0, "low": 105.0, "close": 105.8, "volume": 1000}, ], } enrichment = _enrichment_for("CROWDED") enrichment["CROWDED"]["2026-01-05"]["prev_close"] = 97.0 enrichment["CROWDED"]["2026-01-05"]["ret_5d"] = 0.30 stats: dict[str, int] = {} params = ORBStrategyParams( min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=None, max_candidates=10, crowded_gap_reject_min_gap_pct=0.02, crowded_gap_reject_min_ret_5d=0.20, crowded_gap_reject_min_body_ratio=0.60, crowded_gap_reject_min_close_location=0.65, crowded_gap_action="scale", crowded_gap_size_scale=0.4, ) candidates = compute_orb_candidates( bars, "2026-01-05", params, enrichment, _stats_out=stats ) assert [cand["ticker"] for cand in candidates] == ["CROWDED"] assert candidates[0]["crowded_gap_requires_confirmation"] is False assert candidates[0]["crowded_gap_size_scale"] == 0.4 assert stats["crowded_gap"] == 1 def test_countertrend_gap_scale_keeps_candidate_with_size_scale() -> None: bars = { "BOUNCE": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 105.0, "low": 99.0, "close": 104.5, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 104.5, "high": 106.0, "low": 103.0, "close": 105.0, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 105.0, "high": 106.0, "low": 104.0, "close": 105.5, "volume": 1000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 105.5, "high": 106.0, "low": 105.0, "close": 105.7, "volume": 1000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 105.5, "high": 106.0, "low": 105.0, "close": 105.8, "volume": 1000}, ], } enrichment = _enrichment_for("BOUNCE") enrichment["BOUNCE"]["2026-01-05"]["prev_close"] = 97.0 enrichment["BOUNCE"]["2026-01-05"]["ret_5d"] = -0.08 stats: dict[str, int] = {} params = ORBStrategyParams( min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=None, max_candidates=10, countertrend_gap_min_gap_pct=0.02, countertrend_gap_max_ret_5d=0.0, countertrend_gap_min_body_ratio=0.60, countertrend_gap_min_close_location=0.65, countertrend_gap_action="scale", countertrend_gap_size_scale=0.45, ) candidates = compute_orb_candidates( bars, "2026-01-05", params, enrichment, _stats_out=stats ) assert [cand["ticker"] for cand in candidates] == ["BOUNCE"] assert candidates[0]["countertrend_gap_requires_confirmation"] is False assert candidates[0]["countertrend_gap_size_scale"] == 0.45 assert stats["countertrend_gap"] == 1 def test_red_to_green_gap_weight_prioritizes_down_gap_reclaim_candidates() -> None: bars = { ticker: [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.0, "close": 101.0, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 103.0, "low": 100.8, "close": 102.5, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.5, "high": 103.0, "low": 102.0, "close": 102.8, "volume": 1000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.8, "high": 103.0, "low": 102.5, "close": 102.9, "volume": 1000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.8, "high": 103.0, "low": 102.5, "close": 102.9, "volume": 1000}, ] for ticker in ("DOWN_RECLAIM", "UP_GAP") } enrichment = _enrichment_for("DOWN_RECLAIM", "UP_GAP") enrichment["DOWN_RECLAIM"]["2026-01-05"]["prev_close"] = 103.0 enrichment["UP_GAP"]["2026-01-05"]["prev_close"] = 97.0 params = ORBStrategyParams( engine_family="gainers_leader", min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=None, weight_rvol=0.0, weight_gap=0.0, weight_dollar_vol=0.0, weight_premarket_dollar_vol=0.0, weight_red_to_green_gap=1.0, max_candidates=10, ) candidates = compute_orb_candidates(bars, "2026-01-05", params, enrichment) assert [cand["ticker"] for cand in candidates] == ["DOWN_RECLAIM", "UP_GAP"] assert candidates[0]["score"] > candidates[1]["score"] def test_red_to_green_reserved_slot_replaces_lowest_normal_candidate() -> None: bars = { "UP_HIGH": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.0, "close": 101.0, "volume": 3000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 103.0, "low": 100.8, "close": 102.5, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.5, "high": 103.0, "low": 102.0, "close": 102.8, "volume": 1000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.8, "high": 103.0, "low": 102.5, "close": 102.9, "volume": 1000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.8, "high": 103.0, "low": 102.5, "close": 102.9, "volume": 1000}, ], "UP_SECOND": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.0, "close": 101.0, "volume": 2000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 103.0, "low": 100.8, "close": 102.5, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.5, "high": 103.0, "low": 102.0, "close": 102.8, "volume": 1000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.8, "high": 103.0, "low": 102.5, "close": 102.9, "volume": 1000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.8, "high": 103.0, "low": 102.5, "close": 102.9, "volume": 1000}, ], "DOWN_RECLAIM": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.0, "close": 101.0, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 103.0, "low": 100.8, "close": 102.5, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.5, "high": 103.0, "low": 102.0, "close": 102.8, "volume": 1000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.8, "high": 103.0, "low": 102.5, "close": 102.9, "volume": 1000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.8, "high": 103.0, "low": 102.5, "close": 102.9, "volume": 1000}, ], } enrichment = _enrichment_for("UP_HIGH", "UP_SECOND", "DOWN_RECLAIM") enrichment["UP_HIGH"]["2026-01-05"]["prev_close"] = 97.0 enrichment["UP_SECOND"]["2026-01-05"]["prev_close"] = 97.0 enrichment["DOWN_RECLAIM"]["2026-01-05"]["prev_close"] = 103.0 params = ORBStrategyParams( engine_family="gainers_leader", min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=None, weight_rvol=0.0, weight_gap=0.0, weight_dollar_vol=1.0, weight_premarket_dollar_vol=0.0, max_candidates=2, red_to_green_reserved_slots=1, red_to_green_min_abs_gap_pct=0.02, red_to_green_min_close_location=0.60, ) candidates = compute_orb_candidates(bars, "2026-01-05", params, enrichment) assert [cand["ticker"] for cand in candidates] == ["UP_HIGH", "DOWN_RECLAIM"] assert candidates[1]["red_to_green_reserved"] is True def test_red_to_green_reserved_slot_can_require_opening_acceleration() -> None: bars = { "UP_HIGH": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.0, "close": 101.0, "volume": 3000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 103.0, "low": 100.8, "close": 102.5, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.5, "high": 103.0, "low": 102.0, "close": 102.8, "volume": 1000}, ], "DOWN_WEAK_RECLAIM": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.0, "close": 101.0, "volume": 2000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 103.0, "low": 100.8, "close": 102.5, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.5, "high": 103.0, "low": 102.0, "close": 102.8, "volume": 1000}, ], "DOWN_ACCEL": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 112.0, "low": 99.0, "close": 111.0, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 111.0, "high": 113.0, "low": 110.8, "close": 112.5, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 112.5, "high": 113.0, "low": 112.0, "close": 112.8, "volume": 1000}, ], } enrichment = _enrichment_for("UP_HIGH", "DOWN_WEAK_RECLAIM", "DOWN_ACCEL") enrichment["UP_HIGH"]["2026-01-05"]["prev_close"] = 97.0 enrichment["DOWN_WEAK_RECLAIM"]["2026-01-05"]["prev_close"] = 103.0 enrichment["DOWN_ACCEL"]["2026-01-05"]["prev_close"] = 113.0 params = ORBStrategyParams( engine_family="gainers_leader", min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=None, weight_rvol=0.0, weight_gap=0.0, weight_dollar_vol=1.0, weight_premarket_dollar_vol=0.0, max_candidates=2, red_to_green_reserved_slots=1, red_to_green_min_abs_gap_pct=0.02, red_to_green_min_close_location=0.60, red_to_green_min_orb_return=0.08, ) candidates = compute_orb_candidates(bars, "2026-01-05", params, enrichment) assert [cand["ticker"] for cand in candidates] == ["UP_HIGH", "DOWN_ACCEL"] assert candidates[1]["red_to_green_reserved"] is True def test_daily_loss_limit_is_not_tightened_by_day_size_scalers() -> None: params = ORBStrategyParams( orb_minutes=5, sim_bar_minutes=5, order_timeout_minutes=45, atr_stop_multiplier=0.5, trailing_at_r=99.0, breakeven_at_r=99.0, slippage_bps=0.0, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, max_candidates=20, min_candidates_to_trade=1, risk_per_trade_pct=0.06, max_position_pct=1.0, daily_max_loss_pct=0.05, max_stops_per_day=99, market_regime_ticker="SPY", regime_size_scale_low=0.0, regime_size_scale_high=0.02, regime_size_scale_min=0.5, ) early_loser = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.2, "low": 100.7, "close": 101.1, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.0, "high": 101.0, "low": 100.2, "close": 100.3, "volume": 1000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 100.3, "high": 100.4, "low": 100.2, "close": 100.3, "volume": 1000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 100.3, "high": 100.4, "low": 100.2, "close": 100.3, "volume": 1000}, ] late_winner = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.6, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.6, "high": 100.9, "low": 100.4, "close": 100.7, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 100.7, "high": 100.8, "low": 100.5, "close": 100.6, "volume": 1000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 100.6, "high": 100.7, "low": 100.5, "close": 100.6, "volume": 1000}, {"timestamp": "2026-01-05T10:00:00-05:00", "open": 100.6, "high": 101.3, "low": 100.7, "close": 101.2, "volume": 1000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.2, "high": 102.2, "low": 101.1, "close": 102.0, "volume": 1000}, ] enrichment = _enrichment_for("EARLY_LOSER", "LATE_WINNER") enrichment["SPY"] = { "2026-01-05": { "prev_close": 100.0, "today_open": 100.0, } } day_result = simulate_orb_day( { "EARLY_LOSER": early_loser, "LATE_WINNER": late_winner, }, "2026-01-05", params, enrichment, equity=10_000.0, ) assert day_result.regime_scaler == pytest.approx(0.5) assert [trade.ticker for trade in day_result.trades] == ["EARLY_LOSER", "LATE_WINNER"] def test_sparse_day_scaler_reduces_orb_day_budget_without_skipping_trade() -> None: bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.4, "low": 100.7, "close": 101.2, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.2, "high": 101.6, "low": 101.1, "close": 101.5, "volume": 1000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 101.5, "high": 101.8, "low": 101.4, "close": 101.7, "volume": 1000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 101.7, "high": 101.9, "low": 101.6, "close": 101.8, "volume": 1000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.5, "high": 102.0, "low": 101.4, "close": 101.9, "volume": 1000}, ] baseline = ORBStrategyParams( orb_minutes=5, sim_bar_minutes=5, order_timeout_minutes=20, atr_stop_multiplier=1.0, trailing_at_r=99.0, breakeven_at_r=99.0, slippage_bps=0.0, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=None, max_candidates=5, min_candidates_to_trade=1, risk_per_trade_pct=0.1, max_position_pct=1.0, daily_max_loss_pct=1.0, max_stops_per_day=99, ) scaled = baseline.model_copy(update={ "full_size_positions_threshold": 2, "sparse_day_size_floor": 0.5, }) enrichment = _enrichment_for("AAA") base_day = simulate_orb_day( {"AAA": bars}, "2026-01-05", baseline, enrichment, equity=10_000.0, ) scaled_day = simulate_orb_day( {"AAA": bars}, "2026-01-05", scaled, enrichment, equity=10_000.0, ) assert len(base_day.trades) == 1 assert len(scaled_day.trades) == 1 assert scaled_day.sparse_day_scaler == 0.5 assert scaled_day.capital_deployed < base_day.capital_deployed assert scaled_day.trades[0].shares < base_day.trades[0].shares def test_chunked_orb_simulation_matches_single_run_statefully() -> None: params = ORBStrategyParams( orb_minutes=5, sim_bar_minutes=5, order_timeout_minutes=20, atr_stop_multiplier=1.0, trailing_at_r=99.0, breakeven_at_r=99.0, slippage_bps=0.0, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, max_candidates=20, min_candidates_to_trade=1, risk_per_trade_pct=0.01, max_position_pct=1.0, daily_max_loss_pct=1.0, max_stops_per_day=99, ticker_cooldown_days=1, settlement_days=1, compound_returns=False, ) def bars(day: str) -> list[dict]: return [ {"timestamp": f"{day}T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000}, {"timestamp": f"{day}T09:35:00-05:00", "open": 100.8, "high": 101.4, "low": 100.7, "close": 101.2, "volume": 1000}, {"timestamp": f"{day}T09:40:00-05:00", "open": 101.2, "high": 101.6, "low": 101.1, "close": 101.5, "volume": 1000}, {"timestamp": f"{day}T15:55:00-05:00", "open": 101.5, "high": 102.0, "low": 101.4, "close": 101.9, "volume": 1000}, ] trading_days = ["2026-01-05", "2026-01-06", "2026-01-07"] all_intraday = { day: {"AAA": bars(day)} for day in trading_days } enrichment = { "AAA": { day: { "atr_14": 1.0, "avg_dollar_vol_30d": 1_000_000_000.0, "avg_daily_vol_14d": 10_000.0, "prev_close": 99.0, "today_open": 100.0, } for day in trading_days } } single = run_orb_simulation(all_intraday, trading_days, params, enrichment) chunk1, state = run_orb_simulation_with_state( {day: all_intraday[day] for day in trading_days[:2]}, trading_days[:2], params, enrichment, ) chunk2, _ = run_orb_simulation_with_state( {trading_days[2]: all_intraday[trading_days[2]]}, trading_days[2:], params, enrichment, state=state, ) combined = chunk1 + chunk2 assert [len(day.trades) for day in combined] == [len(day.trades) for day in single] assert [round(day.daily_pnl, 6) for day in combined] == [round(day.daily_pnl, 6) for day in single] assert [trade.ticker for day in combined for trade in day.trades] == [ trade.ticker for day in single for trade in day.trades ] def test_chunked_orb_simulation_preserves_streak_sizing_state() -> None: params = ORBStrategyParams( orb_minutes=5, sim_bar_minutes=5, order_timeout_minutes=20, atr_stop_multiplier=1.0, trailing_at_r=99.0, breakeven_at_r=99.0, slippage_bps=0.0, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, max_candidates=20, min_candidates_to_trade=1, risk_per_trade_pct=0.1, max_position_pct=1.0, daily_max_loss_pct=1.0, max_stops_per_day=99, settlement_days=0, daily_budget_reset=True, streak_sizing_win_bonus=1.0, streak_sizing_max=3.0, single_trade_loss_cap_pct=0.5, ) def bars(day: str) -> list[dict]: return [ {"timestamp": f"{day}T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000}, {"timestamp": f"{day}T09:35:00-05:00", "open": 100.8, "high": 101.4, "low": 100.7, "close": 101.2, "volume": 1000}, {"timestamp": f"{day}T09:40:00-05:00", "open": 101.2, "high": 101.6, "low": 101.1, "close": 101.5, "volume": 1000}, {"timestamp": f"{day}T15:55:00-05:00", "open": 101.5, "high": 103.0, "low": 101.4, "close": 102.8, "volume": 1000}, ] trading_days = ["2026-01-05", "2026-01-06", "2026-01-07"] all_intraday = {day: {"AAA": bars(day)} for day in trading_days} enrichment = { "AAA": { day: { "atr_14": 1.0, "avg_dollar_vol_30d": 1_000_000_000.0, "avg_daily_vol_14d": 10_000.0, "prev_close": 99.0, "today_open": 100.0, } for day in trading_days } } single = run_orb_simulation(all_intraday, trading_days, params, enrichment) chunk1, state = run_orb_simulation_with_state( {day: all_intraday[day] for day in trading_days[:2]}, trading_days[:2], params, enrichment, ) chunk2, _ = run_orb_simulation_with_state( {trading_days[2]: all_intraday[trading_days[2]]}, trading_days[2:], params, enrichment, state=state, ) combined = chunk1 + chunk2 assert round(combined[2].capital_deployed, 6) == round(single[2].capital_deployed, 6) assert round(combined[2].daily_pnl, 6) == round(single[2].daily_pnl, 6) def test_daily_budget_reset_caps_buying_power_to_initial_capital() -> None: params = ORBStrategyParams( orb_minutes=5, sim_bar_minutes=5, order_timeout_minutes=20, atr_stop_multiplier=1.0, trailing_at_r=99.0, breakeven_at_r=99.0, slippage_bps=0.0, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.0, max_candidates=20, min_candidates_to_trade=1, risk_per_trade_pct=1.0, max_position_pct=1.0, daily_max_loss_pct=1.0, max_stops_per_day=99, settlement_days=1, daily_budget_reset=True, initial_capital=10_000.0, streak_sizing_win_bonus=1.0, streak_sizing_max=3.0, ) def bars(day: str) -> list[dict]: return [ {"timestamp": f"{day}T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000}, {"timestamp": f"{day}T09:35:00-05:00", "open": 100.8, "high": 102.0, "low": 100.7, "close": 101.8, "volume": 1000}, {"timestamp": f"{day}T09:40:00-05:00", "open": 101.8, "high": 102.5, "low": 101.7, "close": 102.2, "volume": 1000}, {"timestamp": f"{day}T09:45:00-05:00", "open": 102.2, "high": 103.0, "low": 102.1, "close": 102.8, "volume": 1000}, {"timestamp": f"{day}T15:55:00-05:00", "open": 104.0, "high": 105.0, "low": 103.8, "close": 104.8, "volume": 1000}, ] trading_days = ["2026-01-05", "2026-01-06", "2026-01-07"] all_intraday = { "2026-01-05": {"AAA": bars("2026-01-05")}, "2026-01-06": {"AAA": bars("2026-01-06")}, "2026-01-07": { "AAA": bars("2026-01-07"), "BBB": bars("2026-01-07"), }, } enrichment = { ticker: { day: { "atr_14": 1.0, "avg_dollar_vol_30d": 1_000_000_000.0, "avg_daily_vol_14d": 10_000.0, "prev_close": 99.0, "today_open": 100.0, } for day in trading_days } for ticker in ("AAA", "BBB") } results = run_orb_simulation(all_intraday, trading_days, params, enrichment) assert all(day.available_cash_start == params.initial_capital for day in results) assert results[2].trades assert results[2].capital_deployed <= params.initial_capital assert results[2].daily_return_pct == pytest.approx( results[2].daily_pnl / params.initial_capital ) def test_single_trade_loss_cap_equity_basis_scales_with_compound_equity() -> None: bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.0, "high": 102.0, "low": 100.8, "close": 101.8, "volume": 1000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.8, "high": 102.5, "low": 101.7, "close": 102.0, "volume": 1000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.4, "low": 101.9, "close": 102.2, "volume": 1000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.2, "high": 102.7, "low": 102.0, "close": 102.4, "volume": 1000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 102.0, "high": 103.0, "low": 101.9, "close": 102.8, "volume": 1000}, ] all_intraday = {"2026-01-05": {"AAA": bars}} enrichment = _enrichment_for("AAA") base_kwargs = dict( orb_minutes=5, sim_bar_minutes=5, order_timeout_minutes=20, atr_stop_multiplier=1.0, trailing_at_r=99.0, breakeven_at_r=99.0, slippage_bps=0.0, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=None, max_candidates=5, min_candidates_to_trade=1, risk_per_trade_pct=0.1, max_position_pct=1.0, daily_max_loss_pct=1.0, max_stops_per_day=99, compound_returns=True, daily_budget_reset=False, settlement_days=0, single_trade_loss_cap_pct=0.05, ) grown_state = ORBSimulationState(equity=20_000.0, peak_equity=20_000.0) initial_results, _ = run_orb_simulation_with_state( all_intraday, ["2026-01-05"], ORBStrategyParams(**base_kwargs, single_trade_loss_cap_basis="initial"), enrichment, state=grown_state, ) equity_results, _ = run_orb_simulation_with_state( all_intraday, ["2026-01-05"], ORBStrategyParams(**base_kwargs, single_trade_loss_cap_basis="equity"), enrichment, state=grown_state, ) assert equity_results[0].capital_deployed > initial_results[0].capital_deployed * 1.8 def test_chunked_orb_simulation_preserves_drawdown_governor_peak() -> None: params = ORBStrategyParams( orb_minutes=5, sim_bar_minutes=5, order_timeout_minutes=20, atr_stop_multiplier=1.0, trailing_at_r=99.0, breakeven_at_r=99.0, slippage_bps=0.0, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, max_candidates=20, min_candidates_to_trade=1, risk_per_trade_pct=0.1, max_position_pct=1.0, daily_max_loss_pct=1.0, max_stops_per_day=99, settlement_days=0, daily_budget_reset=True, drawdown_governor_threshold=0.05, drawdown_governor_min_scale=0.5, ) def winner_bars(day: str) -> list[dict]: return [ {"timestamp": f"{day}T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000}, {"timestamp": f"{day}T09:35:00-05:00", "open": 100.8, "high": 101.4, "low": 100.7, "close": 101.2, "volume": 1000}, {"timestamp": f"{day}T09:40:00-05:00", "open": 101.2, "high": 101.6, "low": 101.1, "close": 101.5, "volume": 1000}, {"timestamp": f"{day}T15:55:00-05:00", "open": 101.5, "high": 103.0, "low": 101.4, "close": 102.8, "volume": 1000}, ] def loser_bars(day: str) -> list[dict]: return [ {"timestamp": f"{day}T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 1000}, {"timestamp": f"{day}T09:35:00-05:00", "open": 100.8, "high": 101.0, "low": 99.8, "close": 100.0, "volume": 1000}, {"timestamp": f"{day}T09:40:00-05:00", "open": 100.0, "high": 100.1, "low": 98.8, "close": 99.0, "volume": 1000}, {"timestamp": f"{day}T15:55:00-05:00", "open": 99.0, "high": 99.2, "low": 98.9, "close": 99.1, "volume": 1000}, ] trading_days = ["2026-01-05", "2026-01-06", "2026-01-07"] all_intraday = { "2026-01-05": {"AAA": winner_bars("2026-01-05")}, "2026-01-06": {"AAA": loser_bars("2026-01-06")}, "2026-01-07": {"AAA": winner_bars("2026-01-07")}, } enrichment = { "AAA": { day: { "atr_14": 1.0, "avg_dollar_vol_30d": 1_000_000_000.0, "avg_daily_vol_14d": 10_000.0, "prev_close": 99.0, "today_open": 100.0, } for day in trading_days } } single = run_orb_simulation(all_intraday, trading_days, params, enrichment) chunk1, state = run_orb_simulation_with_state( {day: all_intraday[day] for day in trading_days[:2]}, trading_days[:2], params, enrichment, ) chunk2, _ = run_orb_simulation_with_state( {trading_days[2]: all_intraday[trading_days[2]]}, trading_days[2:], params, enrichment, state=state, ) combined = chunk1 + chunk2 assert round(combined[2].capital_deployed, 6) == round(single[2].capital_deployed, 6) assert round(combined[2].daily_pnl, 6) == round(single[2].daily_pnl, 6) def test_market_orb_quality_scaler_adjusts_day_sizing() -> None: params = ORBStrategyParams( engine_family="gainers_leader", orb_minutes=5, sim_bar_minutes=5, order_timeout_minutes=20, atr_stop_multiplier=1.0, trailing_at_r=99.0, breakeven_at_r=99.0, slippage_bps=0.0, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.0, min_premarket_dollar_vol=0.0, max_gap_pct=None, max_candidates=20, min_candidates_to_trade=1, risk_per_trade_pct=0.1, max_position_pct=1.0, daily_max_loss_pct=1.0, max_stops_per_day=99, daily_budget_reset=True, market_orb_quality_ticker="SPY", market_orb_quality_size_scale_low=0.4, market_orb_quality_size_scale_high=0.6, market_orb_quality_size_scale_min=0.8, market_orb_quality_size_scale_max=1.2, allow_doji_breakout=True, ) aaa_bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.8, "close": 100.9, "volume": 2000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.9, "high": 101.4, "low": 100.8, "close": 101.3, "volume": 2000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.3, "high": 101.5, "low": 101.1, "close": 101.4, "volume": 2000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 101.4, "high": 101.6, "low": 101.2, "close": 101.5, "volume": 2000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.5, "high": 103.0, "low": 101.4, "close": 102.8, "volume": 2000}, ] strong_spy = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 500.9, "volume": 10000}, ] weak_spy = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 499.8, "volume": 10000}, ] enrichment = _enrichment_for("AAA") strong = simulate_orb_day( {"AAA": aaa_bars, "SPY": strong_spy}, "2026-01-05", params, enrichment, equity=10_000.0, ) weak = simulate_orb_day( {"AAA": aaa_bars, "SPY": weak_spy}, "2026-01-05", params, enrichment, equity=10_000.0, ) assert strong.market_orb_quality_scaler == pytest.approx(1.2) assert weak.market_orb_quality_scaler == pytest.approx(0.8) assert strong.market_orb_quality_close_location == pytest.approx((500.9 - 499.5) / (501.0 - 499.5)) assert weak.market_orb_quality_close_location == pytest.approx((499.8 - 499.5) / (501.0 - 499.5)) assert strong.capital_deployed > weak.capital_deployed assert strong.daily_pnl > weak.daily_pnl def test_conditional_confirmation_activates_only_on_red_market_open() -> None: params = ORBStrategyParams( engine_family="gainers_leader", orb_minutes=5, sim_bar_minutes=5, order_timeout_minutes=20, atr_stop_multiplier=1.0, trailing_at_r=99.0, breakeven_at_r=99.0, slippage_bps=0.0, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.0, min_premarket_dollar_vol=0.0, max_gap_pct=None, max_candidates=20, min_candidates_to_trade=1, risk_per_trade_pct=0.1, max_position_pct=1.0, daily_max_loss_pct=1.0, max_stops_per_day=99, daily_budget_reset=True, allow_doji_breakout=True, conditional_confirmation_ticker="QQQ", conditional_confirmation_below_return_pct=0.0, ) aaa_bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 2000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.2, "low": 100.7, "close": 101.1, "volume": 2000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.1, "high": 101.2, "low": 100.7, "close": 100.9, "volume": 2000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 100.9, "high": 101.0, "low": 100.8, "close": 100.95, "volume": 2000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 100.95, "high": 101.0, "low": 100.85, "close": 100.9, "volume": 2000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 100.9, "high": 101.0, "low": 100.8, "close": 100.9, "volume": 2000}, ] green_qqq = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 500.8, "volume": 10000}, ] red_qqq = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 499.8, "volume": 10000}, ] enrichment = _enrichment_for("AAA") green = simulate_orb_day( {"AAA": aaa_bars, "QQQ": green_qqq}, "2026-01-05", params, enrichment, equity=10_000.0, ) red = simulate_orb_day( {"AAA": aaa_bars, "QQQ": red_qqq}, "2026-01-05", params, enrichment, equity=10_000.0, ) assert green.conditional_confirmation_active is False assert len(green.trades) == 1 assert red.conditional_confirmation_active is True assert red.trades == [] def test_market_orb_quality_divergence_guard_reduces_day_sizing() -> None: params = ORBStrategyParams( engine_family="gainers_leader", orb_minutes=5, sim_bar_minutes=5, order_timeout_minutes=20, atr_stop_multiplier=1.0, trailing_at_r=99.0, breakeven_at_r=99.0, slippage_bps=0.0, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.0, min_premarket_dollar_vol=0.0, max_gap_pct=None, max_candidates=20, min_candidates_to_trade=1, risk_per_trade_pct=0.1, max_position_pct=1.0, daily_max_loss_pct=1.0, max_stops_per_day=99, daily_budget_reset=True, allow_doji_breakout=True, market_orb_quality_ticker="SPY", market_orb_quality_secondary_ticker="QQQ", market_orb_quality_primary_strong_above=0.75, market_orb_quality_secondary_weak_below=0.5, market_orb_quality_divergence_scale=0.5, ) aaa_bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 2000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.4, "low": 100.7, "close": 101.2, "volume": 2000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.2, "high": 101.6, "low": 101.1, "close": 101.5, "volume": 2000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 101.5, "high": 101.8, "low": 101.4, "close": 101.7, "volume": 2000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 101.7, "high": 101.9, "low": 101.6, "close": 101.8, "volume": 2000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.5, "high": 103.0, "low": 101.4, "close": 102.8, "volume": 2000}, ] strong_spy = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 500.9, "volume": 10000}, ] weak_qqq = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 400.8, "low": 399.9, "close": 400.1, "volume": 10000}, ] strong_qqq = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 401.0, "low": 399.8, "close": 400.9, "volume": 10000}, ] enrichment = _enrichment_for("AAA") aligned = simulate_orb_day( {"AAA": aaa_bars, "SPY": strong_spy, "QQQ": strong_qqq}, "2026-01-05", params, enrichment, equity=10_000.0, ) diverged = simulate_orb_day( {"AAA": aaa_bars, "SPY": strong_spy, "QQQ": weak_qqq}, "2026-01-05", params, enrichment, equity=10_000.0, ) assert aligned.market_orb_quality_divergence_active is False assert diverged.market_orb_quality_divergence_active is True assert diverged.market_orb_quality_scaler == pytest.approx(0.5) assert diverged.capital_deployed < aligned.capital_deployed def test_market_orb_quality_veto_can_feed_rolling_loss_governor() -> None: params = ORBStrategyParams( engine_family="gainers_leader", orb_minutes=5, sim_bar_minutes=5, order_timeout_minutes=20, atr_stop_multiplier=1.0, trailing_at_r=99.0, breakeven_at_r=99.0, slippage_bps=0.0, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.0, min_premarket_dollar_vol=0.0, max_gap_pct=None, max_candidates=20, min_candidates_to_trade=1, risk_per_trade_pct=0.1, max_position_pct=1.0, daily_max_loss_pct=1.0, max_stops_per_day=99, daily_budget_reset=True, rolling_loss_days=1, rolling_loss_threshold=-0.01, market_orb_quality_ticker="SPY", market_orb_quality_secondary_ticker="QQQ", market_orb_quality_primary_strong_above=0.75, market_orb_quality_secondary_weak_below=0.50, market_orb_quality_divergence_scale=0.0, market_orb_quality_veto_rolling_loss_pct=-0.05, ) bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 2000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.2, "low": 100.7, "close": 101.1, "volume": 2000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.1, "high": 101.5, "low": 101.0, "close": 101.4, "volume": 2000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 101.4, "high": 101.6, "low": 101.2, "close": 101.5, "volume": 2000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 101.5, "high": 101.8, "low": 101.4, "close": 101.7, "volume": 2000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.7, "high": 102.0, "low": 101.2, "close": 101.8, "volume": 2000}, ] day2_bars = [ {**bar, "timestamp": bar["timestamp"].replace("2026-01-05", "2026-01-06")} for bar in bars ] strong_spy = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 500.9, "volume": 10000}, ] weak_qqq = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 401.0, "low": 399.8, "close": 400.2, "volume": 10000}, ] calm_spy_day2 = [ {"timestamp": "2026-01-06T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 500.6, "volume": 10000}, ] calm_qqq_day2 = [ {"timestamp": "2026-01-06T09:30:00-05:00", "open": 400.0, "high": 401.0, "low": 399.8, "close": 400.6, "volume": 10000}, ] enrichment = { "AAA": { day: { "atr_14": 1.0, "avg_dollar_vol_30d": 1_000_000_000.0, "avg_daily_vol_14d": 10_000.0, "prev_close": 99.0, "today_open": 100.0, } for day in ("2026-01-05", "2026-01-06") } } results = run_orb_simulation( { "2026-01-05": {"AAA": bars, "SPY": strong_spy, "QQQ": weak_qqq}, "2026-01-06": {"AAA": day2_bars, "SPY": calm_spy_day2, "QQQ": calm_qqq_day2}, }, ["2026-01-05", "2026-01-06"], params, enrichment, ) assert results[0].daily_pnl == 0.0 assert results[0].rolling_loss_synthetic_pnl == pytest.approx(-500.0) assert results[1].skip_reason == "rolling_loss" def test_market_orb_quality_divergence_guard_can_cap_total_trades() -> None: params = ORBStrategyParams( engine_family="gainers_leader", orb_minutes=5, sim_bar_minutes=5, order_timeout_minutes=20, atr_stop_multiplier=1.0, trailing_at_r=99.0, breakeven_at_r=99.0, slippage_bps=0.0, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.0, min_premarket_dollar_vol=0.0, max_gap_pct=None, max_candidates=20, min_candidates_to_trade=1, risk_per_trade_pct=0.1, max_position_pct=1.0, daily_max_loss_pct=1.0, max_stops_per_day=99, daily_budget_reset=True, allow_doji_breakout=True, market_orb_quality_ticker="SPY", market_orb_quality_secondary_ticker="QQQ", market_orb_quality_primary_strong_above=0.75, market_orb_quality_secondary_weak_below=0.5, market_orb_quality_divergence_max_trades=1, ) bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 2000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.4, "low": 100.7, "close": 101.2, "volume": 2000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.2, "high": 101.6, "low": 101.1, "close": 101.5, "volume": 2000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 101.5, "high": 101.8, "low": 101.4, "close": 101.7, "volume": 2000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 101.7, "high": 101.9, "low": 101.6, "close": 101.8, "volume": 2000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.8, "high": 102.5, "low": 101.7, "close": 102.2, "volume": 2000}, ] strong_spy = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 500.9, "volume": 10000}, ] weak_qqq = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 400.8, "low": 399.9, "close": 400.1, "volume": 10000}, ] strong_qqq = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 401.0, "low": 399.8, "close": 400.9, "volume": 10000}, ] enrichment = _enrichment_for("AAA", "BBB") aligned = simulate_orb_day( {"AAA": bars, "BBB": bars, "SPY": strong_spy, "QQQ": strong_qqq}, "2026-01-05", params, enrichment, equity=10_000.0, ) diverged = simulate_orb_day( {"AAA": bars, "BBB": bars, "SPY": strong_spy, "QQQ": weak_qqq}, "2026-01-05", params, enrichment, equity=10_000.0, ) assert aligned.market_orb_quality_divergence_max_trades_active is False assert len(aligned.trades) == 2 assert diverged.market_orb_quality_divergence_max_trades_active is True assert len(diverged.trades) == 1 def test_market_orb_quality_primary_weak_secondary_strong_guard_can_veto_day() -> None: params = ORBStrategyParams( engine_family="gainers_leader", orb_minutes=5, sim_bar_minutes=5, order_timeout_minutes=20, atr_stop_multiplier=1.0, trailing_at_r=99.0, breakeven_at_r=99.0, slippage_bps=0.0, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.0, min_premarket_dollar_vol=0.0, max_gap_pct=None, max_candidates=20, min_candidates_to_trade=1, risk_per_trade_pct=0.1, max_position_pct=1.0, daily_max_loss_pct=1.0, max_stops_per_day=99, daily_budget_reset=True, allow_doji_breakout=True, market_orb_quality_ticker="SPY", market_orb_quality_secondary_ticker="QQQ", market_orb_quality_primary_weak_above=0.20, market_orb_quality_primary_weak_below=0.30, market_orb_quality_secondary_strong_above=0.60, market_orb_quality_primary_weak_secondary_strong_scale=0.0, ) bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 2000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.4, "low": 100.7, "close": 101.2, "volume": 2000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.2, "high": 101.6, "low": 101.1, "close": 101.5, "volume": 2000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 101.5, "high": 101.8, "low": 101.4, "close": 101.7, "volume": 2000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 101.7, "high": 101.9, "low": 101.6, "close": 101.8, "volume": 2000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.5, "high": 103.0, "low": 101.4, "close": 102.8, "volume": 2000}, ] neutral_spy = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.0, "close": 500.6, "volume": 10000}, ] weak_spy = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.0, "close": 499.5, "volume": 10000}, ] strong_qqq = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 401.0, "low": 399.0, "close": 400.4, "volume": 10000}, ] enrichment = _enrichment_for("AAA") aligned = simulate_orb_day( {"AAA": bars, "SPY": neutral_spy, "QQQ": strong_qqq}, "2026-01-05", params, enrichment, equity=10_000.0, ) split_tape = simulate_orb_day( {"AAA": bars, "SPY": weak_spy, "QQQ": strong_qqq}, "2026-01-05", params, enrichment, equity=10_000.0, ) assert aligned.market_orb_quality_primary_weak_secondary_strong_active is False assert len(aligned.trades) == 1 assert split_tape.market_orb_quality_primary_weak_secondary_strong_active is True assert split_tape.market_orb_quality_scaler == pytest.approx(0.0) assert split_tape.trades == [] def test_market_orb_quality_primary_lag_secondary_lead_guard_can_veto_day() -> None: params = ORBStrategyParams( engine_family="gainers_leader", orb_minutes=5, sim_bar_minutes=5, order_timeout_minutes=20, atr_stop_multiplier=1.0, trailing_at_r=99.0, breakeven_at_r=99.0, slippage_bps=0.0, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.0, min_premarket_dollar_vol=0.0, max_gap_pct=None, max_candidates=20, min_candidates_to_trade=1, risk_per_trade_pct=0.1, max_position_pct=1.0, daily_max_loss_pct=1.0, max_stops_per_day=99, daily_budget_reset=True, allow_doji_breakout=True, market_orb_quality_ticker="SPY", market_orb_quality_secondary_ticker="QQQ", market_orb_quality_primary_lag_above=0.40, market_orb_quality_primary_lag_below=0.50, market_orb_quality_secondary_lead_above=0.70, market_orb_quality_secondary_lead_below=0.80, market_orb_quality_primary_lag_secondary_lead_scale=0.0, ) bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 2000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.2, "low": 100.7, "close": 101.1, "volume": 2000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.1, "high": 101.5, "low": 101.0, "close": 101.4, "volume": 2000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 101.4, "high": 101.6, "low": 101.2, "close": 101.5, "volume": 2000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 101.5, "high": 101.8, "low": 101.4, "close": 101.7, "volume": 2000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.7, "high": 102.0, "low": 101.2, "close": 101.8, "volume": 2000}, ] aligned_spy = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 500.8, "volume": 10000}, ] aligned_qqq = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 401.0, "low": 399.8, "close": 400.9, "volume": 10000}, ] lag_spy = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.0, "close": 499.9, "volume": 10000}, ] lead_qqq = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 401.0, "low": 399.0, "close": 400.5, "volume": 10000}, ] enrichment = _enrichment_for("AAA", "BBB") aligned = simulate_orb_day( {"AAA": bars, "BBB": bars, "SPY": aligned_spy, "QQQ": aligned_qqq}, "2026-01-05", params, enrichment, equity=10_000.0, ) split_tape = simulate_orb_day( {"AAA": bars, "BBB": bars, "SPY": lag_spy, "QQQ": lead_qqq}, "2026-01-05", params, enrichment, equity=10_000.0, ) assert aligned.market_orb_quality_primary_lag_secondary_lead_active is False assert len(aligned.trades) == 2 assert split_tape.market_orb_quality_primary_lag_secondary_lead_active is True assert split_tape.market_orb_quality_scaler == pytest.approx(0.0) assert split_tape.trades == [] def test_market_orb_quality_divergence_guard_can_target_mild_split_tape() -> None: params = ORBStrategyParams( engine_family="gainers_leader", orb_minutes=5, sim_bar_minutes=5, order_timeout_minutes=20, atr_stop_multiplier=1.0, trailing_at_r=99.0, breakeven_at_r=99.0, slippage_bps=0.0, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.0, min_premarket_dollar_vol=0.0, max_gap_pct=None, max_candidates=20, min_candidates_to_trade=1, risk_per_trade_pct=0.1, max_position_pct=1.0, daily_max_loss_pct=1.0, max_stops_per_day=99, daily_budget_reset=True, allow_doji_breakout=True, market_orb_quality_ticker="SPY", market_orb_quality_secondary_ticker="QQQ", market_orb_quality_primary_strong_above=0.75, market_orb_quality_secondary_weak_above=0.4, market_orb_quality_secondary_weak_below=0.5, market_orb_quality_divergence_scale=0.5, ) aaa_bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 2000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.4, "low": 100.7, "close": 101.2, "volume": 2000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.2, "high": 101.6, "low": 101.1, "close": 101.5, "volume": 2000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 101.5, "high": 101.8, "low": 101.4, "close": 101.7, "volume": 2000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 101.7, "high": 101.9, "low": 101.6, "close": 101.8, "volume": 2000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.5, "high": 103.0, "low": 101.4, "close": 102.8, "volume": 2000}, ] strong_spy = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 500.9, "volume": 10000}, ] mild_weak_qqq = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 400.9, "low": 400.0, "close": 400.41, "volume": 10000}, ] deep_weak_qqq = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 400.8, "low": 399.9, "close": 400.1, "volume": 10000}, ] enrichment = _enrichment_for("AAA") mild = simulate_orb_day( {"AAA": aaa_bars, "SPY": strong_spy, "QQQ": mild_weak_qqq}, "2026-01-05", params, enrichment, equity=10_000.0, ) deep = simulate_orb_day( {"AAA": aaa_bars, "SPY": strong_spy, "QQQ": deep_weak_qqq}, "2026-01-05", params, enrichment, equity=10_000.0, ) assert mild.market_orb_quality_divergence_active is True assert deep.market_orb_quality_divergence_active is False def test_market_orb_quality_joint_weak_guard_can_require_confirmation() -> None: params = ORBStrategyParams( engine_family="gainers_leader", orb_minutes=5, sim_bar_minutes=5, order_timeout_minutes=20, atr_stop_multiplier=1.0, trailing_at_r=99.0, breakeven_at_r=99.0, slippage_bps=0.0, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.0, min_premarket_dollar_vol=0.0, max_gap_pct=None, max_candidates=20, min_candidates_to_trade=1, risk_per_trade_pct=0.1, max_position_pct=1.0, daily_max_loss_pct=1.0, max_stops_per_day=99, daily_budget_reset=True, allow_doji_breakout=True, market_orb_quality_ticker="SPY", market_orb_quality_secondary_ticker="QQQ", market_orb_quality_joint_weak_primary_below=0.2, market_orb_quality_joint_weak_secondary_below=0.2, market_orb_quality_joint_weak_require_confirmation=True, ) aaa_bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 2000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.2, "low": 100.7, "close": 101.1, "volume": 2000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.1, "high": 101.2, "low": 100.7, "close": 100.9, "volume": 2000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 100.9, "high": 101.0, "low": 100.8, "close": 100.95, "volume": 2000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 100.95, "high": 101.0, "low": 100.85, "close": 100.9, "volume": 2000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 100.9, "high": 101.0, "low": 100.8, "close": 100.9, "volume": 2000}, ] strong_spy = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 500.8, "volume": 10000}, ] strong_qqq = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 401.0, "low": 399.8, "close": 400.9, "volume": 10000}, ] weak_spy = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 499.6, "volume": 10000}, ] weak_qqq = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 400.8, "low": 399.9, "close": 399.95, "volume": 10000}, ] enrichment = _enrichment_for("AAA") aligned = simulate_orb_day( {"AAA": aaa_bars, "SPY": strong_spy, "QQQ": strong_qqq}, "2026-01-05", params, enrichment, equity=10_000.0, ) joint_weak = simulate_orb_day( {"AAA": aaa_bars, "SPY": weak_spy, "QQQ": weak_qqq}, "2026-01-05", params, enrichment, equity=10_000.0, ) assert aligned.market_orb_quality_joint_weak_active is False assert aligned.conditional_confirmation_active is False assert len(aligned.trades) == 1 assert joint_weak.market_orb_quality_joint_weak_active is True assert joint_weak.conditional_confirmation_active is True assert joint_weak.trades == [] def test_market_orb_quality_joint_weak_guard_can_reduce_day_sizing() -> None: params = ORBStrategyParams( engine_family="gainers_leader", orb_minutes=5, sim_bar_minutes=5, order_timeout_minutes=20, atr_stop_multiplier=1.0, trailing_at_r=99.0, breakeven_at_r=99.0, slippage_bps=0.0, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.0, min_premarket_dollar_vol=0.0, max_gap_pct=None, max_candidates=20, min_candidates_to_trade=1, risk_per_trade_pct=0.1, max_position_pct=1.0, daily_max_loss_pct=1.0, max_stops_per_day=99, daily_budget_reset=True, allow_doji_breakout=True, market_orb_quality_ticker="SPY", market_orb_quality_secondary_ticker="QQQ", market_orb_quality_joint_weak_primary_below=0.2, market_orb_quality_joint_weak_secondary_below=0.2, market_orb_quality_joint_weak_scale=0.5, ) aaa_bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 2000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.4, "low": 100.7, "close": 101.2, "volume": 2000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.2, "high": 101.6, "low": 101.1, "close": 101.5, "volume": 2000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 101.5, "high": 101.8, "low": 101.4, "close": 101.7, "volume": 2000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 101.7, "high": 101.9, "low": 101.6, "close": 101.8, "volume": 2000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.5, "high": 103.0, "low": 101.4, "close": 102.8, "volume": 2000}, ] strong_spy = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 500.8, "volume": 10000}, ] strong_qqq = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 401.0, "low": 399.8, "close": 400.9, "volume": 10000}, ] weak_spy = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 499.6, "volume": 10000}, ] weak_qqq = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 400.8, "low": 399.9, "close": 399.95, "volume": 10000}, ] enrichment = _enrichment_for("AAA") aligned = simulate_orb_day( {"AAA": aaa_bars, "SPY": strong_spy, "QQQ": strong_qqq}, "2026-01-05", params, enrichment, equity=10_000.0, ) joint_weak = simulate_orb_day( {"AAA": aaa_bars, "SPY": weak_spy, "QQQ": weak_qqq}, "2026-01-05", params, enrichment, equity=10_000.0, ) assert aligned.market_orb_quality_joint_weak_active is False assert joint_weak.market_orb_quality_joint_weak_active is True assert joint_weak.market_orb_quality_scaler == pytest.approx(0.5) assert joint_weak.capital_deployed < aligned.capital_deployed def test_market_orb_quality_joint_weak_guard_can_cap_total_trades() -> None: params = ORBStrategyParams( engine_family="gainers_leader", orb_minutes=5, sim_bar_minutes=5, order_timeout_minutes=20, atr_stop_multiplier=1.0, trailing_at_r=99.0, breakeven_at_r=99.0, slippage_bps=0.0, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.0, min_premarket_dollar_vol=0.0, max_gap_pct=None, max_candidates=20, min_candidates_to_trade=1, risk_per_trade_pct=0.1, max_position_pct=1.0, daily_max_loss_pct=1.0, max_stops_per_day=99, daily_budget_reset=True, allow_doji_breakout=True, market_orb_quality_ticker="SPY", market_orb_quality_secondary_ticker="QQQ", market_orb_quality_joint_weak_primary_below=0.2, market_orb_quality_joint_weak_secondary_below=0.2, market_orb_quality_joint_weak_max_trades=1, ) bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 2000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.4, "low": 100.7, "close": 101.2, "volume": 2000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.2, "high": 101.6, "low": 101.1, "close": 101.5, "volume": 2000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 101.5, "high": 101.8, "low": 101.4, "close": 101.7, "volume": 2000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 101.7, "high": 101.9, "low": 101.6, "close": 101.8, "volume": 2000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.8, "high": 102.5, "low": 101.7, "close": 102.2, "volume": 2000}, ] strong_spy = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 500.8, "volume": 10000}, ] strong_qqq = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 401.0, "low": 399.8, "close": 400.9, "volume": 10000}, ] weak_spy = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 499.6, "volume": 10000}, ] weak_qqq = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 400.8, "low": 399.9, "close": 399.95, "volume": 10000}, ] enrichment = _enrichment_for("AAA", "BBB") aligned = simulate_orb_day( {"AAA": bars, "BBB": bars, "SPY": strong_spy, "QQQ": strong_qqq}, "2026-01-05", params, enrichment, equity=10_000.0, ) joint_weak = simulate_orb_day( {"AAA": bars, "BBB": bars, "SPY": weak_spy, "QQQ": weak_qqq}, "2026-01-05", params, enrichment, equity=10_000.0, ) assert aligned.market_orb_quality_joint_weak_max_trades_active is False assert len(aligned.trades) == 2 assert joint_weak.market_orb_quality_joint_weak_active is True assert joint_weak.market_orb_quality_joint_weak_max_trades_active is True assert len(joint_weak.trades) == 1 def test_market_orb_quality_joint_weak_guard_can_target_mild_weak_band() -> None: params = ORBStrategyParams( engine_family="gainers_leader", orb_minutes=5, sim_bar_minutes=5, order_timeout_minutes=20, atr_stop_multiplier=1.0, trailing_at_r=99.0, breakeven_at_r=99.0, slippage_bps=0.0, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.0, min_premarket_dollar_vol=0.0, max_gap_pct=None, max_candidates=20, min_candidates_to_trade=1, risk_per_trade_pct=0.1, max_position_pct=1.0, daily_max_loss_pct=1.0, max_stops_per_day=99, daily_budget_reset=True, allow_doji_breakout=True, market_orb_quality_ticker="SPY", market_orb_quality_secondary_ticker="QQQ", market_orb_quality_joint_weak_primary_above=0.05, market_orb_quality_joint_weak_primary_below=0.15, market_orb_quality_joint_weak_secondary_above=0.10, market_orb_quality_joint_weak_secondary_below=0.15, market_orb_quality_joint_weak_max_trades=1, ) bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 2000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.4, "low": 100.7, "close": 101.2, "volume": 2000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.2, "high": 101.6, "low": 101.1, "close": 101.5, "volume": 2000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 101.5, "high": 101.8, "low": 101.4, "close": 101.7, "volume": 2000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 101.7, "high": 101.9, "low": 101.6, "close": 101.8, "volume": 2000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 101.8, "high": 102.5, "low": 101.7, "close": 102.2, "volume": 2000}, ] mild_weak_spy = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 500.5, "low": 499.5, "close": 499.62, "volume": 10000}, ] mild_weak_qqq = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 400.4, "low": 399.8, "close": 399.88, "volume": 10000}, ] panic_spy = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 500.5, "low": 499.5, "close": 499.50, "volume": 10000}, ] panic_qqq = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 400.4, "low": 399.8, "close": 399.82, "volume": 10000}, ] enrichment = _enrichment_for("AAA", "BBB") mild = simulate_orb_day( {"AAA": bars, "BBB": bars, "SPY": mild_weak_spy, "QQQ": mild_weak_qqq}, "2026-01-05", params, enrichment, equity=10_000.0, ) panic = simulate_orb_day( {"AAA": bars, "BBB": bars, "SPY": panic_spy, "QQQ": panic_qqq}, "2026-01-05", params, enrichment, equity=10_000.0, ) assert mild.market_orb_quality_joint_weak_active is True assert mild.market_orb_quality_joint_weak_max_trades_active is True assert len(mild.trades) == 1 assert panic.market_orb_quality_joint_weak_active is False assert panic.market_orb_quality_joint_weak_max_trades_active is False assert len(panic.trades) == 2 def test_market_orb_quality_joint_panic_guard_can_require_confirmation() -> None: params = ORBStrategyParams( engine_family="gainers_leader", orb_minutes=5, sim_bar_minutes=5, order_timeout_minutes=20, atr_stop_multiplier=1.0, trailing_at_r=99.0, breakeven_at_r=99.0, slippage_bps=0.0, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.0, min_premarket_dollar_vol=0.0, max_gap_pct=None, max_candidates=20, min_candidates_to_trade=1, risk_per_trade_pct=0.1, max_position_pct=1.0, daily_max_loss_pct=1.0, max_stops_per_day=99, daily_budget_reset=True, allow_doji_breakout=True, market_orb_quality_ticker="SPY", market_orb_quality_secondary_ticker="QQQ", market_orb_quality_joint_panic_primary_below=0.1, market_orb_quality_joint_panic_secondary_below=0.1, market_orb_quality_joint_panic_require_confirmation=True, ) aaa_bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.5, "close": 100.8, "volume": 2000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.8, "high": 101.2, "low": 100.7, "close": 101.1, "volume": 2000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.1, "high": 101.2, "low": 100.7, "close": 100.9, "volume": 2000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 100.9, "high": 101.0, "low": 100.8, "close": 100.95, "volume": 2000}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 100.95, "high": 101.0, "low": 100.85, "close": 100.9, "volume": 2000}, ] calm_spy = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.5, "close": 500.8, "volume": 10000}, ] calm_qqq = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 401.0, "low": 399.8, "close": 400.9, "volume": 10000}, ] panic_spy = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 500.0, "high": 501.0, "low": 499.0, "close": 499.05, "volume": 10000}, ] panic_qqq = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 400.0, "high": 401.0, "low": 399.0, "close": 399.1, "volume": 10000}, ] enrichment = _enrichment_for("AAA") calm = simulate_orb_day( {"AAA": aaa_bars, "SPY": calm_spy, "QQQ": calm_qqq}, "2026-01-05", params, enrichment, equity=10_000.0, ) panic = simulate_orb_day( {"AAA": aaa_bars, "SPY": panic_spy, "QQQ": panic_qqq}, "2026-01-05", params, enrichment, equity=10_000.0, ) assert calm.market_orb_quality_joint_panic_active is False assert calm.conditional_confirmation_active is False assert len(calm.trades) == 1 assert panic.market_orb_quality_joint_panic_active is True assert panic.conditional_confirmation_active is True assert panic.trades == [] def test_quality_breakout_min_body_ratio_filters_weak_candle() -> None: params = ORBStrategyParams( engine_family="quality_breakout", min_body_ratio=0.3, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, max_candidates=10, min_candidates_to_trade=1, ) bars_by_ticker = { "STRONG": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.8, "volume": 2000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.8, "high": 102.2, "low": 101.7, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, ], "WEAK": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 100.2, "volume": 2000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.2, "high": 100.5, "low": 100.1, "close": 100.4, "volume": 2000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 100.4, "high": 100.5, "low": 100.3, "close": 100.4, "volume": 2000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 100.4, "high": 100.5, "low": 100.3, "close": 100.4, "volume": 2000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 100.4, "high": 100.5, "low": 100.3, "close": 100.4, "volume": 2000}, ], } candidates = compute_orb_candidates( bars_by_ticker, "2026-01-05", params, { **_enrichment_for("STRONG", "WEAK"), }, ) assert [cand["ticker"] for cand in candidates] == ["STRONG"] def test_compression_breakout_uses_entropy_weight_in_ranking() -> None: params = ORBStrategyParams( engine_family="compression_breakout", weight_rvol=0.0, weight_gap=0.0, weight_dollar_vol=0.0, weight_entropy=-0.15, weight_atr_ratio=0.0, weight_gap_zscore=0.0, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, max_candidates=10, min_candidates_to_trade=1, ) bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.8, "volume": 2000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.8, "high": 102.2, "low": 101.7, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, ] enrichment = _enrichment_for("LOW_ENT", "HIGH_ENT") enrichment["LOW_ENT"]["2026-01-05"]["entropy_20d"] = 0.1 enrichment["HIGH_ENT"]["2026-01-05"]["entropy_20d"] = 0.9 candidates = compute_orb_candidates( {"LOW_ENT": bars, "HIGH_ENT": bars}, "2026-01-05", params, enrichment, ) assert [cand["ticker"] for cand in candidates] == ["LOW_ENT", "HIGH_ENT"] def test_candidates_can_rank_on_premarket_dollar_volume() -> None: params = ORBStrategyParams( engine_family="compression_breakout", weight_rvol=0.0, weight_gap=0.0, weight_dollar_vol=0.0, weight_premarket_dollar_vol=1.0, weight_entropy=0.0, weight_atr_ratio=0.0, weight_gap_zscore=0.0, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, max_candidates=10, min_candidates_to_trade=1, ) market_bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.8, "volume": 2000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.8, "high": 102.2, "low": 101.7, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, ] bars_by_ticker = { "HIGH_PM": [ {"timestamp": "2026-01-05T08:00:00-05:00", "open": 100.0, "high": 100.2, "low": 99.9, "close": 100.0, "volume": 10_000}, *market_bars, ], "LOW_PM": [ {"timestamp": "2026-01-05T08:00:00-05:00", "open": 100.0, "high": 100.2, "low": 99.9, "close": 100.0, "volume": 1_000}, *market_bars, ], } candidates = compute_orb_candidates( bars_by_ticker, "2026-01-05", params, _enrichment_for("HIGH_PM", "LOW_PM"), ) assert [cand["ticker"] for cand in candidates] == ["HIGH_PM", "LOW_PM"] def test_stocks_in_play_can_gate_on_attention_and_sector_relative_strength() -> None: params = ORBStrategyParams( engine_family="stocks_in_play_dual_regime", entry_direction="long_only", min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_candidates_to_trade=1, max_candidates=10, require_event_flag=True, attention_min_wiki_spike_10d=2.0, attention_min_article_count_3d=3, min_close_location=0.6, min_sector_relative_strength=0.01, require_vwap_confirmation=False, ) strong_bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 103.0, "low": 99.8, "close": 102.8, "volume": 2000, "vwap": 101.5}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 102.8, "high": 103.1, "low": 102.6, "close": 103.0, "volume": 2000, "vwap": 102.9}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 103.0, "high": 103.2, "low": 102.9, "close": 103.1, "volume": 2000, "vwap": 103.0}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 103.1, "high": 103.2, "low": 103.0, "close": 103.1, "volume": 2000, "vwap": 103.1}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 103.1, "high": 103.2, "low": 103.0, "close": 103.1, "volume": 2000, "vwap": 103.1}, ] weak_same_sector = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.2, "low": 99.8, "close": 100.7, "volume": 2000, "vwap": 100.5}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.7, "high": 100.9, "low": 100.6, "close": 100.8, "volume": 2000, "vwap": 100.8}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 100.8, "high": 100.9, "low": 100.7, "close": 100.8, "volume": 2000, "vwap": 100.8}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 100.8, "high": 100.9, "low": 100.7, "close": 100.8, "volume": 2000, "vwap": 100.8}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 100.8, "high": 100.9, "low": 100.7, "close": 100.8, "volume": 2000, "vwap": 100.8}, ] no_attention = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.5, "low": 99.8, "close": 102.0, "volume": 2000, "vwap": 101.5}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 102.0, "high": 102.2, "low": 101.9, "close": 102.1, "volume": 2000, "vwap": 102.0}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.1, "high": 102.2, "low": 102.0, "close": 102.1, "volume": 2000, "vwap": 102.1}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.1, "high": 102.2, "low": 102.0, "close": 102.1, "volume": 2000, "vwap": 102.1}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.1, "high": 102.2, "low": 102.0, "close": 102.1, "volume": 2000, "vwap": 102.1}, ] enrichment = _enrichment_for("STRONG", "WEAK", "NO_ATTN") enrichment["STRONG"]["2026-01-05"].update({ "event_flag": True, "event_score": 1.0, "attention_wiki_spike_10d": 3.0, "attention_article_count_3d": 5, }) enrichment["WEAK"]["2026-01-05"].update({ "event_flag": True, "event_score": 1.0, "attention_wiki_spike_10d": 2.5, "attention_article_count_3d": 4, }) enrichment["NO_ATTN"]["2026-01-05"].update({ "event_flag": True, "event_score": 1.0, "attention_wiki_spike_10d": 1.2, "attention_article_count_3d": 0, }) candidates = compute_orb_candidates( {"STRONG": strong_bars, "WEAK": weak_same_sector, "NO_ATTN": no_attention}, "2026-01-05", params, enrichment, ticker_sectors={"STRONG": "TECH", "WEAK": "TECH", "NO_ATTN": "HEALTH"}, ) assert [cand["ticker"] for cand in candidates] == ["STRONG"] def test_gainers_leader_attention_news_weight_affects_ranking() -> None: params = ORBStrategyParams( engine_family="gainers_leader", entry_direction="long_only", min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_candidates_to_trade=1, max_candidates=2, weight_rvol=0.0, weight_gap=0.0, weight_dollar_vol=0.0, weight_premarket_dollar_vol=0.0, weight_attention_news=1.0, ) bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.5, "volume": 2000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.5, "high": 102.1, "low": 101.4, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.2, "low": 101.9, "close": 102.1, "volume": 2000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.1, "high": 102.3, "low": 102.0, "close": 102.2, "volume": 2000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.2, "high": 102.4, "low": 102.1, "close": 102.3, "volume": 2000}, ] enrichment = _enrichment_for("LOW_NEWS", "HIGH_NEWS") enrichment["LOW_NEWS"]["2026-01-05"]["attention_article_count_3d"] = 0 enrichment["HIGH_NEWS"]["2026-01-05"]["attention_article_count_3d"] = 8 candidates = compute_orb_candidates( {"LOW_NEWS": bars, "HIGH_NEWS": bars}, "2026-01-05", params, enrichment, ) assert [cand["ticker"] for cand in candidates] == ["HIGH_NEWS", "LOW_NEWS"] def test_gainers_leader_ownership_initial_weight_affects_ranking() -> None: params = ORBStrategyParams( engine_family="gainers_leader", entry_direction="long_only", min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_candidates_to_trade=1, max_candidates=2, weight_rvol=0.0, weight_gap=0.0, weight_dollar_vol=0.0, weight_premarket_dollar_vol=0.0, ownership_13dg_lookback_days=180, weight_ownership_initial_13dg=1.0, ) bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.5, "volume": 2000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.5, "high": 102.1, "low": 101.4, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.2, "low": 101.9, "close": 102.1, "volume": 2000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.1, "high": 102.3, "low": 102.0, "close": 102.2, "volume": 2000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.2, "high": 102.4, "low": 102.1, "close": 102.3, "volume": 2000}, ] enrichment = _enrichment_for("NO_OWNER", "OWNER") enrichment["OWNER"]["2026-01-05"].update( { "ownership_13dg_flag": True, "ownership_13dg_initial_flag": True, "ownership_13dg_days_since": 20, "ownership_13dg_score": 1.0, "ownership_13dg_initial_score": 1.0, } ) candidates = compute_orb_candidates( {"NO_OWNER": bars, "OWNER": bars}, "2026-01-05", params, enrichment, ) assert [cand["ticker"] for cand in candidates] == ["OWNER", "NO_OWNER"] def test_ownership_initial_size_scale_requires_clean_initial_owner_setup() -> None: params = ORBStrategyParams( ownership_initial_size_scale=1.2, ownership_initial_min_score_rank_pct=0.8, ownership_initial_allowed_trigger_types=["orb"], ) cand = {"ownership_13dg_initial_flag": True} assert _orb_ownership_initial_size_scale(params, cand, 0.9, "orb", "long", (1.0,)) == 1.2 assert _orb_ownership_initial_size_scale(params, cand, 0.7, "orb", "long", (1.0,)) == 1.0 assert _orb_ownership_initial_size_scale(params, cand, 0.9, "vwap_reclaim", "long", (1.0,)) == 1.0 assert _orb_ownership_initial_size_scale(params, cand, 0.9, "orb", "long", (0.5,)) == 1.0 def test_form4_size_scale_requires_cluster_or_csuite_setup() -> None: params = ORBStrategyParams( form4_size_scale=1.2, form4_min_total_value=100_000.0, form4_min_owner_count=2, form4_min_c_suite_count=1, form4_require_cluster_or_csuite=True, form4_allowed_trigger_types=["orb"], ) cluster = { "form4_flag": True, "form4_total_value": 150_000.0, "form4_owner_count": 2, "form4_c_suite_count": 0, } csuite = { "form4_flag": True, "form4_total_value": 150_000.0, "form4_owner_count": 1, "form4_c_suite_count": 1, } weak = { "form4_flag": True, "form4_total_value": 150_000.0, "form4_owner_count": 1, "form4_c_suite_count": 0, } assert _orb_form4_size_scale(params, cluster, "orb", "long", (1.0,)) == 1.2 assert _orb_form4_size_scale(params, csuite, "orb", "long", (1.0,)) == 1.2 assert _orb_form4_size_scale(params, weak, "orb", "long", (1.0,)) == 1.0 assert _orb_form4_size_scale(params, cluster, "vwap_reclaim", "long", (1.0,)) == 1.0 assert _orb_form4_size_scale(params, cluster, "orb", "long", (0.5,)) == 1.0 def test_stocks_in_play_can_allow_red_to_green_reclaim() -> None: params = ORBStrategyParams( engine_family="stocks_in_play_dual_regime", entry_direction="long_only", allow_doji_breakout=True, allow_red_to_green_breakout=True, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_candidates_to_trade=1, max_candidates=10, require_event_flag=True, ) bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 101.0, "low": 99.8, "close": 99.9, "volume": 2000, "vwap": 100.0}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 99.9, "high": 100.3, "low": 99.8, "close": 100.2, "volume": 2000, "vwap": 100.1}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 100.2, "high": 100.4, "low": 100.1, "close": 100.3, "volume": 2000, "vwap": 100.3}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 100.3, "high": 100.4, "low": 100.2, "close": 100.3, "volume": 2000, "vwap": 100.3}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 100.3, "high": 100.4, "low": 100.2, "close": 100.3, "volume": 2000, "vwap": 100.3}, ] enrichment = _enrichment_for("R2G") enrichment["R2G"]["2026-01-05"].update({"event_flag": True, "event_score": 1.0}) candidates = compute_orb_candidates({"R2G": bars}, "2026-01-05", params, enrichment) assert [cand["ticker"] for cand in candidates] == ["R2G"] def test_candidates_can_filter_on_min_abs_gap_pct() -> None: params = ORBStrategyParams( engine_family="compression_breakout", min_abs_gap_pct=0.02, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, max_candidates=10, min_candidates_to_trade=1, ) bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.8, "volume": 2000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.8, "high": 102.2, "low": 101.7, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, ] enrichment = _enrichment_for("BIG_GAP", "SMALL_GAP") enrichment["BIG_GAP"]["2026-01-05"]["prev_close"] = 95.0 enrichment["SMALL_GAP"]["2026-01-05"]["prev_close"] = 99.5 candidates = compute_orb_candidates( {"BIG_GAP": bars, "SMALL_GAP": bars}, "2026-01-05", params, enrichment, ) assert [cand["ticker"] for cand in candidates] == ["BIG_GAP"] def test_candidates_can_cap_names_per_sector_after_ranking() -> None: params = ORBStrategyParams( weight_rvol=1.0, weight_gap=0.0, weight_dollar_vol=0.0, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, max_candidates=3, max_candidates_per_sector=1, min_candidates_to_trade=1, ) bars_by_ticker = { "TECH1": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.8, "volume": 4000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.8, "high": 102.2, "low": 101.7, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, ], "TECH2": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.8, "volume": 3000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.8, "high": 102.2, "low": 101.7, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, ], "HEALTH1": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.8, "volume": 2000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.8, "high": 102.2, "low": 101.7, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, ], } candidates = compute_orb_candidates( bars_by_ticker, "2026-01-05", params, _enrichment_for("TECH1", "TECH2", "HEALTH1"), ticker_sectors={"TECH1": "Technology", "TECH2": "Technology", "HEALTH1": "Healthcare"}, ) assert [cand["ticker"] for cand in candidates] == ["TECH1", "HEALTH1"] def test_sector_confirmation_can_promote_same_sector_cluster() -> None: params = ORBStrategyParams( weight_rvol=1.0, weight_gap=0.0, weight_dollar_vol=0.0, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, max_candidates=1, min_candidates_to_trade=1, sector_confirmation_enabled=True, sector_confirmation_min_members=2, sector_confirmation_score_weight=1.0, ) def bars(volume: int) -> list[dict]: return [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.8, "volume": volume}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.8, "high": 102.2, "low": 101.7, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, ] candidates = compute_orb_candidates( { "LONE": bars(5000), "TECH1": bars(4600), "TECH2": bars(4500), }, "2026-01-05", params, _enrichment_for("LONE", "TECH1", "TECH2"), ticker_sectors={ "LONE": "Healthcare", "TECH1": "Technology", "TECH2": "Technology", }, ) assert [cand["ticker"] for cand in candidates] == ["TECH1"] assert candidates[0]["sector_confirmation_active"] is True assert candidates[0]["sector_confirmation_member_count"] == 2 def test_sector_confirmation_size_scale_supports_liquidity_tiers() -> None: params = ORBStrategyParams( sector_confirmation_enabled=True, sector_confirmation_size_scale=1.10, sector_confirmation_liquid_min_premarket_dollar_vol=20_000_000, sector_confirmation_liquid_size_scale=1.20, sector_confirmation_illiquid_size_scale=1.0, sector_confirmation_unconfirmed_size_scale=0.95, ) liquid = { "sector_confirmation_active": True, "premarket_dollar_vol": 25_000_000, } thin = { "sector_confirmation_active": True, "premarket_dollar_vol": 5_000_000, } unconfirmed = { "sector_confirmation_active": False, "premarket_dollar_vol": 25_000_000, } assert _orb_sector_confirmation_size_scale(params, liquid) == 1.20 assert _orb_sector_confirmation_size_scale(params, thin) == 1.0 assert _orb_sector_confirmation_size_scale(params, unconfirmed) == 0.95 def test_opening_burst_liquid_size_scale_requires_time_liquidity_and_clean_risk() -> None: params = ORBStrategyParams( opening_burst_liquid_size_scale=1.4, opening_burst_liquid_max_entry_minutes_after_open=5, opening_burst_liquid_min_premarket_dollar_vol=100_000_000, opening_burst_liquid_min_gap_pct=0.0, opening_burst_liquid_min_candidate_score=0.65, opening_burst_liquid_allowed_trigger_types=["orb"], ) cand = {"premarket_dollar_vol": 150_000_000, "gap_pct": 0.02, "score": 0.70} entry_ts = dt.datetime( 2026, 1, 5, 9, 35, tzinfo=dt.timezone(dt.timedelta(hours=-5)), ) late_ts = entry_ts + dt.timedelta(minutes=5) assert ( _orb_opening_burst_liquid_size_scale( params, {"premarket_dollar_vol": 150_000_000, "gap_pct": 0.02, "score": 0.60}, date_str="2026-01-05", entry_ts=entry_ts, trigger_type="orb", score_rank_pct=0.5, direction_str="long", risk_size_scales=(1.0, 1.0), ) == 1.0 ) assert ( _orb_opening_burst_liquid_size_scale( params, cand, date_str="2026-01-05", entry_ts=entry_ts, trigger_type="orb", score_rank_pct=0.5, direction_str="long", risk_size_scales=(1.0, 1.0), ) == 1.4 ) assert ( _orb_opening_burst_liquid_size_scale( params, {"premarket_dollar_vol": 150_000_000, "gap_pct": -0.01, "score": 0.70}, date_str="2026-01-05", entry_ts=entry_ts, trigger_type="orb", score_rank_pct=0.5, direction_str="long", risk_size_scales=(1.0, 1.0), ) == 1.0 ) assert ( _orb_opening_burst_liquid_size_scale( params, cand, date_str="2026-01-05", entry_ts=late_ts, trigger_type="orb", score_rank_pct=0.5, direction_str="long", risk_size_scales=(1.0, 1.0), ) == 1.0 ) assert ( _orb_opening_burst_liquid_size_scale( params, cand, date_str="2026-01-05", entry_ts=entry_ts, trigger_type="soft_day_vwap_reclaim", score_rank_pct=0.5, direction_str="long", risk_size_scales=(1.0, 1.0), ) == 1.0 ) assert ( _orb_opening_burst_liquid_size_scale( params, cand, date_str="2026-01-05", entry_ts=entry_ts, trigger_type="orb", score_rank_pct=0.5, direction_str="long", risk_size_scales=(0.5, 1.0), ) == 1.0 ) def test_unboosted_primary_fragility_scales_only_unboosted_fragile_orb() -> None: params = ORBStrategyParams( unboosted_primary_fragility_size_scale=0.25, unboosted_primary_fragility_allowed_trigger_types=["orb"], unboosted_primary_fragility_crowded_min_gap_pct=0.02, unboosted_primary_fragility_crowded_min_premarket_dollar_vol=50_000_000, unboosted_primary_fragility_crowded_max_close_location=0.85, unboosted_primary_fragility_weak_max_rvol=5.0, unboosted_primary_fragility_weak_max_ret_5d=0.05, ) crowded = { "gap_pct": 0.03, "premarket_dollar_vol": 75_000_000, "close_location": 0.70, "rvol": 8.0, "ret_5d": 0.20, } weak_attention = { "gap_pct": -0.03, "premarket_dollar_vol": 10_000_000, "close_location": 0.95, "rvol": 4.5, "ret_5d": -0.02, } clean = { "gap_pct": 0.03, "premarket_dollar_vol": 75_000_000, "close_location": 0.95, "rvol": 8.0, "ret_5d": 0.20, } assert ( _orb_unboosted_primary_fragility_size_scale( params, crowded, {}, trigger_type="orb", direction_str="long", high_conviction_size_scales=(1.0, 1.0), ) == 0.25 ) assert ( _orb_unboosted_primary_fragility_size_scale( params, weak_attention, {}, trigger_type="orb", direction_str="long", high_conviction_size_scales=(1.0, 1.0), ) == 0.25 ) assert ( _orb_unboosted_primary_fragility_size_scale( params, crowded, {}, trigger_type="orb", direction_str="long", high_conviction_size_scales=(1.5, 1.0), ) == 1.0 ) assert ( _orb_unboosted_primary_fragility_size_scale( params, crowded, {}, trigger_type="soft_day_vwap_reclaim", direction_str="long", high_conviction_size_scales=(1.0, 1.0), ) == 1.0 ) assert ( _orb_unboosted_primary_fragility_size_scale( params, clean, {}, trigger_type="orb", direction_str="long", high_conviction_size_scales=(1.0, 1.0), ) == 1.0 ) def test_soft_day_sector_confirmation_override_requires_sector_and_reason() -> None: params = ORBStrategyParams( soft_day_sector_confirmation_override_enabled=True, soft_day_sector_confirmation_override_min_score_pct=0.70, soft_day_sector_confirmation_override_min_premarket_dollar_vol=20_000_000, soft_day_sector_confirmation_override_allowed_reason_parts=["breadth"], soft_day_sector_confirmation_override_allowed_trigger_types=["orb"], ) cand = { "sector_confirmation_active": True, "premarket_dollar_vol": 25_000_000, } assert _orb_soft_day_sector_confirmation_override_allows( params, "market_regime+breadth", cand, score_rank_pct=0.75, trigger_type="orb", ) assert not _orb_soft_day_sector_confirmation_override_allows( params, "market_regime", cand, score_rank_pct=0.75, trigger_type="orb", ) assert not _orb_soft_day_sector_confirmation_override_allows( params, "market_regime+breadth", {**cand, "sector_confirmation_active": False}, score_rank_pct=0.75, trigger_type="orb", ) assert not _orb_soft_day_sector_confirmation_override_allows( params, "market_regime+breadth", cand, score_rank_pct=0.65, trigger_type="orb", ) assert not _orb_soft_day_sector_confirmation_override_allows( params, "market_regime+breadth", cand, score_rank_pct=0.75, trigger_type="soft_day_vwap_reclaim", ) def test_soft_day_sector_confirmation_override_base_sizing_floor_is_active_only() -> None: params = ORBStrategyParams( soft_day_sector_confirmation_override_min_day_size_scale=0.08, ) assert _orb_soft_day_sector_confirmation_override_base_sizing( params, active=False, sizing_cap=10_000, adjusted_sizing=0.0, ) == (0.0, None) assert _orb_soft_day_sector_confirmation_override_base_sizing( params, active=True, sizing_cap=10_000, adjusted_sizing=0.0, ) == (800.0, 0.08) assert _orb_soft_day_sector_confirmation_override_base_sizing( params, active=True, sizing_cap=10_000, adjusted_sizing=1_200.0, ) == (1_200.0, 0.08) def test_candidates_can_prune_weak_tail_with_relative_quality_floor() -> None: params = ORBStrategyParams( weight_rvol=0.0, weight_gap=0.0, weight_dollar_vol=1.0, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, max_candidates=3, min_candidates_to_trade=1, basket_quality_relative_floor=0.6, ) bars_by_ticker = { "LEADER": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.8, "volume": 4000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.8, "high": 102.2, "low": 101.7, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, ], "MID": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.8, "volume": 3000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.8, "high": 102.2, "low": 101.7, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, ], "TAIL": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.8, "volume": 2000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.8, "high": 102.2, "low": 101.7, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, ], } candidates = compute_orb_candidates( bars_by_ticker, "2026-01-05", params, _enrichment_for("LEADER", "MID", "TAIL"), ) assert [cand["ticker"] for cand in candidates] == ["LEADER"] def test_quality_floor_respects_min_count() -> None: params = ORBStrategyParams( weight_rvol=0.0, weight_gap=0.0, weight_dollar_vol=1.0, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, max_candidates=3, min_candidates_to_trade=1, basket_quality_relative_floor=0.95, basket_quality_min_count=2, ) bars_by_ticker = { "LEADER": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.8, "volume": 4000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.8, "high": 102.2, "low": 101.7, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, ], "MID": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.8, "volume": 3000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.8, "high": 102.2, "low": 101.7, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, ], "TAIL": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.8, "volume": 2000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.8, "high": 102.2, "low": 101.7, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, ], } candidates = compute_orb_candidates( bars_by_ticker, "2026-01-05", params, _enrichment_for("LEADER", "MID", "TAIL"), ) assert [cand["ticker"] for cand in candidates] == ["LEADER", "MID"] def test_gainers_leader_prefers_premarket_attention_and_abs_gap() -> None: params = ORBStrategyParams( engine_family="gainers_leader", weight_rvol=0.50, weight_gap=0.15, weight_dollar_vol=0.10, weight_premarket_dollar_vol=0.25, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.02, max_gap_pct=None, max_candidates=10, min_candidates_to_trade=1, ) market_bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 100.0, "high": 102.0, "low": 99.8, "close": 101.8, "volume": 2000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 101.8, "high": 102.2, "low": 101.7, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 102.0, "high": 102.1, "low": 101.9, "close": 102.0, "volume": 2000}, ] bars_by_ticker = { "LEADER": [ {"timestamp": "2026-01-05T08:00:00-05:00", "open": 104.0, "high": 104.5, "low": 103.8, "close": 104.2, "volume": 20_000}, *market_bars, ], "LAGGARD": [ {"timestamp": "2026-01-05T08:00:00-05:00", "open": 101.0, "high": 101.2, "low": 100.8, "close": 101.1, "volume": 1_000}, *market_bars, ], } enrichment = _enrichment_for("LEADER", "LAGGARD") enrichment["LEADER"]["2026-01-05"]["prev_close"] = 95.0 enrichment["LAGGARD"]["2026-01-05"]["prev_close"] = 97.0 candidates = compute_orb_candidates( bars_by_ticker, "2026-01-05", params, enrichment, ) assert [cand["ticker"] for cand in candidates] == ["LEADER", "LAGGARD"] def test_gainers_leader_can_allow_doji_followthrough_breakouts() -> None: params = ORBStrategyParams( engine_family="gainers_leader", entry_direction="long_only", allow_doji_breakout=True, weight_rvol=0.40, weight_gap=0.20, weight_dollar_vol=0.05, weight_premarket_dollar_vol=0.35, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.02, min_premarket_dollar_vol=100_000.0, max_gap_pct=None, max_candidates=5, min_candidates_to_trade=1, ) bars_by_ticker = { "DOJI": [ {"timestamp": "2026-01-05T08:15:00-05:00", "open": 103.0, "high": 103.5, "low": 102.8, "close": 103.2, "volume": 15_000}, {"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 106.0, "low": 104.0, "close": 105.02, "volume": 8_000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 105.2, "high": 106.5, "low": 105.0, "close": 106.2, "volume": 6_000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 106.2, "high": 106.4, "low": 105.9, "close": 106.1, "volume": 3_000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 106.1, "high": 106.6, "low": 106.0, "close": 106.4, "volume": 2_000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 106.4, "high": 106.8, "low": 106.2, "close": 106.7, "volume": 2_000}, ], } enrichment = _enrichment_for("DOJI") enrichment["DOJI"]["2026-01-05"]["prev_close"] = 100.0 candidates = compute_orb_candidates( bars_by_ticker, "2026-01-05", params, enrichment, ) assert [cand["ticker"] for cand in candidates] == ["DOJI"] def test_gainers_leader_can_allow_red_to_green_followthrough_breakouts() -> None: params = ORBStrategyParams( engine_family="gainers_leader", entry_direction="long_only", allow_red_to_green_breakout=True, weight_rvol=0.40, weight_gap=0.20, weight_dollar_vol=0.05, weight_premarket_dollar_vol=0.35, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.02, min_premarket_dollar_vol=100_000.0, max_gap_pct=None, max_candidates=5, min_candidates_to_trade=1, ) bars_by_ticker = { "RED_GREEN": [ {"timestamp": "2026-01-05T08:15:00-05:00", "open": 103.0, "high": 103.5, "low": 102.8, "close": 103.2, "volume": 15_000}, {"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 105.4, "low": 103.8, "close": 104.3, "volume": 8_000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 104.4, "high": 105.8, "low": 104.2, "close": 105.6, "volume": 6_000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 105.6, "high": 105.9, "low": 105.4, "close": 105.8, "volume": 3_000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 105.8, "high": 106.0, "low": 105.6, "close": 105.9, "volume": 2_000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 105.9, "high": 106.2, "low": 105.7, "close": 106.1, "volume": 2_000}, ], } enrichment = _enrichment_for("RED_GREEN") enrichment["RED_GREEN"]["2026-01-05"]["prev_close"] = 100.0 candidates = compute_orb_candidates( bars_by_ticker, "2026-01-05", params, enrichment, ) assert [cand["ticker"] for cand in candidates] == ["RED_GREEN"] def test_gainers_leader_can_conditionally_reject_high_gap_zscore_without_prior_trend_support() -> None: params = ORBStrategyParams( engine_family="gainers_leader", entry_direction="long_only", allow_red_to_green_breakout=True, conditional_gap_zscore_reject_above=2.0, conditional_gap_zscore_reject_ret_5d_below=0.0, weight_rvol=0.40, weight_gap=0.20, weight_dollar_vol=0.05, weight_premarket_dollar_vol=0.35, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.02, min_premarket_dollar_vol=100_000.0, max_gap_pct=None, max_candidates=5, min_candidates_to_trade=1, ) bars = [ {"timestamp": "2026-01-05T08:15:00-05:00", "open": 103.0, "high": 103.5, "low": 102.8, "close": 103.2, "volume": 15_000}, {"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 106.0, "low": 104.4, "close": 105.8, "volume": 3_000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 105.8, "high": 106.2, "low": 105.6, "close": 106.0, "volume": 2_000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 106.0, "high": 106.2, "low": 105.8, "close": 106.1, "volume": 2_000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 106.1, "high": 106.3, "low": 106.0, "close": 106.2, "volume": 2_000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 106.2, "high": 106.4, "low": 106.1, "close": 106.3, "volume": 2_000}, ] enrichment = _enrichment_for("BAD_HIGH_NEG", "GOOD_HIGH_POS", "GOOD_LOW_NEG") for ticker in ("BAD_HIGH_NEG", "GOOD_HIGH_POS", "GOOD_LOW_NEG"): enrichment[ticker]["2026-01-05"]["prev_close"] = 100.0 enrichment["BAD_HIGH_NEG"]["2026-01-05"].update({"gap_zscore_20d": 2.5, "ret_5d": -0.03}) enrichment["GOOD_HIGH_POS"]["2026-01-05"].update({"gap_zscore_20d": 2.5, "ret_5d": 0.08}) enrichment["GOOD_LOW_NEG"]["2026-01-05"].update({"gap_zscore_20d": 1.2, "ret_5d": -0.03}) candidates = compute_orb_candidates( {"BAD_HIGH_NEG": bars, "GOOD_HIGH_POS": bars, "GOOD_LOW_NEG": bars}, "2026-01-05", params, enrichment, ) assert [cand["ticker"] for cand in candidates] == ["GOOD_HIGH_POS", "GOOD_LOW_NEG"] def test_dual_regime_requires_actual_event_flag_and_filters_event_types() -> None: params = ORBStrategyParams( engine_family="stocks_in_play_dual_regime", entry_direction="candle", require_event_flag=True, allowed_event_types=["other_material_event"], min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_close_location=0.6, weight_event_catalyst=1.0, max_candidates=5, min_candidates_to_trade=1, ) bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 107.0, "low": 104.9, "close": 106.8, "volume": 5000, "vwap": 106.1}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 106.8, "high": 107.2, "low": 106.7, "close": 107.0, "volume": 3000, "vwap": 106.9}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 107.0, "high": 107.2, "low": 106.8, "close": 107.1, "volume": 2000, "vwap": 107.0}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 107.1, "high": 107.3, "low": 106.9, "close": 107.2, "volume": 2000, "vwap": 107.1}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 107.2, "high": 107.4, "low": 107.0, "close": 107.3, "volume": 2000, "vwap": 107.2}, ] enrichment = _enrichment_for("GOOD", "WRONG_TYPE", "NO_EVENT") for ticker in ("GOOD", "WRONG_TYPE", "NO_EVENT"): enrichment[ticker]["2026-01-05"]["prev_close"] = 100.0 enrichment["GOOD"]["2026-01-05"]["event_flag"] = True enrichment["GOOD"]["2026-01-05"]["event_types"] = ["other_material_event"] enrichment["GOOD"]["2026-01-05"]["event_score"] = 1.0 enrichment["WRONG_TYPE"]["2026-01-05"]["event_flag"] = True enrichment["WRONG_TYPE"]["2026-01-05"]["event_types"] = ["management_change"] enrichment["WRONG_TYPE"]["2026-01-05"]["event_score"] = 0.6 candidates = compute_orb_candidates( {"GOOD": bars, "WRONG_TYPE": bars, "NO_EVENT": bars}, "2026-01-05", params, enrichment, ) assert [cand["ticker"] for cand in candidates] == ["GOOD"] def test_dual_regime_can_take_failed_gap_up_short_below_vwap() -> None: params = ORBStrategyParams( engine_family="stocks_in_play_dual_regime", entry_direction="candle", require_event_flag=True, allow_failed_orb_short=True, require_vwap_confirmation=True, max_close_location_short=0.40, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, max_candidates=5, min_candidates_to_trade=1, ) bars_by_ticker = { "FAILED": [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 105.3, "low": 103.8, "close": 104.0, "volume": 6000, "vwap": 104.7}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 104.0, "high": 104.2, "low": 103.6, "close": 103.8, "volume": 3000, "vwap": 103.9}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 103.8, "high": 104.0, "low": 103.5, "close": 103.7, "volume": 2000, "vwap": 103.8}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 103.7, "high": 103.9, "low": 103.4, "close": 103.5, "volume": 2000, "vwap": 103.6}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 103.5, "high": 103.7, "low": 103.2, "close": 103.3, "volume": 2000, "vwap": 103.4}, ], } enrichment = _enrichment_for("FAILED") enrichment["FAILED"]["2026-01-05"]["prev_close"] = 100.0 enrichment["FAILED"]["2026-01-05"]["event_flag"] = True enrichment["FAILED"]["2026-01-05"]["event_types"] = ["other_material_event"] enrichment["FAILED"]["2026-01-05"]["event_score"] = 1.0 candidates = compute_orb_candidates( bars_by_ticker, "2026-01-05", params, enrichment, ) assert [cand["ticker"] for cand in candidates] == ["FAILED"] assert candidates[0]["direction"] == "bearish" def test_gainers_leader_can_override_min_gap_for_high_attention_small_gap_names() -> None: params = ORBStrategyParams( engine_family="gainers_leader", entry_direction="long_only", allow_red_to_green_breakout=True, min_abs_gap_pct=0.02, small_gap_attention_override_premarket_dollar_vol=1_000_000.0, small_gap_attention_override_rvol=1.5, weight_rvol=0.40, weight_gap=0.20, weight_dollar_vol=0.05, weight_premarket_dollar_vol=0.35, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_premarket_dollar_vol=100_000.0, max_gap_pct=None, max_candidates=5, min_candidates_to_trade=1, ) bars_by_ticker = { "ATTN_SMALL_GAP": [ {"timestamp": "2026-01-05T08:15:00-05:00", "open": 100.5, "high": 101.0, "low": 100.4, "close": 100.8, "volume": 20_000}, {"timestamp": "2026-01-05T09:30:00-05:00", "open": 101.0, "high": 101.1, "low": 99.8, "close": 100.4, "volume": 3_500}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.5, "high": 101.6, "low": 100.4, "close": 101.5, "volume": 3_000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.5, "high": 101.7, "low": 101.3, "close": 101.6, "volume": 2_000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 101.6, "high": 101.8, "low": 101.5, "close": 101.7, "volume": 2_000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 101.7, "high": 101.9, "low": 101.6, "close": 101.8, "volume": 2_000}, ], } enrichment = _enrichment_for("ATTN_SMALL_GAP") enrichment["ATTN_SMALL_GAP"]["2026-01-05"]["prev_close"] = 100.5 enrichment["ATTN_SMALL_GAP"]["2026-01-05"]["avg_daily_vol_14d"] = 100_000.0 candidates = compute_orb_candidates( bars_by_ticker, "2026-01-05", params, enrichment, ) assert [cand["ticker"] for cand in candidates] == ["ATTN_SMALL_GAP"] def test_gainers_leader_can_cap_small_gap_attention_override_names_per_day() -> None: params = ORBStrategyParams( engine_family="gainers_leader", entry_direction="long_only", allow_red_to_green_breakout=True, min_abs_gap_pct=0.02, small_gap_attention_override_premarket_dollar_vol=1_000_000.0, small_gap_attention_override_rvol=1.5, max_small_gap_attention_candidates=1, weight_rvol=1.0, weight_gap=0.0, weight_dollar_vol=0.0, weight_premarket_dollar_vol=0.0, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_premarket_dollar_vol=100_000.0, max_gap_pct=None, max_candidates=5, min_candidates_to_trade=1, ) bars_by_ticker = { "ATTN1": [ {"timestamp": "2026-01-05T08:15:00-05:00", "open": 100.5, "high": 101.0, "low": 100.4, "close": 100.8, "volume": 20_000}, {"timestamp": "2026-01-05T09:30:00-05:00", "open": 101.0, "high": 101.1, "low": 99.8, "close": 100.4, "volume": 4_500}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.5, "high": 101.6, "low": 100.4, "close": 101.5, "volume": 3_000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.5, "high": 101.7, "low": 101.3, "close": 101.6, "volume": 2_000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 101.6, "high": 101.8, "low": 101.5, "close": 101.7, "volume": 2_000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 101.7, "high": 101.9, "low": 101.6, "close": 101.8, "volume": 2_000}, ], "ATTN2": [ {"timestamp": "2026-01-05T08:15:00-05:00", "open": 100.5, "high": 101.0, "low": 100.4, "close": 100.8, "volume": 20_000}, {"timestamp": "2026-01-05T09:30:00-05:00", "open": 101.0, "high": 101.1, "low": 99.8, "close": 100.4, "volume": 3_500}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 100.5, "high": 101.6, "low": 100.4, "close": 101.5, "volume": 3_000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 101.5, "high": 101.7, "low": 101.3, "close": 101.6, "volume": 2_000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 101.6, "high": 101.8, "low": 101.5, "close": 101.7, "volume": 2_000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 101.7, "high": 101.9, "low": 101.6, "close": 101.8, "volume": 2_000}, ], "BIG_GAP": [ {"timestamp": "2026-01-05T08:15:00-05:00", "open": 103.0, "high": 103.5, "low": 102.8, "close": 103.2, "volume": 15_000}, {"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 106.0, "low": 104.4, "close": 105.8, "volume": 3_000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 105.8, "high": 106.2, "low": 105.6, "close": 106.0, "volume": 2_000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 106.0, "high": 106.2, "low": 105.8, "close": 106.1, "volume": 2_000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 106.1, "high": 106.3, "low": 106.0, "close": 106.2, "volume": 2_000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 106.2, "high": 106.4, "low": 106.1, "close": 106.3, "volume": 2_000}, ], } enrichment = _enrichment_for("ATTN1", "ATTN2", "BIG_GAP") enrichment["ATTN1"]["2026-01-05"]["prev_close"] = 100.5 enrichment["ATTN1"]["2026-01-05"]["avg_daily_vol_14d"] = 100_000.0 enrichment["ATTN2"]["2026-01-05"]["prev_close"] = 100.5 enrichment["ATTN2"]["2026-01-05"]["avg_daily_vol_14d"] = 100_000.0 enrichment["BIG_GAP"]["2026-01-05"]["prev_close"] = 100.0 candidates = compute_orb_candidates( bars_by_ticker, "2026-01-05", params, enrichment, ) assert [cand["ticker"] for cand in candidates] == ["BIG_GAP", "ATTN1"] def test_leader_followthrough_requires_strong_close_location_for_red_to_green() -> None: params = ORBStrategyParams( engine_family="leader_followthrough", entry_direction="long_only", allow_red_to_green_breakout=True, min_close_location=0.55, weight_rvol=0.30, weight_gap=0.05, weight_dollar_vol=0.10, weight_premarket_dollar_vol=0.35, weight_close_location=0.20, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.005, min_premarket_dollar_vol=100_000.0, max_gap_pct=None, max_candidates=5, min_candidates_to_trade=1, ) bars_by_ticker = { "STRONG_RECLAIM": [ {"timestamp": "2026-01-05T08:15:00-05:00", "open": 102.0, "high": 102.4, "low": 101.8, "close": 102.3, "volume": 18_000}, {"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 105.4, "low": 103.8, "close": 104.85, "volume": 8_000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 104.9, "high": 105.8, "low": 104.8, "close": 105.7, "volume": 6_000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 105.7, "high": 106.0, "low": 105.5, "close": 105.9, "volume": 3_000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 105.9, "high": 106.1, "low": 105.8, "close": 106.0, "volume": 2_000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 106.0, "high": 106.2, "low": 105.8, "close": 106.1, "volume": 2_000}, ], "WEAK_RECLAIM": [ {"timestamp": "2026-01-05T08:15:00-05:00", "open": 102.0, "high": 102.4, "low": 101.8, "close": 102.3, "volume": 18_000}, {"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 105.4, "low": 103.8, "close": 104.2, "volume": 8_000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 104.2, "high": 104.6, "low": 104.0, "close": 104.3, "volume": 4_000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 104.3, "high": 104.5, "low": 104.1, "close": 104.4, "volume": 2_000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 104.4, "high": 104.5, "low": 104.2, "close": 104.4, "volume": 2_000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 104.4, "high": 104.6, "low": 104.2, "close": 104.5, "volume": 2_000}, ], } enrichment = _enrichment_for("STRONG_RECLAIM", "WEAK_RECLAIM") enrichment["STRONG_RECLAIM"]["2026-01-05"]["prev_close"] = 104.0 enrichment["WEAK_RECLAIM"]["2026-01-05"]["prev_close"] = 104.0 candidates = compute_orb_candidates( bars_by_ticker, "2026-01-05", params, enrichment, ) assert [cand["ticker"] for cand in candidates] == ["STRONG_RECLAIM"] def test_leader_followthrough_ranking_rewards_close_location() -> None: params = ORBStrategyParams( engine_family="leader_followthrough", entry_direction="long_only", allow_red_to_green_breakout=True, min_close_location=0.0, weight_rvol=0.0, weight_gap=0.0, weight_dollar_vol=0.0, weight_premarket_dollar_vol=0.0, weight_close_location=1.0, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.005, min_premarket_dollar_vol=100_000.0, max_gap_pct=None, max_candidates=5, min_candidates_to_trade=1, ) bars_by_ticker = { "HIGH_CLOSE": [ {"timestamp": "2026-01-05T08:15:00-05:00", "open": 102.0, "high": 102.2, "low": 101.9, "close": 102.1, "volume": 12_000}, {"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 105.4, "low": 103.8, "close": 104.85, "volume": 8_000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 104.95, "high": 105.8, "low": 104.9, "close": 105.7, "volume": 5_000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 105.7, "high": 105.9, "low": 105.5, "close": 105.8, "volume": 2_000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 105.8, "high": 105.9, "low": 105.6, "close": 105.8, "volume": 2_000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 105.8, "high": 106.0, "low": 105.6, "close": 105.9, "volume": 2_000}, ], "LOW_CLOSE": [ {"timestamp": "2026-01-05T08:15:00-05:00", "open": 102.0, "high": 102.2, "low": 101.9, "close": 102.1, "volume": 12_000}, {"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 105.4, "low": 103.8, "close": 104.3, "volume": 8_000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 104.3, "high": 104.8, "low": 104.2, "close": 104.5, "volume": 5_000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 104.5, "high": 104.7, "low": 104.3, "close": 104.6, "volume": 2_000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 104.6, "high": 104.7, "low": 104.4, "close": 104.6, "volume": 2_000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 104.6, "high": 104.8, "low": 104.4, "close": 104.7, "volume": 2_000}, ], } enrichment = _enrichment_for("HIGH_CLOSE", "LOW_CLOSE") enrichment["HIGH_CLOSE"]["2026-01-05"]["prev_close"] = 104.0 enrichment["LOW_CLOSE"]["2026-01-05"]["prev_close"] = 104.0 candidates = compute_orb_candidates( bars_by_ticker, "2026-01-05", params, enrichment, ) assert [cand["ticker"] for cand in candidates] == ["HIGH_CLOSE", "LOW_CLOSE"] def test_vwap_reclaim_ranking_can_prefer_weaker_opening_structure() -> None: params = ORBStrategyParams( engine_family="vwap_reclaim_v1", entry_direction="long_only", allow_red_to_green_breakout=True, weight_rvol=0.0, weight_gap=0.0, weight_dollar_vol=0.0, weight_premarket_dollar_vol=0.0, weight_close_location=-1.0, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.005, min_premarket_dollar_vol=100_000.0, max_gap_pct=None, max_candidates=5, min_candidates_to_trade=1, ) bars_by_ticker = { "DEEP_PULLBACK": [ {"timestamp": "2026-01-05T08:15:00-05:00", "open": 102.0, "high": 102.4, "low": 101.8, "close": 102.3, "volume": 18_000}, {"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 105.3, "low": 103.9, "close": 104.1, "volume": 8_000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 104.2, "high": 104.8, "low": 104.0, "close": 104.6, "volume": 4_000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 104.6, "high": 104.8, "low": 104.4, "close": 104.7, "volume": 2_000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 104.7, "high": 104.9, "low": 104.6, "close": 104.8, "volume": 2_000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 104.8, "high": 105.0, "low": 104.7, "close": 104.9, "volume": 2_000}, ], "STRONG_OPEN": [ {"timestamp": "2026-01-05T08:15:00-05:00", "open": 102.0, "high": 102.4, "low": 101.8, "close": 102.3, "volume": 18_000}, {"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 105.5, "low": 104.7, "close": 105.4, "volume": 8_000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 105.4, "high": 105.8, "low": 105.2, "close": 105.7, "volume": 4_000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 105.7, "high": 105.9, "low": 105.5, "close": 105.8, "volume": 2_000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 105.8, "high": 106.0, "low": 105.7, "close": 105.9, "volume": 2_000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 105.9, "high": 106.1, "low": 105.8, "close": 106.0, "volume": 2_000}, ], } enrichment = _enrichment_for("DEEP_PULLBACK", "STRONG_OPEN") enrichment["DEEP_PULLBACK"]["2026-01-05"]["prev_close"] = 104.0 enrichment["STRONG_OPEN"]["2026-01-05"]["prev_close"] = 104.0 candidates = compute_orb_candidates( bars_by_ticker, "2026-01-05", params, enrichment, ) assert [cand["ticker"] for cand in candidates] == ["DEEP_PULLBACK", "STRONG_OPEN"] def test_orb_pullback_ranking_can_use_event_catalyst_score() -> None: params = ORBStrategyParams( engine_family="orb_pullback_v1", entry_direction="long_only", weight_rvol=0.0, weight_gap=0.0, weight_dollar_vol=0.0, weight_premarket_dollar_vol=0.0, weight_event_catalyst=1.0, min_price=10.0, min_avg_dollar_volume=0.0, min_atr_14=0.1, min_rvol=0.1, min_abs_gap_pct=0.005, min_premarket_dollar_vol=100_000.0, max_gap_pct=None, max_candidates=5, min_candidates_to_trade=1, ) bars_by_ticker = { "PLAIN": [ {"timestamp": "2026-01-05T08:15:00-05:00", "open": 102.0, "high": 102.3, "low": 101.9, "close": 102.2, "volume": 12_000}, {"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 105.5, "low": 104.4, "close": 105.2, "volume": 8_000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 105.2, "high": 105.8, "low": 105.1, "close": 105.6, "volume": 5_000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 105.6, "high": 105.8, "low": 105.4, "close": 105.7, "volume": 2_000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 105.7, "high": 105.9, "low": 105.6, "close": 105.8, "volume": 2_000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 105.8, "high": 106.0, "low": 105.7, "close": 105.9, "volume": 2_000}, ], "EVENTFUL": [ {"timestamp": "2026-01-05T08:15:00-05:00", "open": 102.0, "high": 102.3, "low": 101.9, "close": 102.2, "volume": 12_000}, {"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 105.5, "low": 104.4, "close": 105.2, "volume": 8_000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 105.2, "high": 105.8, "low": 105.1, "close": 105.6, "volume": 5_000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 105.6, "high": 105.8, "low": 105.4, "close": 105.7, "volume": 2_000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 105.7, "high": 105.9, "low": 105.6, "close": 105.8, "volume": 2_000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 105.8, "high": 106.0, "low": 105.7, "close": 105.9, "volume": 2_000}, ], } enrichment = _enrichment_for("PLAIN", "EVENTFUL") enrichment["PLAIN"]["2026-01-05"]["prev_close"] = 104.0 enrichment["EVENTFUL"]["2026-01-05"]["prev_close"] = 104.0 enrichment["PLAIN"]["2026-01-05"]["event_score"] = 0.0 enrichment["EVENTFUL"]["2026-01-05"]["event_flag"] = True enrichment["EVENTFUL"]["2026-01-05"]["event_score"] = 3.0 candidates = compute_orb_candidates( bars_by_ticker, "2026-01-05", params, enrichment, ) assert [cand["ticker"] for cand in candidates] == ["EVENTFUL", "PLAIN"] def test_vwap_reclaim_trade_waits_for_prior_dip_then_enters_on_reclaim() -> None: bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 105.2, "low": 103.8, "close": 104.0, "volume": 1_000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 104.0, "high": 104.2, "low": 103.6, "close": 103.8, "volume": 1_000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 103.8, "high": 104.0, "low": 103.5, "close": 103.7, "volume": 1_000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 103.7, "high": 103.9, "low": 103.6, "close": 103.8, "volume": 1_000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 103.8, "high": 104.8, "low": 103.7, "close": 104.7, "volume": 1_500}, {"timestamp": "2026-01-05T09:55:00-05:00", "open": 104.7, "high": 105.0, "low": 104.5, "close": 104.9, "volume": 1_200}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 104.9, "high": 105.1, "low": 104.8, "close": 105.0, "volume": 1_000}, ] params = ORBStrategyParams( engine_family="vwap_reclaim_v1", orb_minutes=5, sim_bar_minutes=5, vwap_reclaim_window_start_min=20, vwap_reclaim_window_end_min=120, vwap_reclaim_require_prior_dip=True, vwap_reclaim_min_clearance_pct=0.0, atr_stop_multiplier=10.0, trailing_at_r=99.0, breakeven_at_r=99.0, slippage_bps=0.0, ) trade = simulate_orb_trade( bars, bars[0], "long", atr=1.0, rvol=2.0, gap_pct=0.05, params=params, equity=10_000.0, date_str="2026-01-05", ticker="VWAP", ) assert trade is not None assert trade.entry_time == "2026-01-05T09:50:00-05:00" assert trade.entry_price == pytest.approx(104.7) def test_vwap_reclaim_trade_can_require_orb_open_retake_and_rel_volume() -> None: bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 105.2, "low": 103.8, "close": 104.0, "volume": 1_000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 104.0, "high": 104.2, "low": 103.6, "close": 103.8, "volume": 1_000}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 103.8, "high": 104.0, "low": 103.5, "close": 103.7, "volume": 1_000}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 103.7, "high": 103.9, "low": 103.6, "close": 103.8, "volume": 1_000}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 103.8, "high": 104.9, "low": 103.7, "close": 104.7, "volume": 1_000}, {"timestamp": "2026-01-05T09:55:00-05:00", "open": 104.7, "high": 105.4, "low": 104.6, "close": 105.2, "volume": 1_500}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 105.2, "high": 105.4, "low": 105.1, "close": 105.3, "volume": 1_000}, ] params = ORBStrategyParams( engine_family="vwap_reclaim_v1", orb_minutes=5, sim_bar_minutes=5, vwap_reclaim_window_start_min=20, vwap_reclaim_window_end_min=120, vwap_reclaim_require_prior_dip=True, vwap_reclaim_min_clearance_pct=0.0, vwap_reclaim_require_orb_open_retake=True, vwap_reclaim_confirm_rel_vol=1.2, atr_stop_multiplier=10.0, trailing_at_r=99.0, breakeven_at_r=99.0, slippage_bps=0.0, ) trade = simulate_orb_trade( bars, bars[0], "long", atr=1.0, rvol=2.0, gap_pct=0.05, params=params, equity=10_000.0, date_str="2026-01-05", ticker="VWAP", ) assert trade is not None assert trade.entry_time == "2026-01-05T09:55:00-05:00" assert trade.entry_price == pytest.approx(105.2) def test_pullback_trade_waits_for_breakout_retake_and_reclaim_volume() -> None: bars = [ {"timestamp": "2026-01-05T09:30:00-05:00", "open": 105.0, "high": 106.0, "low": 104.8, "close": 105.8, "volume": 1_000}, {"timestamp": "2026-01-05T09:35:00-05:00", "open": 105.8, "high": 106.3, "low": 105.7, "close": 106.1, "volume": 1_500}, {"timestamp": "2026-01-05T09:40:00-05:00", "open": 106.1, "high": 106.8, "low": 106.0, "close": 106.6, "volume": 1_300}, {"timestamp": "2026-01-05T09:45:00-05:00", "open": 106.6, "high": 106.7, "low": 105.8, "close": 106.0, "volume": 700}, {"timestamp": "2026-01-05T09:50:00-05:00", "open": 106.0, "high": 106.2, "low": 105.9, "close": 106.05, "volume": 700}, {"timestamp": "2026-01-05T09:55:00-05:00", "open": 106.05, "high": 106.5, "low": 106.0, "close": 106.35, "volume": 1_600}, {"timestamp": "2026-01-05T15:55:00-05:00", "open": 106.35, "high": 106.5, "low": 106.2, "close": 106.4, "volume": 1_000}, ] params = ORBStrategyParams( engine_family="orb_pullback_v1", orb_minutes=5, sim_bar_minutes=5, pullback_entry=True, pullback_max_bars=6, pullback_min_retracement_pct=0.25, pullback_require_breakout_retake=True, pullback_breakout_retake_clearance_pct=0.002, pullback_reclaim_confirm_rel_vol=1.2, atr_stop_multiplier=10.0, trailing_at_r=99.0, breakeven_at_r=99.0, slippage_bps=0.0, ) trade = simulate_orb_trade( bars, bars[0], "long", atr=1.0, rvol=2.0, gap_pct=0.05, params=params, equity=10_000.0, date_str="2026-01-05", ticker="PULL", ) assert trade is not None assert trade.entry_time == "2026-01-05T09:55:00-05:00" assert trade.entry_price == pytest.approx(106.35)