live: keep new sessions on subset refreshes; replay books from a backfilled day

A refresh of a few tickers judged a new date's coverage against every
ticker in the stored panel, so the date was dropped as sparse; it also
re-ran the sparse filter over the whole history. Coverage is now checked
only inside the refresh window, with unrequested tickers counted where they
already have a bar.

When a session that was skipped earlier appears in the panel, the trader
rewinds the ledger to that day (undoing later trades, or restoring a legacy
snapshot) and replays every session from it in order. State files are
written atomically, and the monitor refreshes volume with close/open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 05:16:16 -07:00
parent 2e69bc8c7a
commit 31155fdcfb
3 changed files with 132 additions and 9 deletions

View File

@@ -1,6 +1,7 @@
"""Contracts of the live pipeline: price panel refresh, session gating,
order sizing, and how results are reported."""
import copy
import unittest
from datetime import datetime, timezone
from types import SimpleNamespace
@@ -62,17 +63,47 @@ class PanelRefreshTests(unittest.TestCase):
self.assertNotIn(panel.index[2], cleaned.index)
self.assertEqual(len(cleaned), 4)
def test_subset_refresh_keeps_trading_days_without_erasing_older_history(self):
stored = _panel({"A": [np.nan, 10, 11], "B": [np.nan, 20, 21],
"C": [30, 31, 32], "D": [40, 41, 42], "E": [50, 51, 52]})
fresh = pd.DataFrame({"A": [11, 12], "B": [21, 22]},
index=pd.to_datetime(["2026-09-16", "2026-09-17"]))
merged = data_manager._merge_refresh(stored, fresh, fresh.index[0])
self.assertIn(fresh.index[-1], merged.index)
self.assertEqual(merged.loc[fresh.index[-1], "A"], 12)
self.assertTrue(pd.isna(merged.loc[fresh.index[-1], "C"]))
pd.testing.assert_frame_equal(merged.loc[stored.index[:2]], stored.iloc[:2], check_freq=False)
def test_subset_refresh_still_drops_partial_rows_in_the_requested_universe(self):
stored = _panel({c: [10, 10, 10] for c in "ABCDEFGH"})
fresh = pd.DataFrame({"A": [10, np.nan], "B": [10, np.nan],
"C": [10, np.nan], "D": [10, 11]},
index=pd.to_datetime(["2026-09-16", "2026-09-17"]))
merged = data_manager._merge_refresh(stored, fresh, fresh.index[0])
self.assertNotIn(fresh.index[-1], merged.index)
self.assertIn(stored.index[0], merged.index)
def test_failed_requested_columns_still_count_toward_freshness(self):
stored = _panel({c: [10, 10, 10] for c in "ABCDEFGH"})
fresh = pd.DataFrame({"A": [10, 11], "B": [np.nan, np.nan],
"C": [np.nan, np.nan], "D": [np.nan, np.nan]},
index=pd.to_datetime(["2026-09-16", "2026-09-17"]))
merged = data_manager._merge_refresh(stored, fresh, fresh.index[0])
self.assertNotIn(fresh.index[-1], merged.index)
self.assertEqual(merged.loc[stored.index[-1], "B"], 10)
class LiveBookTests(unittest.TestCase):
BOOK = {"label": "test", "start": {"us": "2026-09-16"}, "capital": {"us": 5_000.0},
"fee_per_order": {"us": 1.0}, "strategies": {"us": ["momentum"]}}
def _run(self, state: dict):
close = _panel({"AAA": [10, 11, 12, 13, 14], "BBB": [20, 20, 21, 21, 22],
"SPY": [1, 1, 1, 1, 1]})
def _run(self, state: dict, close=None, targets=None):
if close is None:
close = _panel({"AAA": [10, 11, 12, 13, 14], "BBB": [20, 20, 21, 21, 22],
"SPY": [1, 1, 1, 1, 1]})
args = SimpleNamespace(market="us", strategy="momentum", book="test", capital=None,
tx_cost=0.001, fixed_fee=2.0, integer_shares=True)
equal = pd.DataFrame(0.5, index=close.index, columns=["AAA", "BBB"])
equal = targets if targets is not None else pd.DataFrame(0.5, index=close.index, columns=["AAA", "BBB"])
with mock.patch.dict(trader.LIVE_BOOKS, {"test": self.BOOK}), \
mock.patch.object(trader, "strategy_universe", lambda m, n: (["AAA", "BBB"], "SPY")), \
mock.patch.object(trader, "pit_candidates", lambda m, n, s: []), \
@@ -92,6 +123,46 @@ class LiveBookTests(unittest.TestCase):
with mock.patch.dict(self.BOOK, {"start": {"us": "2026-10-01"}}):
self.assertIsNone(self._run({}))
def test_backfilled_session_replays_later_trades_and_is_idempotent(self):
close = _panel({"AAA": [10, 11, 12, 13, 14], "BBB": [20, 20, 21, 21, 22],
"SPY": [1, 1, 1, 1, 1]})
targets = pd.DataFrame({"AAA": [1, 1, 1, 0, 1], "BBB": [0, 0, 0, 1, 0]}, index=close.index)
expected = self._run({}, close, targets)
missing = close.index[3]
state = self._run({}, close.drop(missing), targets)
self.assertNotEqual(state["cash"], expected["cash"])
repaired = self._run(state, close, targets)
self.assertEqual(repaired["holdings"], expected["holdings"])
self.assertAlmostEqual(repaired["cash"], expected["cash"])
self.assertEqual(repaired["daily_equity"], expected["daily_equity"])
self.assertEqual(repaired["daily_log"], expected["daily_log"])
self.assertEqual(repaired["trade_log"], expected["trade_log"])
self.assertEqual(repaired["last_evening"], "2026-09-18")
before = copy.deepcopy(repaired)
self.assertEqual(self._run(repaired, close, targets), before)
def test_backfilled_first_book_session_restores_original_capital(self):
close = _panel({"AAA": [10, 11, 12, 13, 14], "BBB": [20, 20, 21, 21, 22],
"SPY": [1, 1, 1, 1, 1]})
expected = self._run({}, close)
state = self._run({}, close.drop(close.index[2]))
repaired = self._run(state, close)
self.assertEqual(repaired["daily_equity"], expected["daily_equity"])
self.assertEqual(repaired["holdings"], expected["holdings"])
self.assertAlmostEqual(repaired["cash"], expected["cash"])
def test_legacy_checkpoint_restores_cash_when_old_trade_values_were_rounded(self):
state = trader.init_state("us", "momentum", 50)
state.update(cash=9.98, holdings={"AAA": 4}, last_evening="2026-09-18",
daily_equity={"2026-09-16": 50, "2026-09-18": 49.98},
daily_log=[{"date": "2026-09-16", "cash": 50, "holdings": {}}],
trade_log=[{"date": "2026-09-18", "action": "BUY", "ticker": "AAA",
"shares": 4, "value": 40, "commission": 0}])
trader._rewind_state(state, pd.Timestamp("2026-09-17"))
self.assertEqual(state["cash"], 50)
self.assertEqual(state["holdings"], {})
self.assertEqual(state["last_evening"], "2026-09-16")
def test_book_strategies_are_live_strategies_of_their_market(self):
for book, cfg in trader.LIVE_BOOKS.items():
for market, names in cfg["strategies"].items():