You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
121 lines
4.2 KiB
Python
121 lines
4.2 KiB
Python
"""Tests: AutoScheduler catchup re-runs missed phases after restart."""
|
|
from __future__ import annotations
|
|
|
|
import datetime as dt
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
from zoneinfo import ZoneInfo
|
|
|
|
import pytest
|
|
|
|
|
|
ET = ZoneInfo("America/New_York")
|
|
|
|
|
|
def _make_scheduler():
|
|
from apps.web.paper_trading_service import AutoScheduler
|
|
sched = AutoScheduler()
|
|
sched._sessions = ["session_a"]
|
|
sched._db_path = "/tmp/test.db"
|
|
sched._log = lambda msg: None
|
|
sched._dry_run = False
|
|
return sched
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_open_reruns_if_missed_and_not_in_phases():
|
|
"""If server restarts at 10 AM ET and run_open not in processed_phases, it must run."""
|
|
sched = _make_scheduler()
|
|
|
|
# now_et = 10:00 AM ET on a trading day
|
|
now_et = dt.datetime(2026, 4, 24, 10, 0, 0, tzinfo=ET)
|
|
|
|
mock_state = MagicMock()
|
|
mock_session = MagicMock()
|
|
mock_session.session_id = "sid-abc"
|
|
mock_state.get_session.return_value = mock_session
|
|
mock_state.is_phase_processed.return_value = False # not yet run
|
|
|
|
run_trading_calls = []
|
|
|
|
async def fake_run_trading(cmd, sessions):
|
|
run_trading_calls.append(cmd)
|
|
|
|
with (
|
|
patch.object(sched, "_now_et", return_value=now_et),
|
|
patch.object(sched, "_is_trading_day", return_value=True),
|
|
patch.object(sched, "_prev_trading_day", return_value=dt.date(2026, 4, 23)),
|
|
patch.object(sched, "_run_pipeline", new_callable=AsyncMock, return_value=True),
|
|
patch.object(sched, "_run_trading", side_effect=fake_run_trading),
|
|
patch("apps.paper_trader.state.StateManager", return_value=mock_state),
|
|
):
|
|
await sched._run_catchup()
|
|
|
|
assert "run-open" in run_trading_calls
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_open_not_rerun_if_already_in_phases():
|
|
"""If run_open already succeeded today (in processed_phases), skip it."""
|
|
sched = _make_scheduler()
|
|
|
|
now_et = dt.datetime(2026, 4, 24, 11, 0, 0, tzinfo=ET)
|
|
|
|
mock_state = MagicMock()
|
|
mock_session = MagicMock()
|
|
mock_session.session_id = "sid-abc"
|
|
mock_state.get_session.return_value = mock_session
|
|
mock_state.is_phase_processed.return_value = True # already ran
|
|
|
|
run_trading_calls = []
|
|
|
|
async def fake_run_trading(cmd, sessions):
|
|
run_trading_calls.append(cmd)
|
|
|
|
with (
|
|
patch.object(sched, "_now_et", return_value=now_et),
|
|
patch.object(sched, "_is_trading_day", return_value=True),
|
|
patch.object(sched, "_prev_trading_day", return_value=dt.date(2026, 4, 23)),
|
|
patch.object(sched, "_run_pipeline", new_callable=AsyncMock, return_value=True),
|
|
patch.object(sched, "_run_trading", side_effect=fake_run_trading),
|
|
patch("apps.paper_trader.state.StateManager", return_value=mock_state),
|
|
):
|
|
await sched._run_catchup()
|
|
|
|
assert "run-open" not in run_trading_calls
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_close_reruns_if_missed():
|
|
"""If server restarts at 16:00 ET and run_close not in processed_phases, it must run."""
|
|
sched = _make_scheduler()
|
|
|
|
now_et = dt.datetime(2026, 4, 24, 16, 0, 0, tzinfo=ET)
|
|
|
|
mock_state = MagicMock()
|
|
mock_session = MagicMock()
|
|
mock_session.session_id = "sid-abc"
|
|
mock_state.get_session.return_value = mock_session
|
|
# is_phase_processed: next_open=True (already ran), reaction_close=False (missed)
|
|
def is_phase_processed(sid, date, phase):
|
|
return phase == "next_open"
|
|
mock_state.is_phase_processed.side_effect = is_phase_processed
|
|
|
|
run_trading_calls = []
|
|
|
|
async def fake_run_trading(cmd, sessions):
|
|
run_trading_calls.append(cmd)
|
|
|
|
with (
|
|
patch.object(sched, "_now_et", return_value=now_et),
|
|
patch.object(sched, "_is_trading_day", return_value=True),
|
|
patch.object(sched, "_prev_trading_day", return_value=dt.date(2026, 4, 23)),
|
|
patch.object(sched, "_run_pipeline", new_callable=AsyncMock, return_value=True),
|
|
patch.object(sched, "_run_trading", side_effect=fake_run_trading),
|
|
patch("apps.paper_trader.state.StateManager", return_value=mock_state),
|
|
):
|
|
await sched._run_catchup()
|
|
|
|
assert "run-close" in run_trading_calls
|
|
# run_open should NOT re-run (market already closed)
|
|
assert "run-open" not in run_trading_calls
|