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:
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user