"""Replay test for the live bar-high breakout helper. Verifies that ORBTradingEngine._post_orb_bar_high_breakout correctly identifies spike-and-retrace breakouts using today's actual 2026-05-07 intraday bars (SHAK, SNDK, ARM). Standalone — does not require the live broker, DB, or network — bars and breakout levels are pinned in the test data. """ from __future__ import annotations from unittest.mock import MagicMock import pytest from apps.orb_trader.engine import ORBTradingEngine from libs.intraday.domain import ORBStrategyParams # Bar-high reference data from /api/v1/alpaca/intraday/today on 2026-05-07. # Only fields needed by the helper (timestamp, high, low) are kept. SHAK_BARS = [ {"timestamp": "2026-05-07T13:30:00+00:00", "high": 71.53, "low": 69.075}, # ORB bar {"timestamp": "2026-05-07T13:35:00+00:00", "high": 70.09, "low": 68.505}, {"timestamp": "2026-05-07T13:40:00+00:00", "high": 70.325, "low": 68.75}, {"timestamp": "2026-05-07T13:45:00+00:00", "high": 70.575, "low": 68.85}, {"timestamp": "2026-05-07T13:50:00+00:00", "high": 69.03, "low": 67.965}, {"timestamp": "2026-05-07T13:55:00+00:00", "high": 68.29, "low": 67.92}, ] ARM_BARS = [ {"timestamp": "2026-05-07T13:30:00+00:00", "high": 231.475, "low": 218.25}, # ORB bar {"timestamp": "2026-05-07T13:35:00+00:00", "high": 232.19, "low": 223.69}, # crosses inside timeout {"timestamp": "2026-05-07T13:40:00+00:00", "high": 224.04, "low": 218.25}, {"timestamp": "2026-05-07T13:45:00+00:00", "high": 220.35, "low": 216.95}, {"timestamp": "2026-05-07T13:50:00+00:00", "high": 222.87, "low": 219.21}, {"timestamp": "2026-05-07T13:55:00+00:00", "high": 222.65, "low": 220.29}, ] SNDK_BARS = [ {"timestamp": "2026-05-07T13:30:00+00:00", "high": 1396.475, "low": 1355.3}, # ORB bar {"timestamp": "2026-05-07T13:35:00+00:00", "high": 1388.94, "low": 1370.69}, {"timestamp": "2026-05-07T13:40:00+00:00", "high": 1373.57, "low": 1342.75}, {"timestamp": "2026-05-07T13:45:00+00:00", "high": 1348.2, "low": 1328.12}, {"timestamp": "2026-05-07T13:50:00+00:00", "high": 1359.66, "low": 1344.165}, {"timestamp": "2026-05-07T13:55:00+00:00", "high": 1367.69, "low": 1352.4}, # Below: post-timeout (after 09:55 ET = 13:55 UTC + 5min boundary). # SNDK's only crossing of 1396.475 is at 14:20 UTC (10:20 ET), past timeout. {"timestamp": "2026-05-07T14:00:00+00:00", "high": 1354.98, "low": 1344.62}, {"timestamp": "2026-05-07T14:05:00+00:00", "high": 1366.06, "low": 1345.93}, {"timestamp": "2026-05-07T14:10:00+00:00", "high": 1369.58, "low": 1359.39}, {"timestamp": "2026-05-07T14:15:00+00:00", "high": 1389.71, "low": 1362.02}, {"timestamp": "2026-05-07T14:20:00+00:00", "high": 1399.98, "low": 1377.18}, # crosses, post-timeout ] def _make_engine(orb_minutes: int = 5, order_timeout_minutes: int = 25) -> ORBTradingEngine: """Build an engine instance with only the fields the helper touches.""" params = ORBStrategyParams( orb_minutes=orb_minutes, order_timeout_minutes=order_timeout_minutes, live_breakout_use_bar_high=True, ) eng = ORBTradingEngine.__new__(ORBTradingEngine) eng._params = params eng._session = MagicMock(session_id="test") return eng @pytest.mark.parametrize( "name,bars,direction,breakout_level,expected,reason", [ ("ARM_inside_timeout", ARM_BARS, "bullish", 231.475, True, "13:35 UTC bar high 232.19 crosses 231.475 inside 25-min timeout"), ("SNDK_post_timeout", SNDK_BARS, "bullish", 1396.475, False, "Only crossing (14:20 UTC high 1399.98) is past 25-min timeout"), ("SHAK_no_breakout", SHAK_BARS, "bullish", 71.53, False, "No post-ORB bar high reaches 71.53"), ], ) def test_bar_high_breakout_replay(name, bars, direction, breakout_level, expected, reason): eng = _make_engine(orb_minutes=5, order_timeout_minutes=25) actual = eng._post_orb_bar_high_breakout( bars, direction, breakout_level, "2026-05-07" ) assert actual is expected, f"{name}: expected {expected} ({reason}), got {actual}" def test_bar_high_breakout_skips_orb_bar(): """The ORB bar's own high must not be allowed to trigger its own breakout (breakout_level is set FROM that bar, so it always equals it).""" eng = _make_engine(orb_minutes=5, order_timeout_minutes=25) # Only the ORB bar — no post-ORB bars at all. bars = [{"timestamp": "2026-05-07T13:30:00+00:00", "high": 100.0, "low": 90.0}] assert eng._post_orb_bar_high_breakout(bars, "bullish", 100.0, "2026-05-07") is False def test_bar_high_breakout_short_direction(): """Bearish direction triggers when bar low <= breakout_level (the ORB low).""" eng = _make_engine(orb_minutes=5, order_timeout_minutes=25) bars = [ {"timestamp": "2026-05-07T13:30:00+00:00", "high": 100.0, "low": 95.0}, # ORB {"timestamp": "2026-05-07T13:35:00+00:00", "high": 99.0, "low": 94.5}, # crosses ] assert eng._post_orb_bar_high_breakout(bars, "bearish", 95.0, "2026-05-07") is True