import itertools, random import pandas as pd from backtest.engine import run_backtest, WINDOW from backtest.metrics import compute_metrics def _score(m, min_trades): if m["trades"] < min_trades: return float("-inf") return m["return_pct"] / max(m["max_dd_pct"], 1.0) # rendement / drawdown def walk_forward(df, strat_name, base_params, grid, tf, symbol, spec, risk_cfg, bt, wf, log=print): keys = list(grid) combos = [dict(base_params, **dict(zip(keys, v))) for v in itertools.product(*grid.values())] if len(combos) > wf["max_combos"]: combos = random.Random(1).sample(combos, wf["max_combos"]) n, tr_n, te_n = len(df), wf["train_bars"], wf["test_bars"] windows, oos_trades, oos_eq = [], [], [] balance, start, k = bt["initial_balance"], 0, 0 while start + tr_n + te_n <= n: k += 1 a, b, e = start, start + tr_n, start + tr_n + te_n train = df.iloc[a:b].reset_index(drop=True) best, best_s, best_m = None, float("-inf"), None for p in combos: r = run_backtest(train, strat_name, p, tf, symbol, spec, risk_cfg, bt) m = compute_metrics(r["trades"], r["equity"], r["initial"]) s = _score(m, wf["min_trades"]) if s > best_s: best, best_s, best_m = p, s, m if best is None: log(f"Fenêtre {k}: aucune combinaison valide (trop peu de trades), ignorée") start += te_n continue test = df.iloc[b - WINDOW:e].reset_index(drop=True) # warm-up + test r = run_backtest(test, strat_name, best, tf, symbol, spec, risk_cfg, bt, initial=balance, start_idx=WINDOW) m = compute_metrics(r["trades"], r["equity"], r["initial"]) balance = m["final"] oos_trades.append(r["trades"]); oos_eq.append(r["equity"]) windows.append({"k": k, "train_from": str(df.time.iloc[a])[:10], "test_from": str(df.time.iloc[b])[:10], "test_to": str(df.time.iloc[e - 1])[:10], "params": {x: best[x] for x in keys}, "is_return": best_m["return_pct"], "is_dd": best_m["max_dd_pct"], "oos_return": m["return_pct"], "oos_dd": m["max_dd_pct"], "oos_trades": m["trades"]}) log(f"Fenêtre {k}: IS {best_m['return_pct']:+.1f}% -> OOS {m['return_pct']:+.1f}% | {windows[-1]['params']}") start += te_n trades = pd.concat(oos_trades, ignore_index=True) if oos_trades else pd.DataFrame() eq = pd.concat(oos_eq) if oos_eq else pd.Series(dtype=float) metrics = compute_metrics(trades, eq, bt["initial_balance"]) if len(eq) else None wfe = None if windows: is_avg = sum(w["is_return"] for w in windows) / len(windows) oos_avg = sum(w["oos_return"] for w in windows) / len(windows) wfe = (oos_avg / is_avg * 100) if is_avg > 0 else None # >50% = plutôt robuste return {"windows": windows, "trades": trades, "equity": eq, "metrics": metrics, "wfe": wfe}