eval runner
Browse files- 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())
|