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>
182 lines
6.4 KiB
Bash
Executable File
182 lines
6.4 KiB
Bash
Executable File
#!/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
|