Avi-L commited on
Commit
bdc6050
·
verified ·
1 Parent(s): caea0d1

daily panel sweep runner (66 trials, per-config CPCV)

Browse files
Files changed (1) hide show
  1. sweep2.py +171 -0
sweep2.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Panel sweep v2: daily multi-asset strategies through the eval gates.
2
+
3
+ Every configuration is a counted trial. Reports PBO across the grid,
4
+ DSR (n_trials = grid size) on the best config, and CPCV OOS for the top-K.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+ import json
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ import numpy as np
14
+ import pandas as pd
15
+
16
+ from crypto_eval.engine2 import run_panel_backtest
17
+ from crypto_eval.grid2 import (
18
+ buy_and_hold_btc, donchian_panel, xs_momentum,
19
+ xs_momentum_tsmom_filter, tsmom_panel,
20
+ )
21
+ from crypto_eval.cv import make_cpcv
22
+ from crypto_eval.stats import eval_backtest, eval_trial_set
23
+
24
+ PANEL_META_DEFAULT_ADV = {
25
+ "BTC": 20e9, "ETH": 10e9, "LTC": 1.0e9, "XRP": 1.5e9,
26
+ "ADA": 0.5e9, "DOGE": 1.0e9, "ETC": 0.3e9, "DASH": 0.1e9,
27
+ }
28
+
29
+
30
+ def build_grid() -> dict:
31
+ """Returns {name: (fn, kwargs, subset_cols or None)}. subset restricts
32
+ the panel to one asset (single-asset configs count as trials too)."""
33
+ g: dict = {}
34
+ for lb in (10, 20, 50, 100, 200, 400):
35
+ for lo in (False, True):
36
+ for vt in (None, 0.30):
37
+ g[f"tsmom_lb{lb}_{'lo' if lo else 'ls'}_vt{vt}"] = (
38
+ tsmom_panel, dict(lookback=lb, long_only=lo, vol_target=vt), None)
39
+ for w in (20, 55):
40
+ for lo in (False, True):
41
+ for vt in (None, 0.30):
42
+ g[f"donchian_w{w}_{'lo' if lo else 'ls'}_vt{vt}"] = (
43
+ donchian_panel, dict(window=w, long_only=lo, vol_target=vt), None)
44
+ for lb in (30, 90, 180):
45
+ for k in (2, 3):
46
+ for sb in (False, True):
47
+ g[f"xs_lb{lb}_k{k}_{'sb' if sb else 'lo'}"] = (
48
+ xs_momentum, dict(lookback=lb, top_k=k, short_bottom=sb), None)
49
+ g[f"xs_lb{lb}_k{k}_tfilt"] = (
50
+ xs_momentum_tsmom_filter, dict(lookback=lb, top_k=k), None)
51
+ # single-asset long-only trend (era-stability family)
52
+ for sym in ("BTC", "ETH"):
53
+ for lb in (20, 50, 100, 200):
54
+ for fam, fn in (("donch", donchian_panel), ("tsmom", tsmom_panel)):
55
+ kw = ({"window": lb} if fam == "donch" else {"lookback": lb})
56
+ kw["long_only"] = True
57
+ g[f"{sym}_{fam}_lb{lb}_lo"] = (fn, kw, [sym])
58
+ return g
59
+
60
+
61
+ def main() -> int:
62
+ ap = argparse.ArgumentParser()
63
+ ap.add_argument("--panel", required=True, help="panel_daily_close.parquet")
64
+ ap.add_argument("--out", default="sweep2_report.json")
65
+ ap.add_argument("--top-k", type=int, default=5)
66
+ ap.add_argument("--max-configs", type=int, default=0)
67
+ args = ap.parse_args()
68
+
69
+ panel = pd.read_parquet(args.panel)
70
+ panel.index = pd.to_datetime(panel.index, utc=True)
71
+ panel = panel.sort_index()
72
+ start = "2014-01-01" # >= 4 assets listed
73
+ panel = panel[panel.index >= start]
74
+ print(f"[sweep2] panel {panel.shape[0]:,} days x {panel.shape[1]} assets "
75
+ f"from {panel.index[0].date()}", flush=True)
76
+
77
+ grid = build_grid()
78
+ if args.max_configs:
79
+ grid = dict(list(grid.items())[: args.max_configs])
80
+ print(f"[sweep2] {len(grid)} configurations", flush=True)
81
+
82
+ returns, failures = {}, {}
83
+ for i, (name, (fn, kw, subset)) in enumerate(grid.items()):
84
+ try:
85
+ p = panel[subset] if subset else panel
86
+ adv = {s: PANEL_META_DEFAULT_ADV[s] for s in p.columns}
87
+ pos = fn(p, **kw)
88
+ bt = run_panel_backtest(p, pos, adv)
89
+ returns[name] = bt["portfolio_return"].to_numpy()
90
+ except Exception as e:
91
+ failures[name] = repr(e)
92
+ print(f"[sweep2] CONFIG FAILED {name}: {e}", flush=True)
93
+ if (i + 1) % 10 == 0:
94
+ print(f"[sweep2] {i+1}/{len(grid)} done", flush=True)
95
+ if failures:
96
+ print(f"[sweep2] {len(failures)} configs failed", flush=True)
97
+ n_trials = len(returns)
98
+
99
+ sr = {n: float(np.nanmean(r) / (np.nanstd(r, ddof=1) + 1e-12))
100
+ for n, r in returns.items()}
101
+ ranked = sorted(sr, key=sr.get, reverse=True)
102
+ print(f"[sweep2] best per-day SR: {ranked[0]} = {sr[ranked[0]]:.5f} "
103
+ f"(ann {sr[ranked[0]]*np.sqrt(365):.2f})", flush=True)
104
+
105
+ print("[sweep2] computing PBO ...", flush=True)
106
+ trials = eval_trial_set([returns[n] for n in ranked])
107
+ best = ranked[0]
108
+ best_stats = eval_backtest(
109
+ returns[best], n_trials=n_trials,
110
+ trial_sr_variance=trials["trial_sr_variance"],
111
+ periods_per_year=365,
112
+ )
113
+ print(f"[sweep2] PBO={trials['probability_of_backtest_overfitting']:.3f} "
114
+ f"DSR={best_stats['deflated_sharpe']:.4f}", flush=True)
115
+
116
+ # CPCV OOS for top-K (daily bars, horizon 1, embargo 5 days)
117
+ # CPCV audit on the full panel (per-config combos built on demand)
118
+ _, _, audit = make_cpcv(panel.index, horizon=1, embargo=5, freq="1D")
119
+ print(f"[sweep2] CPCV audit {'OK' if audit.ok else 'FAILED'}", flush=True)
120
+ oos_top = {}
121
+ for name in ranked[: args.top_k]:
122
+ fn, kw, subset = grid[name]
123
+ p = panel[subset] if subset else panel
124
+ adv = {s: PANEL_META_DEFAULT_ADV[s] for s in p.columns}
125
+ pos = fn(p, **kw)
126
+ bt = run_panel_backtest(p, pos, adv)
127
+ ret_series = bt["portfolio_return"]
128
+ rets = ret_series.to_numpy()
129
+ # CPCV on the config's OWN history (single-asset configs start at
130
+ # the asset's listing date, shorter than the panel)
131
+ _, combos_k, audit_k = make_cpcv(ret_series.index, horizon=1,
132
+ embargo=5, freq="1D")
133
+ if not audit_k.ok:
134
+ raise RuntimeError(f"OOS audit failed for {name}")
135
+ per = [eval_backtest(rets[t], n_trials=1, trial_sr_variance=0.0,
136
+ periods_per_year=365) for _, t in combos_k]
137
+ srs = [c["sharpe_annualized"] for c in per]
138
+ oos_top[name] = {
139
+ "oos_mean_sharpe_annualized": float(np.mean(srs)),
140
+ "n_combos_positive": int(sum(1 for s in srs if s > 0)),
141
+ "net_total_pnl": float(np.nansum(rets) * 1_000_000),
142
+ }
143
+ print(f"[sweep2] OOS {name}: mean SR {np.mean(srs):.2f}", flush=True)
144
+
145
+ report = {
146
+ "panel_start": str(panel.index[0].date()),
147
+ "n_days": len(panel), "n_assets": panel.shape[1],
148
+ "n_trials": n_trials, "n_configs_failed": len(failures),
149
+ "config_failures": failures,
150
+ "grid": trials,
151
+ "best_config": best,
152
+ "best_config_stats": best_stats,
153
+ "top_k_oos": oos_top,
154
+ "all_configs_ranked": {n: {"sharpe_per_day": sr[n],
155
+ "sharpe_annualized": sr[n] * np.sqrt(365)}
156
+ for n in ranked},
157
+ "decision_rule": "accept only if DSR > 0.95 (n_trials=grid) and "
158
+ "PBO <= 0.5 and leakage audit OK",
159
+ }
160
+ Path(args.out).write_text(json.dumps(report, indent=2, default=str))
161
+ passed = (best_stats["deflated_sharpe"] is not None
162
+ and best_stats["deflated_sharpe"] > 0.95
163
+ and trials["probability_of_backtest_overfitting"] <= 0.5)
164
+ print(f"[sweep2] VERDICT: {'PASS' if passed else 'FAIL'} "
165
+ f"(DSR={best_stats['deflated_sharpe']:.4f}, "
166
+ f"PBO={trials['probability_of_backtest_overfitting']:.3f})", flush=True)
167
+ return 0
168
+
169
+
170
+ if __name__ == "__main__":
171
+ sys.exit(main())