Skip to content

Commit 80a3524

Browse files
Pigbibicodex
andcommitted
fix: separate current drift returns from baseline window
Co-Authored-By: Codex <noreply@openai.com>
1 parent 6887174 commit 80a3524

2 files changed

Lines changed: 59 additions & 19 deletions

File tree

scripts/run_walk_forward_backtest.py

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from typing import Any
1515

1616
import pandas as pd
17+
from quant_platform_kit.strategy_lifecycle.performance_metrics import compute_window_metrics
1718

1819
from crypto_strategies.backtest.orchestrator_runner import (
1920
COMBO_DEFAULT_MIN_HISTORY_DAYS,
@@ -22,7 +23,6 @@
2223
SUPPORTED_PROFILES,
2324
build_backtest_runner,
2425
)
25-
from crypto_strategies.backtest.live_pool_simulator import _performance_metrics
2626
from crypto_strategies.strategies.crypto_equity_combo import PROFILE_NAME as CRYPTO_EQUITY_COMBO_PROFILE
2727

2828
DEFAULT_WINDOWS: tuple[tuple[date, date], ...] = (
@@ -95,7 +95,7 @@ def _normalize_panel(panel: pd.DataFrame) -> pd.DataFrame:
9595
frame["open"] = pd.to_numeric(frame["open"], errors="coerce")
9696
frame["final_score"] = pd.to_numeric(frame["final_score"], errors="coerce")
9797
frame["in_universe"] = frame["in_universe"].astype(str).str.lower().isin({"true", "1"})
98-
frame = frame.dropna(subset=["date", "symbol", "open", "final_score"])
98+
frame = frame.dropna(subset=["date", "symbol", "open"])
9999
if frame.duplicated(["date", "symbol"]).any():
100100
raise ValueError("research panel contains duplicate date/symbol rows")
101101
return frame.set_index(["date", "symbol"]).sort_index()
@@ -137,18 +137,23 @@ def _shared_inputs(
137137
normalized_panel = _normalize_panel(panel)
138138
panel_dates = normalized_panel.index.get_level_values("date")
139139
normalized_panel = normalized_panel.loc[
140-
(panel_dates >= pd.Timestamp(full_start)) & (panel_dates <= pd.Timestamp(full_end))
140+
panel_dates >= pd.Timestamp(full_start)
141141
]
142142
if normalized_panel.empty or normalized_panel.index.get_level_values("date").max() < pd.Timestamp(full_end) - pd.Timedelta(days=2):
143143
raise ValueError("research panel does not cover the latest walk-forward window")
144-
if normalized_panel.groupby(level="date")["in_universe"].sum().min() < 2:
144+
scored_panel = normalized_panel.dropna(subset=["final_score"])
145+
if scored_panel.groupby(level="date")["in_universe"].sum().min() < 2:
145146
raise ValueError("research panel requires at least two in-universe symbols")
146147

147148
normalized_history = _normalize_market_history(market_history)
148149
lookback_start = pd.Timestamp(full_start) - pd.Timedelta(days=COMBO_DEFAULT_MIN_HISTORY_DAYS + 5)
150+
current_end = min(
151+
normalized_panel.index.get_level_values("date").max(),
152+
normalized_history["date"].max(),
153+
)
149154
normalized_history = normalized_history.loc[
150155
(normalized_history["date"] >= lookback_start)
151-
& (normalized_history["date"] <= pd.Timestamp(full_end))
156+
& (normalized_history["date"] <= current_end)
152157
].copy()
153158
required_symbols = {"BTCUSDT", "ETHUSDT"}
154159
missing_symbols = sorted(required_symbols - set(normalized_history["symbol"]))
@@ -186,21 +191,21 @@ def _write_return_matrix(
186191

187192
def _baseline_from_return_tail(full_result: Any, returns: pd.Series) -> Any:
188193
tail = returns.tail(DRIFT_BASELINE_HORIZON_DAYS)
189-
metrics = _performance_metrics(tail)
190-
max_drawdown = float(metrics["Max Drawdown"])
191-
cagr = float(metrics["CAGR"])
194+
metrics = compute_window_metrics(tail, window_days=DRIFT_BASELINE_HORIZON_DAYS)
195+
max_drawdown = float(metrics.max_drawdown)
196+
cagr = float(metrics.cagr)
192197
return replace(
193198
full_result,
194-
sharpe_ratio=float(metrics["Sharpe"]),
195-
calmar_ratio=abs(cagr / max_drawdown) if max_drawdown else None,
199+
sharpe_ratio=float(metrics.sharpe_ratio),
200+
calmar_ratio=float(metrics.calmar_ratio),
196201
max_drawdown=max_drawdown,
197202
cagr=cagr,
198-
volatility=float(metrics["Annualized Volatility"]),
199-
win_rate=float(metrics["Win Rate"]),
200-
total_return=float(metrics["total_return"]),
201-
start_date=tail.index.min().date(),
202-
end_date=tail.index.max().date(),
203-
observation_count=int(metrics["Trading Days"]),
203+
volatility=float(metrics.volatility),
204+
win_rate=float(metrics.win_rate),
205+
total_return=float(metrics.total_return),
206+
start_date=metrics.start_date,
207+
end_date=metrics.end_date,
208+
observation_count=metrics.observation_count,
204209
)
205210

206211

@@ -273,10 +278,26 @@ def run_walk_forward(
273278
if returns_output is not None:
274279
if shared_market_history is None:
275280
raise ValueError("returns_output requires market_history")
281+
current_end = min(
282+
shared_panel.index.get_level_values("date").max(),
283+
shared_market_history["date"].max(),
284+
).date()
285+
current_runner = _build_runner(
286+
profile=profile,
287+
panel=shared_panel,
288+
market_history=shared_market_history,
289+
synthetic_days=synthetic_days,
290+
)
291+
current_runner.run(
292+
profile,
293+
copy.deepcopy(params),
294+
start_date=min(start for start, _ in windows),
295+
end_date=current_end,
296+
)
276297
_write_return_matrix(
277298
returns_output,
278299
profile=profile,
279-
returns=full_window_returns,
300+
returns=current_runner.last_daily_returns,
280301
market_history=shared_market_history,
281302
)
282303
return {

tests/test_run_walk_forward_backtest.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def test_run_walk_forward_uses_real_panel_and_writes_return_matrix(
8888
tmp_path: Path,
8989
monkeypatch: pytest.MonkeyPatch,
9090
) -> None:
91-
dates = pd.date_range("2022-01-01", "2024-12-31", freq="D")
91+
dates = pd.date_range("2022-01-01", "2025-02-28", freq="D")
9292
symbols = ("BTCUSDT", "ETHUSDT", "SOLUSDT")
9393
rows = []
9494
market_rows = []
@@ -131,7 +131,7 @@ def test_run_walk_forward_uses_real_panel_and_writes_return_matrix(
131131
assert payload["baseline"]["observation_count"] == 126
132132
assert {"as_of", "crypto_live_pool_rotation", "buy_hold_BTC"} <= set(return_matrix.columns)
133133
assert len(return_matrix) > payload["baseline"]["observation_count"]
134-
assert len(return_matrix) == sum(item["observation_count"] for item in payload["walk_forward_folds"])
134+
assert pd.Timestamp(return_matrix["as_of"].max()) > pd.Timestamp("2024-12-31")
135135

136136

137137
def test_baseline_uses_exact_tail_of_full_return_stream() -> None:
@@ -169,3 +169,22 @@ def test_external_inputs_reject_duplicate_keys() -> None:
169169
_normalize_panel(duplicate_panel)
170170
with pytest.raises(ValueError, match="market history contains duplicate"):
171171
_normalize_market_history(duplicate_history)
172+
173+
174+
def test_normalized_panel_preserves_unscored_open_rows() -> None:
175+
panel = pd.DataFrame(
176+
[
177+
{
178+
"date": "2024-01-01",
179+
"symbol": "BTCUSDT",
180+
"in_universe": False,
181+
"open": 100.0,
182+
"final_score": None,
183+
}
184+
]
185+
)
186+
187+
normalized = _normalize_panel(panel)
188+
189+
assert len(normalized) == 1
190+
assert pd.isna(normalized.iloc[0]["final_score"])

0 commit comments

Comments
 (0)