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>
130 lines
3.7 KiB
Rust
130 lines
3.7 KiB
Rust
//! 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(())
|
||
}
|