Files
foxhunt/crates/ml-alpha/examples/phase1a.rs
jgrusewski db874b1841 feat(foxhuntq): Phase 1c snapshot-resolution alpha + leakage fix + variable-dim fxcache
Three things landing atomically because they're load-bearing for each other:

1. **Trend-scanning leakage fix** — trend_scanning.rs was emitting OLS slope+t-stat
   over a *forward* window [t, t+L]. With the Phase 1a label = sign(price[t+60]
   − price[t]), the forward feature window overlaps the label window, contaminating
   it. Purged walk-forward only sterilizes forward-looking *labels* that cross
   the train/val split, not forward-looking *features* that peek inside the same
   horizon the label measures. The leak inflated MLP accuracy from 0.49
   (legacy 74-dim baseline) to 0.75 — vanished to 0.50 after switching to a
   trailing window. Bounded the perfect-fit t-stat sentinel from ±1e6 → ±20
   (p<1e-30 is already meaningless); eliminated the 16k corruption-cap drops.

2. **Variable-dim alpha column** — fxcache schema now carries the alpha-feature
   width via metadata (`alpha_feature_dim`), not a compile-time constant. Same
   on-disk format hosts the 134-dim bar-level stack OR the 81-dim snapshot stack.
   Reader + auto-detect honor the metadata-declared dim; downstream MLP auto-sizes
   `in_dim`. Single schema, no forks.

3. **Snapshot pipeline (Phase 1c falsification)** — `snapshot_pipeline.rs`: 81-dim
   per-MBP10-snapshot extractor reusing 10 snapshot-native alpha blocks + 6 new
   snapshot-specific features (time-since-trade, time-since-snap, event-rate,
   spread-bps, L1-imbalance, microprice-mid drift). `precompute_features` gets
   `--row-unit snapshot` flag; emits one fxcache row per LOB update (1.97M rows
   from MBP-10 data vs 206K for bar mode).

**Smoke verdict on real data** (ES.FUT, 1.97M snapshots, 384K val):
- Bar-level honest alpha: accuracy=0.5005, AUC=0.5043 (no signal)
- **Snapshot-level alpha**: accuracy=0.5241, AUC=0.6849 (real signal, 384K val)
- GBM corroboration: accuracy=0.5401 (non-linear partitioning sees more)
- Horizon decay: alpha peaks at K=20-50 snapshots (~5-25ms), gone by K=500
- Regime-conditional: spread-Q4 quintile hits 0.752 accuracy on 76k samples

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

130 lines
3.7 KiB
Rust
Raw 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.
//! Phase 1a entry-point binary.
//!
//! Loads local fxcache, runs purged walk-forward split, trains a 2-layer MLP
//! on binary direction labels, evaluates validation accuracy, prints the gate
//! verdict.
//!
//! ## Usage
//!
//! Defaults to the local Phase 1a test fxcache:
//! ```
//! cargo run -p ml-alpha --example phase1a --release
//! ```
//!
//! Override config via CLI:
//! ```
//! cargo run -p ml-alpha --example phase1a --release -- \
//! --fxcache-path /path/to/file.fxcache \
//! --horizon 60 \
//! --epochs 5
//! ```
//!
//! Or via TOML config:
//! ```
//! cargo run -p ml-alpha --example phase1a --release -- --config config/foxhuntq-phase1a.toml
//! ```
use std::sync::Arc;
use anyhow::Result;
use clap::Parser;
use cudarc::driver::CudaContext;
use tracing_subscriber::EnvFilter;
use ml_alpha::training::{Phase1aConfig, Phase1aTrainer};
#[derive(Debug, Parser)]
#[command(name = "phase1a", about = "FoxhuntQ-Δ Phase 1a falsification smoke")]
struct Cli {
/// Override fxcache path. Defaults to local test_data fxcache.
#[arg(long)]
fxcache_path: Option<String>,
/// Number of training epochs.
#[arg(long, default_value_t = 5)]
epochs: usize,
/// Batch size for training.
#[arg(long, default_value_t = 1024)]
batch_size: usize,
/// Adam learning rate.
#[arg(long, default_value_t = 1e-3)]
learning_rate: f32,
/// Label horizon in bars (`sign(price[t+H] price[t])`).
#[arg(long, default_value_t = 60)]
horizon: usize,
/// Train fraction for the purged walk-forward split.
#[arg(long, default_value_t = 0.8)]
train_frac: f32,
/// Additional embargo bars beyond `horizon` (Lopez de Prado embargo).
#[arg(long, default_value_t = 0)]
embargo_bars: usize,
/// Hidden dim of the 2-layer MLP.
#[arg(long, default_value_t = 256)]
hidden_dim: usize,
/// Deterministic RNG seed.
#[arg(long, default_value_t = 42)]
seed: u64,
/// Path to a TOML config file (overrides individual flags).
#[arg(long)]
config: Option<String>,
}
fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")))
.init();
let cli = Cli::parse();
if cli.config.is_some() {
// TOML config support deferred to step 3; skeleton uses CLI defaults only.
anyhow::bail!("--config (TOML) not yet supported; use CLI flags");
}
let mut config = Phase1aConfig::default();
// Apply CLI overrides over the loaded / default config.
if let Some(p) = cli.fxcache_path { config.fxcache_path = p; }
config.epochs = cli.epochs;
config.batch_size = cli.batch_size;
config.learning_rate = cli.learning_rate;
config.horizon = cli.horizon;
config.train_frac = cli.train_frac;
config.embargo_bars = cli.embargo_bars;
config.mlp.hidden_dim = cli.hidden_dim;
config.seed = cli.seed;
tracing::info!(?config, "Phase 1a config resolved");
// Initialize CUDA context + stream. RTX 3050 Ti on local; H100/L40S on
// Argo (compute-cap auto-derived by the workflow).
let ctx = CudaContext::new(0)
.map_err(|e| anyhow::anyhow!("CUDA context init: {e}"))?;
let stream = ctx.new_stream()
.map_err(|e| anyhow::anyhow!("CUDA stream init: {e}"))?;
let stream = Arc::new(stream);
let mut trainer = Phase1aTrainer::from_config(config, Arc::clone(&stream))?;
let report = trainer.run()?;
tracing::info!(
accuracy = report.accuracy,
auc = report.auc,
n_samples = report.n_samples,
up_fraction = report.up_fraction,
tied_fraction = report.tied_fraction,
"Phase 1a evaluation complete"
);
println!();
println!("{}", report.verdict_line());
Ok(())
}