Avi-L commited on
Commit
d8dfb8b
·
verified ·
1 Parent(s): cb40d27

eval runner

Browse files
Files changed (1) hide show
  1. run_eval.py +115 -0
run_eval.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Run the institutional eval on a strategy.
2
+
3
+ Usage:
4
+ python run_eval.py --data /path/to/curated --symbol BTCUSDT --freq 1h \
5
+ --strategy funding_carry --cpcv-blocks 6 --cpcv-k 2
6
+
7
+ Pipeline:
8
+ 1. load curated data, assert schema and no-lookahead invariants
9
+ 2. CPCV (N=6, K=2 default) with purge=label horizon, embargo=8 bars
10
+ 3. for each combination: train tuning-free strategy, test OOS
11
+ 4. reconstruct paths, report PSR/DSR/PBO/MinBTL + raw risk metrics
12
+ 5. record the source-hash manifest with every run
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import json
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ import numpy as np
23
+
24
+ from crypto_eval.data import load_local, load_manifest
25
+ from crypto_eval.engine import run_backtest
26
+ from crypto_eval.strategies import STRATEGIES
27
+ from crypto_eval.cv import make_cpcv, make_walkforward
28
+ from crypto_eval.stats import eval_backtest, eval_trial_set
29
+
30
+ PERIODS_PER_YEAR = { # bars per year by freq
31
+ "1h": 24 * 365, "4h": 6 * 365, "1d": 365, "1m": 60 * 24 * 365,
32
+ }
33
+
34
+
35
+ def main() -> int:
36
+ ap = argparse.ArgumentParser()
37
+ ap.add_argument("--data", required=True)
38
+ ap.add_argument("--symbol", default="BTCUSDT")
39
+ ap.add_argument("--freq", default="1h")
40
+ ap.add_argument("--strategy", default="funding_carry")
41
+ ap.add_argument("--horizon", type=int, default=8,
42
+ help="label horizon in bars (default 8 = one funding interval)")
43
+ ap.add_argument("--embargo", type=int, default=8)
44
+ ap.add_argument("--cpcv-blocks", type=int, default=6)
45
+ ap.add_argument("--cpcv-k", type=int, default=2)
46
+ ap.add_argument("--out", default="eval_report.json")
47
+ args = ap.parse_args()
48
+
49
+ data = load_local(args.data, args.symbol, args.freq)
50
+ manifest = load_manifest(args.data)
51
+ strategy_fn = STRATEGIES[args.strategy]
52
+
53
+ n = len(data.bars)
54
+ print(f"[eval] {args.symbol} {args.freq}: {n:,} bars, "
55
+ f"{data.check_funding_coverage(args.freq):,} funding ticks in range")
56
+
57
+ splitter, combos, audit = make_cpcv(
58
+ data.bars.index, horizon=args.horizon, embargo=args.embargo,
59
+ freq=args.freq, n_blocks=args.cpcv_blocks, k=args.cpcv_k,
60
+ )
61
+ print(f"[eval] CPCV: {len(combos)} combinations, "
62
+ f"{len(combos) * args.cpcv_blocks // args.cpcv_k} paths, audit OK")
63
+
64
+ # strategies here are tuning-free; run OOS per combination
65
+ positions = strategy_fn(data.bars, data.funding)
66
+ bt = run_backtest(data.bars, positions, data.funding)
67
+
68
+ oos_returns = []
69
+ for j, (train_idx, test_idx) in enumerate(combos):
70
+ # with a tuning-free strategy, train is unused; the test slice is
71
+ # the OOS segment. With tuned strategies, tune on train ONLY.
72
+ rets = bt["net_return"].to_numpy()[test_idx]
73
+ oos_returns.append(rets)
74
+
75
+ ppy = PERIODS_PER_YEAR[args.freq]
76
+ trial_stats = eval_trial_set(oos_returns)
77
+ per_combo = []
78
+ for j, rets in enumerate(oos_returns):
79
+ s = eval_backtest(
80
+ rets, n_trials=1, trial_sr_variance=0.0,
81
+ periods_per_year=ppy,
82
+ )
83
+ per_combo.append(s)
84
+
85
+ report = {
86
+ "symbol": args.symbol,
87
+ "freq": args.freq,
88
+ "strategy": args.strategy,
89
+ "n_bars": n,
90
+ "cpcv": {"n_blocks": args.cpcv_blocks, "k": args.cpcv_k,
91
+ "n_combinations": len(combos)},
92
+ "purge_horizon": args.horizon,
93
+ "embargo": args.embargo,
94
+ "audit": {"ok": audit.ok, "violations": audit.violations},
95
+ "data_manifest_sha256_inputs": sorted(set(manifest["outputs"].keys())),
96
+ "costs": {
97
+ "total_fees": bt["total_fees"],
98
+ "total_slippage": bt["total_slippage"],
99
+ "total_funding": bt["total_funding"],
100
+ "net_total_pnl": bt["net_total_pnl"],
101
+ },
102
+ "trials": trial_stats,
103
+ "per_combination": per_combo,
104
+ "decision_rule": "accept only if deflated_sharpe > 0.95 and PBO <= 0.5",
105
+ }
106
+ Path(args.out).write_text(json.dumps(report, indent=2, default=str))
107
+ print(f"[eval] wrote {args.out}")
108
+ print(f"[eval] PBO={trial_stats['probability_of_backtest_overfitting']:.3f} "
109
+ f"DSR(mean combo)="
110
+ f"{np.mean([c['deflated_sharpe'] for c in per_combo if c['deflated_sharpe'] is not None]):.3f}")
111
+ return 0
112
+
113
+
114
+ if __name__ == "__main__":
115
+ sys.exit(main())