fix(ml): wire spread_cost_bps into all training examples, remove dead code

- Add tx_cost_bps/tick_size/spread_ticks CLI args to tggn, xlstm, diffusion
- Subtract spread + commission from target returns during data prep
- Delete unused validate_model_parameters from train_mamba2_dbn
- Wire validate_training_batch into mamba2 pre-training validation

All 16 training examples now compile with zero warnings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-02-25 22:30:24 +01:00
parent 6c59e16cd6
commit 9729395073
4 changed files with 103 additions and 57 deletions

View File

@@ -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<Vec<f32>> {
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<Vec<f32>> {
if bars.len() < seq_len + 1 {
return Vec::new();
}
let closes: Vec<f64> = 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<f64> = 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<f64> = closes
let window: Vec<f64> = 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<Vec<f32>>, 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 {}.",

View File

@@ -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(())
}

View File

@@ -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>) -> [f64; NOD
fn build_dataset(
bars: &[ml::types::OHLCVBar],
device: &Device,
tx_cost_bps: f64,
tick_size: f64,
spread_ticks: f64,
) -> Result<Vec<(Tensor, Tensor)>> {
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"),

View File

@@ -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<Vec<(Tensor, Tensor)>> {
// 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());
}