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

@@ -676,8 +676,10 @@ def load_state(market: str, strategy_name: str, book: str | None = None) -> dict
def save_state(state: dict, market: str, strategy_name: str, book: str | None = None) -> None:
path = _state_path(market, strategy_name, book)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w") as f:
tmp = f"{path}.tmp"
with open(tmp, "w") as f:
json.dump(state, f, indent=2, default=str)
os.replace(tmp, path)
def init_state(market: str, strategy_name: str, capital: float) -> dict:
@@ -1830,12 +1832,44 @@ def refresh_panels(market: str, strategy_names: list[str], label: str) -> None:
data_market = strategy_data_market(market, name)
requests.setdefault(data_market, set()).update(
price_request(market, list(tickers) + [benchmark]))
fields.setdefault(data_market, {"close", "open"}).update(strategy_fields(name))
fields.setdefault(data_market, {"close", "open", "volume"}).update(strategy_fields(name))
for data_market, request in requests.items():
print(f"[{label}] Refreshing {data_market} panel for {len(request)} tickers")
data_manager.update(data_market, sorted(request), fields=sorted(fields[data_market]))
def _rewind_state(state: dict, before: pd.Timestamp) -> None:
cutoff = str(before.date())
holdings = dict(state["holdings"])
cash = state["cash"]
# Trade values and commissions retain full precision; daily snapshots round cash.
for trade in reversed(state["trade_log"]):
if trade["date"] < cutoff:
continue
ticker, shares = trade["ticker"], trade["shares"]
if trade["action"] == "BUY":
holdings[ticker] = holdings.get(ticker, 0) - shares
cash += trade["value"] + trade["commission"]
else:
holdings[ticker] = holdings.get(ticker, 0) + shares
cash -= trade["value"] - trade["commission"]
state["holdings"] = {t: q for t, q in holdings.items() if abs(q) > 1e-8}
state["cash"] = cash
state["trade_log"] = [t for t in state["trade_log"] if t["date"] < cutoff]
state["daily_log"] = [d for d in state.get("daily_log", []) if d["date"] < cutoff]
state["daily_equity"] = {d: v for d, v in state["daily_equity"].items() if d < cutoff}
state["engine_changes"] = [e for e in state.get("engine_changes", []) if e["first_session"] < cutoff]
last = max(state["daily_equity"], default=None)
state["last_morning"] = state["last_evening"] = last
state["pending_trades"] = None
state["engine_version"] = state["daily_log"][-1].get("engine_version") if state["daily_log"] else None
if state["daily_log"] and not state["engine_version"]:
# The legacy ledger rounded trades, so its recorded snapshot is authoritative.
snapshot = state["daily_log"][-1]
state["cash"] = snapshot["cash"]
state["holdings"] = {t: h["shares"] for t, h in snapshot["holdings"].items()}
def _process_sessions(args, state: dict | None, start: pd.Timestamp | None = None,
label: str = "auto", refresh: bool = True) -> dict | None:
"""Run every completed session not yet recorded in ``state`` (catch-up safe).
@@ -1876,6 +1910,16 @@ def _process_sessions(args, state: dict | None, start: pd.Timestamp | None = Non
else:
last = state.get("last_evening")
first = book_start(getattr(args, "book", None), market)
if last and state.get("daily_equity"):
beginning = first if first is not None else pd.Timestamp(min(state["daily_equity"]))
recorded = pd.to_datetime(list(state["daily_equity"]))
gaps = sessions[(sessions >= beginning) & (sessions <= pd.Timestamp(last))
& ~sessions.isin(recorded)]
if len(gaps):
print(f"[{label}] {strategy_name}: backfilled {gaps[0].date()}; replaying from that session")
_rewind_state(state, gaps[0])
last = state.get("last_evening")
first = beginning
if last:
todo = sessions[sessions > pd.Timestamp(last)]
else: # created by a morning run: start with the book's first session
@@ -1910,7 +1954,8 @@ def cmd_auto(args):
Processes every completed session since the last recorded one, so missed
days (monitor downtime, late data) are caught up in order. A session is
only processed after the exchange has closed.
only processed after the exchange has closed. Backfilled historical days
rewind the ledger and replay the affected sessions in order.
Usage with cron (US market, run at 5pm ET weekdays):
0 17 * * 1-5 cd /path/to/quant && uv run python trader.py auto --market us