|
14 | 14 | from typing import Any |
15 | 15 |
|
16 | 16 | import pandas as pd |
| 17 | +from quant_platform_kit.strategy_lifecycle.performance_metrics import compute_window_metrics |
17 | 18 |
|
18 | 19 | from crypto_strategies.backtest.orchestrator_runner import ( |
19 | 20 | COMBO_DEFAULT_MIN_HISTORY_DAYS, |
|
22 | 23 | SUPPORTED_PROFILES, |
23 | 24 | build_backtest_runner, |
24 | 25 | ) |
25 | | -from crypto_strategies.backtest.live_pool_simulator import _performance_metrics |
26 | 26 | from crypto_strategies.strategies.crypto_equity_combo import PROFILE_NAME as CRYPTO_EQUITY_COMBO_PROFILE |
27 | 27 |
|
28 | 28 | DEFAULT_WINDOWS: tuple[tuple[date, date], ...] = ( |
@@ -95,7 +95,7 @@ def _normalize_panel(panel: pd.DataFrame) -> pd.DataFrame: |
95 | 95 | frame["open"] = pd.to_numeric(frame["open"], errors="coerce") |
96 | 96 | frame["final_score"] = pd.to_numeric(frame["final_score"], errors="coerce") |
97 | 97 | 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"]) |
99 | 99 | if frame.duplicated(["date", "symbol"]).any(): |
100 | 100 | raise ValueError("research panel contains duplicate date/symbol rows") |
101 | 101 | return frame.set_index(["date", "symbol"]).sort_index() |
@@ -137,18 +137,23 @@ def _shared_inputs( |
137 | 137 | normalized_panel = _normalize_panel(panel) |
138 | 138 | panel_dates = normalized_panel.index.get_level_values("date") |
139 | 139 | 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) |
141 | 141 | ] |
142 | 142 | if normalized_panel.empty or normalized_panel.index.get_level_values("date").max() < pd.Timestamp(full_end) - pd.Timedelta(days=2): |
143 | 143 | 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: |
145 | 146 | raise ValueError("research panel requires at least two in-universe symbols") |
146 | 147 |
|
147 | 148 | normalized_history = _normalize_market_history(market_history) |
148 | 149 | 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 | + ) |
149 | 154 | normalized_history = normalized_history.loc[ |
150 | 155 | (normalized_history["date"] >= lookback_start) |
151 | | - & (normalized_history["date"] <= pd.Timestamp(full_end)) |
| 156 | + & (normalized_history["date"] <= current_end) |
152 | 157 | ].copy() |
153 | 158 | required_symbols = {"BTCUSDT", "ETHUSDT"} |
154 | 159 | missing_symbols = sorted(required_symbols - set(normalized_history["symbol"])) |
@@ -186,21 +191,21 @@ def _write_return_matrix( |
186 | 191 |
|
187 | 192 | def _baseline_from_return_tail(full_result: Any, returns: pd.Series) -> Any: |
188 | 193 | 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) |
192 | 197 | return replace( |
193 | 198 | 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), |
196 | 201 | max_drawdown=max_drawdown, |
197 | 202 | 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, |
204 | 209 | ) |
205 | 210 |
|
206 | 211 |
|
@@ -273,10 +278,26 @@ def run_walk_forward( |
273 | 278 | if returns_output is not None: |
274 | 279 | if shared_market_history is None: |
275 | 280 | 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 | + ) |
276 | 297 | _write_return_matrix( |
277 | 298 | returns_output, |
278 | 299 | profile=profile, |
279 | | - returns=full_window_returns, |
| 300 | + returns=current_runner.last_daily_returns, |
280 | 301 | market_history=shared_market_history, |
281 | 302 | ) |
282 | 303 | return { |
|
0 commit comments