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:
@@ -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 {}.",
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user