diff --git a/ml/tests/dqn_training_smoke_test.rs b/ml/tests/dqn_training_smoke_test.rs index 1f6fc2d4f..941ef2a02 100644 --- a/ml/tests/dqn_training_smoke_test.rs +++ b/ml/tests/dqn_training_smoke_test.rs @@ -1,7 +1,7 @@ //! DQN Training Smoke Test //! //! Verifies the complete train -> checkpoint -> validate pipeline -//! works on real 6E.FUT data with 6 assertions: +//! works on real 6E.FUT data with 7 assertions: //! //! 1. All 20 epochs complete (no premature early stopping) //! 2. Loss decreases >5% (gradient flow works) @@ -9,6 +9,7 @@ //! 4. Q-value divergence (model develops action preferences) //! 5. Checkpoint round-trip (save/load weight integrity) //! 6. Epsilon decayed below 0.5 (exploration schedule ran) +//! 7. Walk-forward validation produces finite Sharpe (pipeline works end-to-end) #![allow(unused_crate_dependencies)] @@ -165,8 +166,119 @@ async fn test_dqn_training_smoke() -> Result<()> { " Training time: {:.1}s", metrics.training_time_seconds ); + // === ASSERT 7: Walk-forward Sharpe validates training produces value === + // Uses DqnStrategy (15-dim, 3-action) with the validation harness to prove + // the walk-forward pipeline works. Since DQNTrainer uses 51-dim/45-action + // architecture, we test the validation pipeline independently with a simpler + // DQN that trains inside the harness vs. random baseline. + use ml::dqn::DQNConfig; + use ml::real_data_loader::RealDataLoader; + use ml::validation::{ + DqnStrategy, TimeSeriesData, ValidationHarness, ValidationHarnessConfig, + WalkForwardConfig, + }; + + let mut loader = RealDataLoader::new_from_workspace()?; + let bars = loader.load_symbol_data("6E.FUT").await?; + + // Build 15-dim features + let feat_matrix = loader.extract_features(&bars)?; + let indicators = loader.calculate_indicators(&bars)?; + let n = bars.len(); + let mut features = Vec::with_capacity(n); + for i in 0..n { + let mut row = Vec::with_capacity(15); + if let Some(price_row) = feat_matrix.prices.get(i) { + row.extend_from_slice(price_row); + } else { + row.extend_from_slice(&[0.0_f32; 5]); + } + let close = bars.get(i).map(|b| b.close as f32).unwrap_or(1.0); + let denom = if close.abs() > 1e-10 { close } else { 1.0 }; + row.push(indicators.rsi.get(i).copied().unwrap_or(50.0) / 100.0); + row.push(indicators.ema_fast.get(i).copied().unwrap_or(0.0) / denom); + row.push(indicators.ema_slow.get(i).copied().unwrap_or(0.0) / denom); + let macd_line = indicators.macd.get(i).copied().unwrap_or(0.0); + let macd_signal = indicators.macd_signal.get(i).copied().unwrap_or(0.0); + row.push(macd_line); + row.push(macd_signal); + row.push(macd_line - macd_signal); + row.push(indicators.bb_upper.get(i).copied().unwrap_or(0.0) / denom); + row.push(indicators.bb_middle.get(i).copied().unwrap_or(0.0) / denom); + row.push(indicators.bb_lower.get(i).copied().unwrap_or(0.0) / denom); + row.push(indicators.atr.get(i).copied().unwrap_or(0.0) / denom); + features.push(row); + } + + let timestamps: Vec> = + bars.iter().map(|b| b.timestamp).collect(); + let prices: Vec = bars.iter().map(|b| b.close).collect(); + let ts_data = TimeSeriesData::new(timestamps, features, prices)?; + + // Configure walk-forward harness + let num_bars = ts_data.len(); + let train_bars = (num_bars / 5).max(200); + let test_bars = (num_bars / 20).max(50); + + let harness_config = ValidationHarnessConfig { + wf_config: WalkForwardConfig { + train_bars, + test_bars, + embargo_bars: 20, + step_bars: test_bars, + min_train_samples: 100, + }, + num_permutations: 100, + num_trials: 1, + seed: 42, + }; + let harness = ValidationHarness::new(harness_config); + + let mut dqn_config = DQNConfig::default(); + dqn_config.state_dim = 15; + dqn_config.num_actions = 3; + dqn_config.hidden_dims = vec![64, 32]; + dqn_config.batch_size = 16; + dqn_config.min_replay_size = 16; + dqn_config.warmup_steps = 0; + dqn_config.use_noisy_nets = false; + dqn_config.use_iqn = false; + dqn_config.use_distributional = false; + dqn_config.use_dueling = true; + dqn_config.use_per = false; + dqn_config.epsilon_start = 0.3; + dqn_config.epsilon_end = 0.01; + + let mut strategy = DqnStrategy::new(dqn_config)?; + let report = harness.validate(&mut strategy, &ts_data)?; + + // The validation harness trains the DQN during walk-forward folds. + // A Sharpe ratio that is finite and produces at least 2 folds proves + // the entire train->validate pipeline works end-to-end. + assert!( + report.aggregate_sharpe.is_finite(), + "ASSERT 7 FAILED: Aggregate Sharpe is not finite: {}", + report.aggregate_sharpe + ); + assert!( + report.num_folds >= 2, + "ASSERT 7 FAILED: Walk-forward produced fewer than 2 folds (got {})", + report.num_folds + ); + + let trained_sharpe = report.aggregate_sharpe; + println!("{}", "=".repeat(70)); println!(" ASSERTIONS 1-6: ALL PASSED"); + println!("{}", "-".repeat(70)); + println!(" ASSERT 7: Walk-Forward Validation"); + println!(" Folds: {}", report.num_folds); + println!(" Aggregate Sharpe: {:.4}", trained_sharpe); + println!(" DSR p-value: {:.4}", report.dsr.pvalue); + println!(" PBO: {:.4}", report.pbo.pbo); + println!(" Verdict: {}", report.verdict); + println!("{}", "=".repeat(70)); + println!(" ALL 7 ASSERTIONS PASSED"); println!("{}", "=".repeat(70)); Ok(())