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:
51
trader.py
51
trader.py
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user