perf(replay): refresh panels once and compute signals in one pass
Replay refreshes each price panel once for the union of tickers and then replays offline; target weights for all sessions come from one causal signal pass instead of re-running the strategy per day (verified identical daily equity for v7_vt36, recovery_mom_top10 and fc monthly). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
66
trader.py
66
trader.py
@@ -337,17 +337,23 @@ def price_request(market: str, tickers: list[str]) -> list[str]:
|
|||||||
|
|
||||||
|
|
||||||
def load_strategy_prices(market: str, strategy_name: str, tickers: list[str],
|
def load_strategy_prices(market: str, strategy_name: str, tickers: list[str],
|
||||||
benchmark: str, with_open: bool = False):
|
benchmark: str, with_open: bool = False, refresh: bool = True):
|
||||||
"""Refresh and load a strategy's prices in its portfolio currency.
|
"""Load a strategy's prices in its portfolio currency, refreshing them first
|
||||||
|
unless ``refresh`` is False (the panel is then read as stored).
|
||||||
|
|
||||||
Returns ``(close, open_or_None, close_raw)``. ``close_raw`` keeps NaN
|
Returns ``(close, open_or_None, close_raw)``. ``close_raw`` keeps NaN
|
||||||
where the exchange printed no bar that day, so callers can distinguish a
|
where the exchange printed no bar that day, so callers can distinguish a
|
||||||
stale forward-filled price from a real one.
|
stale forward-filled price from a real one.
|
||||||
"""
|
"""
|
||||||
data_market = strategy_data_market(market, strategy_name)
|
data_market = strategy_data_market(market, strategy_name)
|
||||||
request = price_request(market, list(tickers) + [benchmark])
|
if refresh:
|
||||||
result = data_manager.update(data_market, request, with_open=with_open)
|
request = price_request(market, list(tickers) + [benchmark])
|
||||||
close, open_data = result if with_open else (result, None)
|
result = data_manager.update(data_market, request, with_open=with_open)
|
||||||
|
close, open_data = result if with_open else (result, None)
|
||||||
|
else:
|
||||||
|
close = data_manager.load(data_market, "close")
|
||||||
|
open_data = (data_manager._fill_open(close, data_manager.load_raw(data_market, "open"))
|
||||||
|
if with_open else None)
|
||||||
close_raw = data_manager.load_raw(data_market, "close").reindex(
|
close_raw = data_manager.load_raw(data_market, "close").reindex(
|
||||||
index=close.index, columns=close.columns,
|
index=close.index, columns=close.columns,
|
||||||
)
|
)
|
||||||
@@ -723,19 +729,37 @@ def _strategy_fees(args, market: str) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _signals_for_sessions(strategy_name: str, open_data: pd.DataFrame,
|
||||||
|
days: pd.DatetimeIndex) -> pd.DataFrame:
|
||||||
|
"""Target weights for many sessions in one pass: row ``d`` uses opens up to ``d``.
|
||||||
|
|
||||||
|
Equivalent to ``generate_target_weights`` per day for causal strategies
|
||||||
|
(weights at t never depend on data after t), which every registered
|
||||||
|
strategy must be.
|
||||||
|
"""
|
||||||
|
hist = open_data.loc[:days[-1]]
|
||||||
|
dummy = pd.DataFrame([hist.iloc[-1].values], index=[hist.index[-1] + pd.Timedelta(days=1)],
|
||||||
|
columns=hist.columns)
|
||||||
|
weights = STRATEGY_REGISTRY[strategy_name]().generate_signals(pd.concat([hist, dummy]))
|
||||||
|
return weights.shift(-1).reindex(days).fillna(0.0)
|
||||||
|
|
||||||
|
|
||||||
def _run_session(state: dict, strategy_name: str, market: str, day: pd.Timestamp,
|
def _run_session(state: dict, strategy_name: str, market: str, day: pd.Timestamp,
|
||||||
tickers: list[str], benchmark: str, close_data: pd.DataFrame,
|
tickers: list[str], benchmark: str, close_data: pd.DataFrame,
|
||||||
open_data: pd.DataFrame, close_raw: pd.DataFrame, fees: dict,
|
open_data: pd.DataFrame, close_raw: pd.DataFrame, fees: dict,
|
||||||
integer_shares: bool) -> dict:
|
integer_shares: bool, signal_row: pd.Series | None = None) -> dict:
|
||||||
"""Signal on ``day``'s open, execute at ``day``'s close, record the day.
|
"""Signal on ``day``'s open, execute at ``day``'s close, record the day.
|
||||||
|
|
||||||
Tickers without a real closing print on ``day`` (forward-filled price) are
|
Tickers without a real closing print on ``day`` (forward-filled price) are
|
||||||
valued but not traded.
|
valued but not traded. ``signal_row`` supplies precomputed target weights.
|
||||||
"""
|
"""
|
||||||
day_str = str(day.date())
|
day_str = str(day.date())
|
||||||
lot_size = MARKET_LOT_SIZE.get(market, 1) if integer_shares else 1
|
lot_size = MARKET_LOT_SIZE.get(market, 1) if integer_shares else 1
|
||||||
strategy = STRATEGY_REGISTRY[strategy_name]()
|
if signal_row is None:
|
||||||
target_weights = generate_target_weights(strategy, open_data.loc[:day, tickers], day)
|
strategy = STRATEGY_REGISTRY[strategy_name]()
|
||||||
|
target_weights = generate_target_weights(strategy, open_data.loc[:day, tickers], day)
|
||||||
|
else:
|
||||||
|
target_weights = {t: float(w) for t, w in signal_row.items() if w > 1e-6}
|
||||||
|
|
||||||
all_held = sorted(set(target_weights) | set(state["holdings"]))
|
all_held = sorted(set(target_weights) | set(state["holdings"]))
|
||||||
close_prices = _with_last_known_prices(
|
close_prices = _with_last_known_prices(
|
||||||
@@ -1672,7 +1696,7 @@ def cmd_monitor(args):
|
|||||||
|
|
||||||
|
|
||||||
def _process_sessions(args, state: dict | None, start: pd.Timestamp | None = None,
|
def _process_sessions(args, state: dict | None, start: pd.Timestamp | None = None,
|
||||||
label: str = "auto") -> dict | None:
|
label: str = "auto", refresh: bool = True) -> dict | None:
|
||||||
"""Run every completed session not yet recorded in ``state`` (catch-up safe).
|
"""Run every completed session not yet recorded in ``state`` (catch-up safe).
|
||||||
|
|
||||||
A fresh portfolio starts at ``start`` when given, otherwise at the most
|
A fresh portfolio starts at ``start`` when given, otherwise at the most
|
||||||
@@ -1685,7 +1709,7 @@ def _process_sessions(args, state: dict | None, start: pd.Timestamp | None = Non
|
|||||||
return None
|
return None
|
||||||
tickers, benchmark = strategy_universe(market, strategy_name)
|
tickers, benchmark = strategy_universe(market, strategy_name)
|
||||||
close_data, open_data, close_raw = load_strategy_prices(
|
close_data, open_data, close_raw = load_strategy_prices(
|
||||||
market, strategy_name, tickers, benchmark, with_open=True,
|
market, strategy_name, tickers, benchmark, with_open=True, refresh=refresh,
|
||||||
)
|
)
|
||||||
tickers = filter_tradable_tickers(close_data, tickers)
|
tickers = filter_tradable_tickers(close_data, tickers)
|
||||||
sessions = completed_sessions(close_raw, strategy_data_market(market, strategy_name), benchmark)
|
sessions = completed_sessions(close_raw, strategy_data_market(market, strategy_name), benchmark)
|
||||||
@@ -1711,9 +1735,12 @@ def _process_sessions(args, state: dict | None, start: pd.Timestamp | None = Non
|
|||||||
f"{todo[0].date()} .. {todo[-1].date()}")
|
f"{todo[0].date()} .. {todo[-1].date()}")
|
||||||
|
|
||||||
fees = _strategy_fees(args, market)
|
fees = _strategy_fees(args, market)
|
||||||
|
signals = (_signals_for_sessions(strategy_name, open_data[tickers], todo)
|
||||||
|
if len(todo) > 1 else None)
|
||||||
for day in todo:
|
for day in todo:
|
||||||
r = _run_session(state, strategy_name, market, day, tickers, benchmark,
|
r = _run_session(state, strategy_name, market, day, tickers, benchmark,
|
||||||
close_data, open_data, close_raw, fees, args.integer_shares)
|
close_data, open_data, close_raw, fees, args.integer_shares,
|
||||||
|
signal_row=signals.loc[day] if signals is not None else None)
|
||||||
frozen = f" | no fresh price: {', '.join(r['frozen'])}" if r["frozen"] else ""
|
frozen = f" | no fresh price: {', '.join(r['frozen'])}" if r["frozen"] else ""
|
||||||
print(f"[{label}] {r['date']} {strategy_name:<34} value {r['value']:>12,.2f} "
|
print(f"[{label}] {r['date']} {strategy_name:<34} value {r['value']:>12,.2f} "
|
||||||
f"({r['daily_return']:+.2%}) trades {r['trades']:>2} "
|
f"({r['daily_return']:+.2%}) trades {r['trades']:>2} "
|
||||||
@@ -1755,6 +1782,19 @@ def cmd_replay(args):
|
|||||||
os.path.basename(p)[len(f"trader_{args.market}_"):-len(".json")]
|
os.path.basename(p)[len(f"trader_{args.market}_"):-len(".json")]
|
||||||
for p in __import__("glob").glob(os.path.join("data", f"trader_{args.market}_*.json"))
|
for p in __import__("glob").glob(os.path.join("data", f"trader_{args.market}_*.json"))
|
||||||
)
|
)
|
||||||
|
live_names = [n for n in names if not n.startswith("sim_") and n in STRATEGY_REGISTRY
|
||||||
|
and n not in LIVE_EXCLUDED_STRATEGIES and strategy_supports_market(n, args.market)]
|
||||||
|
|
||||||
|
# Refresh each price panel once for the union of tickers, then replay offline.
|
||||||
|
requests: dict[str, set[str]] = {}
|
||||||
|
for name in live_names:
|
||||||
|
tickers, benchmark = strategy_universe(args.market, name)
|
||||||
|
requests.setdefault(strategy_data_market(args.market, name), set()).update(
|
||||||
|
price_request(args.market, list(tickers) + [benchmark]))
|
||||||
|
for data_market, request in requests.items():
|
||||||
|
print(f"[replay] Refreshing {data_market} panel for {len(request)} tickers")
|
||||||
|
data_manager.update(data_market, sorted(request), with_open=True)
|
||||||
|
|
||||||
for name in names:
|
for name in names:
|
||||||
if name.startswith("sim_"):
|
if name.startswith("sim_"):
|
||||||
continue
|
continue
|
||||||
@@ -1774,7 +1814,7 @@ def cmd_replay(args):
|
|||||||
if start is None:
|
if start is None:
|
||||||
print(f"[replay] {name}: no start date; skipping")
|
print(f"[replay] {name}: no start date; skipping")
|
||||||
continue
|
continue
|
||||||
state = _process_sessions(sub, None, start=start, label="replay")
|
state = _process_sessions(sub, None, start=start, label="replay", refresh=False)
|
||||||
if state is not None:
|
if state is not None:
|
||||||
save_state(state, args.market, name)
|
save_state(state, args.market, name)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user