Files
foxhunt/scripts/alpha_pipeline.sh
jgrusewski 34586dad68 refactor(alpha_baseline): rename, drop conditionals, strip dead paths
Rename binary alpha_compose_backtest → alpha_baseline and remove the
boolean flags whose features are now mandatory:

  --c51            (always C51 distributional Q)
  --temporal       (always Mamba2 temporal encoder)
  --isv-continual  (controller always fires per eval episode)
  --regime-scale   (vol-EMA regime defense always on)
  --pruned-actions (FALSIFIED 2026-05-15 per pearl_action_pruning_falsified)

Every dependent code path was stripped, not just gated:

- Linear-Q kernels (lq_fwd, lq_grad, munch_kernel) and their cubin loads
  are gone — C51 is the only Q-network.
- Single-env push_kernel / h_store_kernel loads removed; the backtest
  has been batched-parallel-env since T14 and only the _batched
  variants are called here. (The smoke binary still uses single-env
  variants because one env per episode is its job.)
- Dead transition buffers removed: states_dev, next_states_dev,
  actions_dev, rewards_dev, dones_dev, q_current_dev, q_next_dev,
  target_dev, single_state_dev, single_q_dev, probs_current_dev,
  probs_next_dev, m_dev, single_probs_dev, single-env state_pinned,
  action_pinned, window_tensor, h_enriched_buf_dev.
- Dead constants and helpers: PRUNED_ACTIONS, N_WEIGHTS, N_BIASES,
  epsilon_greedy, epsilon_greedy_gated.

End-to-end verification on the existing Q1 fxcache (rebuild was OOM
locally; full multi-quarter validation is the next phase):

  cost=0.0000  best τ=0.250  Sharpe_ann=+36.83  win=0.984  trades/ep=83.3
  cost=0.0625  best τ=0.250  Sharpe_ann=+38.53  win=0.996  trades/ep=83.2
  cost=0.1250  best τ=0.250  Sharpe_ann=+38.37  win=0.994  trades/ep=83.3
  cost=0.2500  best τ=0.250  Sharpe_ann=+34.24  win=0.990  trades/ep=85.6
  cost=0.5000  best τ=0.250  Sharpe_ann=+31.83  win=0.946  trades/ep=84.8

Numbers track the prior T16-flag config (within stochastic noise),
confirming the conditional-stripping was a pure simplification — no
behavioral change, just a smaller, honester binary.

Also updated:
- scripts/alpha_pipeline.sh — A/B conditions collapse to fixed-cost
  vs cost-randomized training (the only opt-in left).
- scripts/walk_forward_cv.sh — drop legacy flags, pass --window-k only.
- crates/ml/src/env/loaders.rs — module doc-comment updated.

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

182 lines
6.4 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# Alpha pipeline — full 2-quarter validation after fxcache lands.
#
# 1. Locate the freshly-built Q1+Q2 2024 fxcache (newest .fxcache in
# test_data/feature-cache/ that is NOT the existing Q1-only file).
# 2. Train alpha_train_stacker on it → alpha_logits_cache_2q.bin.
# 3. Run 4-condition cross-quarter walk-forward CV:
# A: baseline (trained Mamba2 + C51, no regime scale, fixed cost)
# B: + cost randomization U[0.0625, 0.5]
# C: + vol regime scale only
# D: + both (the combined regime-defence hypothesis)
# 4. Aggregate Sharpe across conditions × folds.
#
# Run from the sp19-20-wr-first worktree root.
set -euo pipefail
REPO=/home/jgrusewski/Work/foxhunt/.worktrees/sp19-20-wr-first
CACHE_DIR=/home/jgrusewski/Work/foxhunt/test_data/feature-cache
FILL=config/ml/alpha_fill_coeffs.json
BIN_BT=./target/release/examples/alpha_baseline
BIN_STACK=./target/release/examples/alpha_train_stacker
cd "$REPO"
# Identify the Q1+Q2 fxcache: newest .fxcache whose hash is NOT the
# known Q1-only one.
Q1_HASH=9297017b6db6795f75e57be8aefb03e45e6427513f42c08e22521e58fa025d5e
NEW_CACHE=$(ls -t "$CACHE_DIR"/*.fxcache 2>/dev/null | grep -v "$Q1_HASH" | head -1)
if [[ -z "$NEW_CACHE" ]]; then
echo "FATAL: no new fxcache found in $CACHE_DIR (only the Q1 one)"
exit 1
fi
echo "Using fxcache: $NEW_CACHE"
# ----- 1. Train alpha_train_stacker → alpha_logits_cache_2q.bin -----
ALPHA_OUT=config/ml/alpha_logits_cache_2q.bin
if [[ ! -f "$ALPHA_OUT" ]]; then
echo "===================================================================="
echo "Stage 1: alpha_train_stacker on $NEW_CACHE"
echo "===================================================================="
SQLX_OFFLINE=true cargo build --release --example alpha_train_stacker -p ml-alpha
SQLX_OFFLINE=true RUST_LOG=info "$BIN_STACK" \
--fxcache-path "$NEW_CACHE" \
--horizon 6000 \
--seq-len 32 \
--hidden-dim 64 \
--state-dim 16 \
--epochs 5 \
--batch-size 128 \
--lr 3e-3 \
--train-frac 0.8 \
--alpha-cache-out "$ALPHA_OUT"
fi
echo "Alpha cache: $ALPHA_OUT"
# ----- 2. Build backtest binary (in case of code changes) -----
SQLX_OFFLINE=true cargo build --release --example alpha_baseline -p ml >/dev/null
# ----- 3. Walk-forward CV across 4 conditions × 3 folds -----
# Fold A: offset=0, window=1.2M → train 0..720K, eval 720K..1.2M
# Fold B: offset=900K, window=1.2M → train 900K..1.62M, eval 1.62M..2.1M
# Fold C: offset=1.8M, window=1.2M → train 1.8M..2.52M, eval 2.52M..3.0M
# (Assumes Q1+Q2 fxcache has ~3-6M bars total at MBP-10 resolution.)
# Cost randomization is the only opt-in left: `--train-cost-hi 0.5` enables
# uniform[0.0625, 0.5] per-epoch cost sampling. C51 / temporal / ISV-continual
# / regime-scale are now always-on properties of alpha_baseline; the A/B
# split is now just fixed-cost vs cost-randomised training.
declare -a CONDS=(
"A_fixed:--window-k 16"
"B_costrand:--window-k 16 --train-cost-hi 0.5"
)
declare -a FOLDS=(
"A:0:1200000"
"B:900000:1200000"
"C:1800000:1200000"
)
mkdir -p /tmp/alpha_cv_results
for cond in "${CONDS[@]}"; do
cname="${cond%%:*}"
cflags="${cond##*:}"
for fold in "${FOLDS[@]}"; do
IFS=':' read -r fname offset window <<<"$fold"
out="/tmp/alpha_cv_results/${cname}_fold-${fname}.json"
if [[ -f "$out" ]]; then
echo "Skip (exists): $out"; continue
fi
echo "===================================================================="
echo "Cond $cname, fold $fname (offset=$offset, window=$window)"
echo "===================================================================="
SQLX_OFFLINE=true RUST_LOG=info "$BIN_BT" \
--fxcache-path "$NEW_CACHE" \
--alpha-cache "$ALPHA_OUT" \
--fill-coeffs "$FILL" \
--data-start-offset "$offset" \
--max-snapshots "$window" \
--train-frac 0.6 \
$cflags \
--out-path "$out"
done
done
# ----- 4. Aggregate -----
echo
echo "===================================================================="
echo "Alpha pipeline cross-quarter walk-forward summary"
echo "===================================================================="
python3 - <<'PY'
import json, os, statistics
conds = ["A_fixed", "B_costrand"]
folds = ["A", "B", "C"]
# For each (cond, cost): list of best Sharpe across folds
by_cell = {}
for cond in conds:
for f in folds:
p = f"/tmp/alpha_cv_results/{cond}_fold-{f}.json"
if not os.path.exists(p):
continue
with open(p) as fh:
j = json.load(fh)
for b in j["bins"]:
c = b["cost"]
key = (cond, c)
best = by_cell.get(key, (-1e9, None, None, None))
if b["sharpe_annualised"] > best[0]:
by_cell[key] = (
b["sharpe_annualised"],
b["threshold"],
b["win_rate"],
f,
)
# Per (cond, cost) — collect across folds for std-dev
fold_sharpes = {} # (cond, cost) -> [Sharpe per fold]
for cond in conds:
for f in folds:
p = f"/tmp/alpha_cv_results/{cond}_fold-{f}.json"
if not os.path.exists(p):
continue
with open(p) 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]:
by_cost[c] = b["sharpe_annualised"]
for c, sa in by_cost.items():
fold_sharpes.setdefault((cond, c), []).append(sa)
costs = sorted({c for (_, c) in fold_sharpes.keys()})
print(f"{'cost':>8} " + " ".join(f"{c:>22}" for c in conds))
for c in costs:
cells = []
for cond in conds:
sas = fold_sharpes.get((cond, c), [])
if not sas:
cells.append(" n/a ")
elif len(sas) == 1:
cells.append(f" {sas[0]:+7.2f} ")
else:
m = statistics.mean(sas)
sd = statistics.stdev(sas)
cells.append(f" {m:+7.2f} ± {sd:5.2f} ")
print(f"{c:>8.4f} " + " ".join(cells))
print()
print("Per-condition aggregate stats (across all folds × costs):")
for cond in conds:
all_sas = []
for c in costs:
all_sas.extend(fold_sharpes.get((cond, c), []))
if not all_sas:
continue
m = statistics.mean(all_sas)
sd = statistics.stdev(all_sas) if len(all_sas) > 1 else 0.0
print(f" {cond:14} mean Sharpe = {m:+7.2f} std = {sd:5.2f} n = {len(all_sas)}")
PY