Files
momentum-system/tests/unit/test_eod_restart.py
T
momentum devandClaude Fable 5 d16eb85ee9 money-path: daily self-restart + SIGTERM + positive-evidence exit gate
Plan 003 steps 1-3.
- Loop exit(0) after a SUCCESSFUL EOD flatten (TRADING_EXIT_AFTER_EOD_FLATTEN,
  default on); compose restarts fresh. Restart-churn guard: _rebuild_daily_state
  detects today's eod_flatten event and seeds _session_closed/_eod_done so a
  post-flatten restart neither re-flattens nor re-exits. SIGTERM = clean drain.
- Exit manager: _FETCH_FAILED sentinel distinguishes a failed get_orders from a
  genuinely-empty snapshot. A failed fetch skips naked-stop enforcement (catastrophe
  still exits, re-fetching); an empty-but-successful snapshot needs TWO consecutive
  reads before 'naked' is trusted (Alpaca silently returns [] for symbols=). Costs
  ~one 12s pass on the all-naked case only; isolated naked positions unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 22:20:37 -04:00

89 lines
3.2 KiB
Python

"""Daily self-restart: the process is ONE trading session (plan 003, steps 1-2).
After a SUCCESSFUL EOD flatten the loop requests a clean exit and compose
restarts it fresh; a post-flatten restart must neither re-flatten nor re-exit
(today's eod_flatten event in the store is the truth); a FAILED flatten keeps
retrying inside the window.
"""
from datetime import datetime
import run_live_paper as rlp
from alpaca_paper.execution import AlpacaPaperExecutor
from storage.event_schema import EventMode, RiskRuleTriggeredEvent
from storage.event_store import EventStore
from trading_execution import ExecutionSettings, TradingExecutionService
class _FakeClient:
def submit_order(self, **kw):
return {}
def get_account(self):
return {"equity": "100000", "last_equity": "100000"}
def _svc(store):
return TradingExecutionService(
store, executor=AlpacaPaperExecutor(store, client=_FakeClient()),
settings=ExecutionSettings(), session_id="t",
)
def _emit_eod_flatten(store, errors):
store.emit(RiskRuleTriggeredEvent(
timestamp=datetime.now(), mode=EventMode.PAPER, correlation_id="t",
message="End-of-day flatten (eod_flatten)",
rule_type="eod_flatten", rule_value=0.0,
current_state={"cancelled_entries": 0,
"closed_positions": [] if errors else ["AAA"],
"errors": errors},
action_taken="closed_session",
))
def test_eod_exit_flag(monkeypatch):
"""_request_exit_after_eod: ok+flag-on -> exit requested; failed flatten or
flag-off -> not (a failed flatten must keep retrying inside the window)."""
rlp._clean_exit_requested.clear()
monkeypatch.setenv("TRADING_EXIT_AFTER_EOD_FLATTEN", "1")
assert rlp._request_exit_after_eod(True) is True
assert rlp._clean_exit_requested.is_set()
rlp._clean_exit_requested.clear()
assert rlp._request_exit_after_eod(False) is False
assert not rlp._clean_exit_requested.is_set()
monkeypatch.setenv("TRADING_EXIT_AFTER_EOD_FLATTEN", "0")
assert rlp._request_exit_after_eod(True) is False
assert not rlp._clean_exit_requested.is_set()
rlp._clean_exit_requested.clear()
def test_boot_after_eod_flatten():
"""Restart-churn guard: with today's SUCCESSFUL eod_flatten event in the
store, a fresh service boots session-closed (no re-entry) and reports
done-today (the loop then neither re-flattens nor re-exits)."""
store = EventStore(":memory:")
_emit_eod_flatten(store, errors=[])
svc = _svc(store)
assert svc._session_closed is True
assert svc.eod_flatten_done_today() is True
def test_boot_after_failed_eod_flatten_still_retries():
"""A FAILED flatten blocks re-entry but must NOT read as done — the fresh
process retries the flatten inside the window exactly as before."""
store = EventStore(":memory:")
_emit_eod_flatten(store, errors=["close AAA: 403"])
svc = _svc(store)
assert svc._session_closed is True
assert svc.eod_flatten_done_today() is False
def test_boot_fresh_day_not_closed():
store = EventStore(":memory:")
svc = _svc(store)
assert svc._session_closed is False
assert svc.eod_flatten_done_today() is False