diff --git a/crates/ml/examples/train_diffusion_dbn.rs b/crates/ml/examples/train_diffusion_dbn.rs index a36521b0f..3e2de0f12 100644 --- a/crates/ml/examples/train_diffusion_dbn.rs +++ b/crates/ml/examples/train_diffusion_dbn.rs @@ -50,7 +50,7 @@ use ml::types::OHLCVBar; #[allow(unreachable_pub)] mod baseline_common; -use baseline_common::load_all_bars; +use baseline_common::{load_all_bars, spread_cost_bps}; // --------------------------------------------------------------------------- // CLI Arguments @@ -111,29 +111,67 @@ struct Args { /// Early stopping patience (epochs without improvement) #[arg(long, default_value_t = 10)] patience: usize, + + /// Round-trip commission cost in basis points + #[arg(long, default_value_t = 1.0)] + tx_cost_bps: f64, + + /// Instrument tick size in price units (ES=0.25) + #[arg(long, default_value_t = 0.25)] + tick_size: f64, + + /// Typical bid-ask spread in ticks + #[arg(long, default_value_t = 1.0)] + spread_ticks: f64, } // --------------------------------------------------------------------------- // Data preparation // --------------------------------------------------------------------------- -/// Extract normalized close-price sequences from OHLCV bars. +/// Extract normalized net-of-cost return sequences from OHLCV bars. /// /// Returns a vector of f32 sequences, each of length `seq_len`, created by -/// sliding a window over the close prices. Prices are z-score normalized -/// within each window to keep the diffusion model input in a stable range. -fn prepare_sequences(bars: &[OHLCVBar], seq_len: usize) -> Vec> { - if bars.len() < seq_len { +/// sliding a window over bar-to-bar returns minus transaction and spread costs. +/// Returns are z-score normalized within each window for stable training. +fn prepare_sequences( + bars: &[OHLCVBar], + seq_len: usize, + tx_cost_bps: f64, + tick_size: f64, + spread_ticks: f64, +) -> Vec> { + if bars.len() < seq_len + 1 { return Vec::new(); } - let closes: Vec = bars.iter().map(|b| b.close).collect(); - let n_sequences = closes.len().saturating_sub(seq_len); + // Compute net-of-cost returns for each consecutive pair of bars + let mut returns: Vec = Vec::with_capacity(bars.len().saturating_sub(1)); + for i in 0..bars.len().saturating_sub(1) { + let prev = match bars.get(i) { + Some(b) => b, + None => continue, + }; + let cur = match bars.get(i + 1) { + Some(b) => b, + None => continue, + }; + let raw_ret = if prev.close.abs() > 1e-10 { + (cur.close - prev.close) / prev.close + } else { + 0.0 + }; + let spread = spread_cost_bps(prev.close, tick_size, spread_ticks); + let cost = (tx_cost_bps + spread) / 10_000.0; + returns.push(raw_ret - cost); + } + + let n_sequences = returns.len().saturating_sub(seq_len); let mut sequences = Vec::with_capacity(n_sequences); for start in 0..n_sequences { let end = start + seq_len; - let window: Vec = closes + let window: Vec = returns .get(start..end) .map(|s| s.to_vec()) .unwrap_or_default(); @@ -487,7 +525,7 @@ fn load_and_prepare_data(args: &Args) -> Result<(Vec>, usize)> { ); info!("Step 2/4: Preparing training sequences (seq_len={})...", args.seq_len); - let sequences = prepare_sequences(&bars, args.seq_len); + let sequences = prepare_sequences(&bars, args.seq_len, args.tx_cost_bps, args.tick_size, args.spread_ticks); if sequences.is_empty() { anyhow::bail!( "No sequences generated. Need at least {} bars, got {}.", diff --git a/crates/ml/examples/train_mamba2_dbn.rs b/crates/ml/examples/train_mamba2_dbn.rs index 78115f612..6a605345e 100644 --- a/crates/ml/examples/train_mamba2_dbn.rs +++ b/crates/ml/examples/train_mamba2_dbn.rs @@ -558,6 +558,12 @@ async fn main() -> Result<()> { config.seq_len, config.d_model ); + // Validate ALL training and validation sequences before expensive training loop + validate_training_batch(&train_data, config.seq_len, config.d_model) + .context("Training data validation failed")?; + validate_training_batch(&val_data, config.seq_len, config.d_model) + .context("Validation data validation failed")?; + let training_history = model .train(&train_data, &val_data, config.epochs, None) .await @@ -938,40 +944,3 @@ fn validate_training_batch( Ok(()) } -/// Validate model parameter tensors are properly initialized (Agent 201) -/// -/// Checks that all model parameters are: -/// - Contiguous in memory -/// - Non-empty dimensions -/// - Reasonable rank (1D-3D for MAMBA-2) -fn validate_model_parameters(parameters: &[&Tensor]) -> Result<()> { - info!("Validating {} model parameter tensors...", parameters.len()); - - for (idx, param) in parameters.iter().enumerate() { - // Check contiguity - if !param.is_contiguous() { - warn!("⚠ Parameter {} is not contiguous", idx); - } - - // Check for empty tensors - if param.dims().iter().any(|&d| d == 0) { - return Err(anyhow::anyhow!( - "Parameter {} has zero dimension: {:?}", - idx, - param.dims() - )); - } - - // Check tensor rank (should be 1D, 2D, or 3D for MAMBA-2) - let rank = param.dims().len(); - if rank > 3 { - warn!("⚠ Parameter {} has high rank: {}D", idx, rank); - } - } - - info!( - "✓ All {} model parameters validated successfully", - parameters.len() - ); - Ok(()) -} diff --git a/crates/ml/examples/train_tggn_dbn.rs b/crates/ml/examples/train_tggn_dbn.rs index d355fe889..fd40bba84 100644 --- a/crates/ml/examples/train_tggn_dbn.rs +++ b/crates/ml/examples/train_tggn_dbn.rs @@ -49,7 +49,7 @@ use ml::training::unified_trainer::UnifiedTrainable; #[allow(unreachable_pub)] mod baseline_common; -use baseline_common::load_all_bars; +use baseline_common::{load_all_bars, spread_cost_bps}; // --------------------------------------------------------------------------- // CLI Arguments @@ -86,6 +86,18 @@ struct Args { /// Output directory for checkpoints #[arg(long, default_value = "ml/trained_models/tggn")] output_dir: PathBuf, + + /// Round-trip commission cost in basis points + #[arg(long, default_value_t = 1.0)] + tx_cost_bps: f64, + + /// Instrument tick size in price units (ES=0.25) + #[arg(long, default_value_t = 0.25)] + tick_size: f64, + + /// Typical bid-ask spread in ticks + #[arg(long, default_value_t = 1.0)] + spread_ticks: f64, } // --------------------------------------------------------------------------- @@ -142,6 +154,9 @@ fn bar_features(bar: &ml::types::OHLCVBar, prev_close: Option) -> [f64; NOD fn build_dataset( bars: &[ml::types::OHLCVBar], device: &Device, + tx_cost_bps: f64, + tick_size: f64, + spread_ticks: f64, ) -> Result> { if bars.len() < 3 { anyhow::bail!("Need at least 3 bars to build training data, got {}", bars.len()); @@ -166,12 +181,15 @@ fn build_dataset( let feats = bar_features(cur_bar, prev_close); let cur_close = cur_bar.close; - // Target: next-bar log return - let target = if cur_close.abs() > 1e-12 { + // Target: next-bar log return minus transaction and spread costs (in log-return units) + let raw_return = if cur_close.abs() > 1e-12 { (next_bar.close / cur_close).ln() } else { 0.0 }; + let spread = spread_cost_bps(cur_close, tick_size, spread_ticks); + let cost = (tx_cost_bps + spread) / 10_000.0; // convert bps to fractional + let target = raw_return - cost; inputs.push(feats); targets.push(target); @@ -280,7 +298,7 @@ fn main() -> Result<()> { // Build datasets // ----------------------------------------------------------------------- info!("Building training dataset ..."); - let train_data = build_dataset(train_bars, &device)?; + let train_data = build_dataset(train_bars, &device, args.tx_cost_bps, args.tick_size, args.spread_ticks)?; let (train_inputs, train_targets) = match train_data.first() { Some(pair) => pair, None => anyhow::bail!("Empty training dataset"), @@ -293,7 +311,7 @@ fn main() -> Result<()> { info!("Training samples: {}", n_train); info!("Building validation dataset ..."); - let val_data = build_dataset(val_bars, &device)?; + let val_data = build_dataset(val_bars, &device, args.tx_cost_bps, args.tick_size, args.spread_ticks)?; let (val_inputs, val_targets) = match val_data.first() { Some(pair) => pair, None => anyhow::bail!("Empty validation dataset"), diff --git a/crates/ml/examples/train_xlstm_dbn.rs b/crates/ml/examples/train_xlstm_dbn.rs index 11c6c723c..52760f5f7 100644 --- a/crates/ml/examples/train_xlstm_dbn.rs +++ b/crates/ml/examples/train_xlstm_dbn.rs @@ -52,7 +52,7 @@ use ml::xlstm::{XLSTMConfig, XLSTMTrainableAdapter}; #[allow(unreachable_pub)] mod baseline_common; -use baseline_common::load_all_bars; +use baseline_common::{load_all_bars, spread_cost_bps}; // --------------------------------------------------------------------------- // CLI Arguments @@ -130,6 +130,18 @@ struct Args { /// Early stopping patience (epochs without improvement) #[arg(long, default_value_t = 10)] patience: usize, + + /// Round-trip commission cost in basis points + #[arg(long, default_value_t = 1.0)] + tx_cost_bps: f64, + + /// Instrument tick size in price units (ES=0.25) + #[arg(long, default_value_t = 0.25)] + tick_size: f64, + + /// Typical bid-ask spread in ticks + #[arg(long, default_value_t = 1.0)] + spread_ticks: f64, } // --------------------------------------------------------------------------- @@ -152,6 +164,9 @@ fn build_sequences( seq_len: usize, max_steps: usize, device: &Device, + tx_cost_bps: f64, + tick_size: f64, + spread_ticks: f64, ) -> Result> { // Extract 51-dim features (consumes a warmup period of ~50 bars) let features = extract_ml_features(bars).context("Feature extraction failed")?; @@ -197,11 +212,14 @@ fn build_sequences( let close_prev = bars.get(target_bar_idx.saturating_sub(1)).map(|b| b.close).unwrap_or(1.0); let close_cur = bars.get(target_bar_idx).map(|b| b.close).unwrap_or(close_prev); - let ret = if close_prev.abs() > 1e-10 { - ((close_cur - close_prev) / close_prev).clamp(-0.05, 0.05) as f32 + let raw_ret = if close_prev.abs() > 1e-10 { + ((close_cur - close_prev) / close_prev).clamp(-0.05, 0.05) } else { - 0.0_f32 + 0.0 }; + let spread = spread_cost_bps(close_prev, tick_size, spread_ticks); + let cost = (tx_cost_bps + spread) / 10_000.0; // convert bps to fractional + let ret = (raw_ret - cost) as f32; // Only add if we built the full window if input_data.len() == seq_len * FEATURE_DIM { @@ -304,7 +322,10 @@ fn main() -> Result<()> { // ---- Build sequences ---- info!("Step 2/5: Building input/target sequences (seq_len={})...", args.seq_len); - let all_sequences = build_sequences(&all_bars, args.seq_len, args.max_steps_per_epoch, &device)?; + let all_sequences = build_sequences( + &all_bars, args.seq_len, args.max_steps_per_epoch, &device, + args.tx_cost_bps, args.tick_size, args.spread_ticks, + )?; if all_sequences.is_empty() { anyhow::bail!("No sequences produced from {} bars", all_bars.len()); }