Files
foxhunt/scripts/walk_forward_cv.sh
jgrusewski 3aef276255 feat(phase-e-4-a): walk-forward CV via --data-start-offset
Adds a sliding-window walk-forward harness for the T10 backtest:

- New load_snapshots_from_fxcache_at(start_offset, ...) loader variant
  reads bars [start_offset..start_offset+max_snapshots) from the fxcache.
  Alpha-cache lookups use absolute bar indices, so the same
  alpha_logits_cache.bin works across folds.
- New --data-start-offset CLI flag on alpha_compose_backtest.
- scripts/walk_forward_cv.sh runs 3 folds (window=700K, train_frac=0.6)
  at offsets 0 / 600K / 1.2M, producing /tmp/cv_fold_{A,B,C}.json plus
  an aggregated mean±stddev Sharpe table across folds.

Walk-forward result (alpha_logits_cache trained on bars 0..1.57M, so
fold C eval is fully past the stacker cut):

  cost     fold-A  fold-B  fold-C   mean ± stddev
  0.0000   +91.52  -21.44  +46.74   +38.94 ± 56.88
  0.0625   +84.94  -27.97  +38.42   +31.79 ± 56.74
  0.1250   +79.91  -31.22  +33.51   +27.40 ± 55.82
  0.2500   +72.77  -45.41  +15.16   +14.17 ± 59.09
  0.5000   +50.52  -59.82  -12.75    -7.35 ± 55.37

Fold B (mid-quarter, bars 600K..1.3M) is a disaster — win rate
collapses to 0-22% across all costs. Folds A and C succeed strongly.
Cross-fold SD ≈ mean, so the policy is regime-dependent and cannot
be reliably deployed without regime detection.

Mean Sharpe at half-tick (+27.40) is still ~7× the stateless
Phase 1d.4 baseline (-4.0), so the temporal encoder adds real value
on average — but the single-window +62 OOS celebrated earlier was
a cherry-picked favorable regime, not a deployment-ready result.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 00:02:24 +02:00

80 lines
2.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# Walk-forward CV for the Phase E.4.A.T10 compose backtest.
#
# Slides a fixed-size window across the fxcache. Each fold trains DQN
# from scratch on the front of the window and evaluates on the back.
# Fold C is the cleanest stacker-OOS test (eval bars 1.62M..1.9M lie
# entirely past the stacker training cut at 1.57M).
#
# Outputs per-fold JSON to /tmp/cv_fold_*.json. Run from repo root.
set -euo pipefail
FXCACHE="${FXCACHE:-/home/jgrusewski/Work/foxhunt/test_data/feature-cache/9297017b6db6795f75e57be8aefb03e45e6427513f42c08e22521e58fa025d5e.fxcache}"
ALPHA="${ALPHA:-config/ml/alpha_logits_cache.bin}"
FILL="${FILL:-config/ml/alpha_fill_coeffs.json}"
BIN="${BIN:-./target/release/examples/alpha_compose_backtest}"
WINDOW=700000
TRAIN_FRAC=0.6 # 420K train, 280K eval per fold
declare -a FOLDS=(
"A:0"
"B:600000"
"C:1200000"
)
for fold_spec in "${FOLDS[@]}"; do
name="${fold_spec%%:*}"
offset="${fold_spec##*:}"
out="/tmp/cv_fold_${name}.json"
echo "===================================================================="
echo "Fold ${name}: offset=${offset} window=${WINDOW} train_frac=${TRAIN_FRAC}"
echo "===================================================================="
SQLX_OFFLINE=true RUST_LOG=info "$BIN" \
--fxcache-path "$FXCACHE" \
--alpha-cache "$ALPHA" \
--fill-coeffs "$FILL" \
--data-start-offset "$offset" \
--max-snapshots "$WINDOW" \
--train-frac "$TRAIN_FRAC" \
--c51 --temporal --window-k 16 --isv-continual \
--out-path "$out"
done
echo
echo "===================================================================="
echo "Walk-forward summary (best Sharpe_ann per cost, per fold)"
echo "===================================================================="
python3 - <<'PY'
import json, statistics
folds = ["A", "B", "C"]
rows = []
for f in folds:
with open(f"/tmp/cv_fold_{f}.json") as fh:
j = json.load(fh)
by_cost = {}
for b in j["bins"]:
c = b["cost"]
if c not in by_cost or b["sharpe_annualised"] > by_cost[c][1]:
by_cost[c] = (b["threshold"], b["sharpe_annualised"], b["win_rate"], b["avg_n_trades"])
rows.append((f, by_cost))
costs = sorted(rows[0][1].keys())
print(f"{'cost':>8} " + " ".join(f"fold-{f}" for f, _ in rows))
for c in costs:
cells = []
for _, by_cost in rows:
tau, sa, wr, tp = by_cost[c]
cells.append(f"τ={tau:.2f} S={sa:+6.2f} ({wr*100:.0f}%/{tp:.0f})")
print(f"{c:>8.4f} " + " ".join(cells))
print()
print("Per-cost mean Sharpe_ann ± stddev across folds:")
for c in costs:
sas = [by_cost[c][1] for _, by_cost in rows]
m = statistics.mean(sas)
sd = statistics.stdev(sas) if len(sas) > 1 else 0.0
print(f" cost={c:.4f} mean Sharpe_ann = {m:+7.2f} ± {sd:5.2f}")
PY