Files
foxhunt/ml/examples/backtest_dqn.rs
jgrusewski f946dcd952 feat: Wave 2 - Update MEDIUM RISK files (225→54 features)
WAVE 22: All examples, benchmarks, and data loaders updated

Files Modified (41 files):
- DQN examples: 7 files (train_dqn, evaluate_dqn, validate_dqn, etc.)
- PPO examples: 6 files (train_ppo, continuous_ppo, benchmark_ppo, etc.)
- TFT examples: 9 files (train_tft, validate_tft, benchmark_tft, etc.)
- MAMBA-2 examples: 3 files (train_mamba2, verify_dimensions, etc.)
- Benchmarks: 5 files (cuda_speedup, weight_caching, future_decoder, etc.)
- Data loaders: 7 files (parquet_utils, dbn_sequence_loader, tlob_loader, etc.)
- Integration: 4 files (load_parquet_data, streaming loaders, etc.)

Key Changes:
- state_dim: 225 → 54 (DQN, PPO)
- input_dim: 225 → 54 (TFT)
- d_model: 225 → 54 (MAMBA-2)
- Memory: 1.8KB → 0.43KB per vector (76% reduction)
- All tensor shapes updated: (batch, 225) → (batch, 54)

Agents Deployed: 5 parallel agents
Validation: cargo check PASSING

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 00:57:17 +01:00

884 lines
31 KiB
Rust

//! DQN Backtest Validation Script
//!
//! Validates trained DQN checkpoints against production criteria:
//! - Sharpe ratio > 2.0
//! - Win rate > 55%
//! - Max drawdown < 20%
//!
//! # Usage
//!
//! ```bash
//! # Evaluate single checkpoint
//! cargo run -p ml --example backtest_dqn --release --features cuda -- \
//! --checkpoint ml/trained_models/dqn_epoch_5.safetensors \
//! --data test_data/ES_FUT_180d.parquet
//!
//! # Compare against baseline
//! cargo run -p ml --example backtest_dqn --release --features cuda -- \
//! --checkpoint ml/trained_models/dqn_epoch_5.safetensors \
//! --baseline ml/trained_models/dqn_baseline.safetensors \
//! --data test_data/ES_FUT_180d.parquet
//!
//! # Export results to JSON
//! cargo run -p ml --example backtest_dqn --release --features cuda -- \
//! --checkpoint ml/trained_models/dqn_epoch_5.safetensors \
//! --data test_data/ES_FUT_180d.parquet \
//! --output-json backtest_results.json
//!
//! # Export markdown report
//! cargo run -p ml --example backtest_dqn --release --features cuda -- \
//! --checkpoint ml/trained_models/dqn_epoch_5.safetensors \
//! --baseline ml/trained_models/dqn_baseline.safetensors \
//! --data test_data/ES_FUT_180d.parquet \
//! --output-markdown backtest_report.md
//! ```
//!
//! # Architecture
//!
//! ```text
//! ┌─────────────────────────────────────────────────────────────────┐
//! │ BACKTEST VALIDATION PIPELINE │
//! │ │
//! │ 1. LOAD CHECKPOINTS │
//! │ ├─ Primary checkpoint (required) │
//! │ └─ Baseline checkpoint (optional) │
//! │ │
//! │ 2. LOAD VALIDATION DATA │
//! │ ├─ Parquet file → OHLCV bars │
//! │ └─ Extract 128-dim features (Wave D) │
//! │ │
//! │ 3. RUN BACKTEST │
//! │ ├─ Primary model inference (greedy actions) │
//! │ ├─ Baseline model inference (if provided) │
//! │ ├─ Execute trades via EvaluationEngine │
//! │ └─ Record trade history │
//! │ │
//! │ 4. CALCULATE METRICS │
//! │ ├─ Sharpe ratio (risk-adjusted return) │
//! │ ├─ Win rate (% profitable trades) │
//! │ ├─ Max drawdown (peak-to-trough decline) │
//! │ └─ Total return (% gain/loss) │
//! │ │
//! │ 5. VALIDATE CRITERIA │
//! │ ├─ Sharpe > 2.0 ✅/❌ │
//! │ ├─ Win rate > 55% ✅/❌ │
//! │ └─ Drawdown < 20% ✅/❌ │
//! │ │
//! │ 6. GENERATE REPORT │
//! │ ├─ Console output (always) │
//! │ ├─ JSON output (--output-json) │
//! │ └─ Markdown report (--output-markdown) │
//! └─────────────────────────────────────────────────────────────────┘
//! ```
use anyhow::{Context, Result};
use candle_core::{Device, Tensor};
use clap::Parser;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::time::Instant;
use tracing::{info, warn};
use tracing_subscriber::FmtSubscriber;
use ml::data_loaders::load_parquet_data_with_timestamps;
use ml::dqn::dqn::{WorkingDQN, WorkingDQNConfig};
use ml::evaluation::{EvaluationEngine, PerformanceMetrics};
use ml::features::extraction::OHLCVBar;
use ml::preprocessing::{preprocess_prices, PreprocessConfig};
// ============================================================================
// CLI Configuration
// ============================================================================
/// Backtest validation configuration
#[derive(Parser, Debug)]
#[command(
name = "backtest_dqn",
about = "Validate DQN checkpoint against production criteria",
long_about = "Comprehensive backtest validation pipeline with Sharpe, win rate, \
and drawdown metrics. Compares against baseline if provided."
)]
struct BacktestConfig {
/// Path to DQN checkpoint to validate (SafeTensors format)
#[arg(long, required = true)]
checkpoint: PathBuf,
/// Path to baseline checkpoint for comparison (optional)
#[arg(long)]
baseline: Option<PathBuf>,
/// Path to validation data (Parquet format)
#[arg(long, required = true)]
data: PathBuf,
/// Device selection: cpu, cuda, or auto
#[arg(long, default_value = "auto")]
device: String,
/// Initial capital for backtest (default: $100,000)
#[arg(long, default_value = "100000.0")]
initial_capital: f32,
/// Warmup bars to skip (insufficient feature history)
#[arg(long, default_value = "50")]
warmup_bars: usize,
/// Output JSON file path (optional)
#[arg(long)]
output_json: Option<PathBuf>,
/// Output markdown report path (optional)
#[arg(long)]
output_markdown: Option<PathBuf>,
/// Verbose logging (DEBUG level)
#[arg(short, long)]
verbose: bool,
/// Success criteria: minimum Sharpe ratio
#[arg(long, default_value = "2.0")]
min_sharpe: f64,
/// Success criteria: minimum win rate (%)
#[arg(long, default_value = "55.0")]
min_win_rate: f64,
/// Success criteria: maximum drawdown (%)
#[arg(long, default_value = "20.0")]
max_drawdown: f64,
}
impl BacktestConfig {
/// Validate configuration parameters
fn validate(&self) -> Result<()> {
// Validate checkpoint exists
if !self.checkpoint.exists() {
anyhow::bail!(
"Checkpoint file not found: {}\n\
Suggestion: Train a model first using train_dqn example",
self.checkpoint.display()
);
}
// Validate baseline (if specified)
if let Some(ref baseline) = self.baseline {
if !baseline.exists() {
anyhow::bail!("Baseline checkpoint not found: {}", baseline.display());
}
}
// Validate data file
if !self.data.exists() {
anyhow::bail!(
"Data file not found: {}\n\
Suggestion: Use test_data/ES_FUT_180d.parquet",
self.data.display()
);
}
// Validate device string
if !["cpu", "cuda", "auto"].contains(&self.device.as_str()) {
anyhow::bail!(
"Invalid device: '{}'. Must be: cpu, cuda, or auto",
self.device
);
}
// Validate output paths (if specified)
if let Some(ref output_path) = self.output_json {
if let Some(parent) = output_path.parent() {
if !parent.exists() {
anyhow::bail!(
"Output JSON directory does not exist: {}\n\
Suggestion: mkdir -p {}",
parent.display(),
parent.display()
);
}
}
}
if let Some(ref output_path) = self.output_markdown {
if let Some(parent) = output_path.parent() {
if !parent.exists() {
anyhow::bail!(
"Output markdown directory does not exist: {}\n\
Suggestion: mkdir -p {}",
parent.display(),
parent.display()
);
}
}
}
Ok(())
}
}
// ============================================================================
// Model Loading
// ============================================================================
/// Load DQN checkpoint from SafeTensors file
fn load_checkpoint(path: &Path, _device: &Device) -> Result<WorkingDQN> {
info!("📦 Loading checkpoint: {}", path.display());
// Create WorkingDQN configuration (matches training architecture)
let config = WorkingDQNConfig {
state_dim: 54, // 54 features (Wave 21 feature reduction)
hidden_dims: vec![256, 128, 64], // Match trainer architecture
num_actions: 3,
learning_rate: 0.001,
gamma: 0.99,
epsilon_start: 0.0, // No exploration during evaluation
epsilon_end: 0.0,
epsilon_decay: 1.0,
replay_buffer_capacity: 1000,
batch_size: 32,
min_replay_size: 64,
target_update_freq: 1000,
use_double_dqn: true,
use_huber_loss: true,
huber_delta: 1.0,
leaky_relu_alpha: 0.01,
gradient_clip_norm: 10.0,
tau: 1.0,
use_soft_updates: false,
warmup_steps: 0,
};
// Create model
let mut dqn = WorkingDQN::new(config).context("Failed to create WorkingDQN")?;
// Load weights
let path_str = path
.to_str()
.ok_or_else(|| anyhow::anyhow!("Path contains invalid UTF-8"))?;
dqn.load_from_safetensors(path_str)
.context(format!("Failed to load checkpoint: {}", path.display()))?;
info!("✅ Checkpoint loaded successfully");
Ok(dqn)
}
// ============================================================================
// Backtest Execution
// ============================================================================
/// Run backtest on validation data
fn run_backtest(
dqn: &mut WorkingDQN,
features: &[[f64; 128]],
bars: &[OHLCVBar],
initial_capital: f32,
) -> Result<PerformanceMetrics> {
info!("🔄 Running backtest ({} bars)...", bars.len());
if features.len() != bars.len() {
anyhow::bail!(
"Feature/bar mismatch: {} features, {} bars",
features.len(),
bars.len()
);
}
let mut engine = EvaluationEngine::new(initial_capital);
let start = Instant::now();
// Run inference for each bar
for (i, (feature_vec, bar)) in features.iter().zip(bars.iter()).enumerate() {
// Convert f64 features to f32
let state_f32: Vec<f32> = feature_vec.iter().map(|&x| x as f32).collect();
// Get greedy action (epsilon=0)
let trading_action = dqn
.select_action(state_f32.as_slice())
.context(format!("Inference failed at bar {}", i))?;
// Convert to evaluation Action enum
let action = match trading_action.to_index() {
0 => ml::evaluation::engine::Action::Buy,
1 => ml::evaluation::engine::Action::Hold,
2 => ml::evaluation::engine::Action::Sell,
_ => ml::evaluation::engine::Action::Hold,
};
// Convert OHLCVBar to evaluation OHLCVBar
let eval_bar = ml::evaluation::metrics::OHLCVBar {
timestamp: bar.timestamp.timestamp(),
open: bar.open as f32,
high: bar.high as f32,
low: bar.low as f32,
close: bar.close as f32,
volume: bar.volume as f32,
};
// Process bar
engine.process_bar(i, &eval_bar, action);
}
// Close any open position at end
if engine.current_position.is_some() {
let last_bar = &bars[bars.len() - 1];
let eval_bar = ml::evaluation::metrics::OHLCVBar {
timestamp: last_bar.timestamp.timestamp(),
open: last_bar.open as f32,
high: last_bar.high as f32,
low: last_bar.low as f32,
close: last_bar.close as f32,
volume: last_bar.volume as f32,
};
engine.close_position(bars.len() - 1, &eval_bar);
}
let elapsed = start.elapsed();
info!("✅ Backtest complete ({:.2}s)", elapsed.as_secs_f64());
// Calculate performance metrics
let eval_bars: Vec<ml::evaluation::metrics::OHLCVBar> = bars
.iter()
.map(|b| ml::evaluation::metrics::OHLCVBar {
timestamp: b.timestamp.timestamp(),
open: b.open as f32,
high: b.high as f32,
low: b.low as f32,
close: b.close as f32,
volume: b.volume as f32,
})
.collect();
let metrics = PerformanceMetrics::from_trades(&engine.trades, initial_capital, &eval_bars);
// Log action distribution
let dist = engine.get_action_distribution();
info!("📊 Action Distribution:");
info!(" BUY: {} ({:.1}%)", dist.buy_count, dist.buy_pct);
info!(" HOLD: {} ({:.1}%)", dist.hold_count, dist.hold_pct);
info!(" SELL: {} ({:.1}%)", dist.sell_count, dist.sell_pct);
Ok(metrics)
}
// ============================================================================
// Validation & Reporting
// ============================================================================
/// Backtest validation result
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ValidationResult {
checkpoint_name: String,
baseline_name: Option<String>,
metrics: PerformanceMetrics,
baseline_metrics: Option<PerformanceMetrics>,
success_criteria: SuccessCriteria,
verdict: Verdict,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SuccessCriteria {
min_sharpe: f64,
min_win_rate: f64,
max_drawdown: f64,
sharpe_passed: bool,
win_rate_passed: bool,
drawdown_passed: bool,
overall_passed: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
enum Verdict {
ProductionReady,
Failed { reasons: Vec<String> },
}
/// Validate backtest results against production criteria
fn validate_results(
checkpoint_name: String,
baseline_name: Option<String>,
metrics: PerformanceMetrics,
baseline_metrics: Option<PerformanceMetrics>,
config: &BacktestConfig,
) -> ValidationResult {
info!("📋 Validating against production criteria...");
let sharpe_passed = metrics.sharpe_ratio >= config.min_sharpe;
let win_rate_passed = metrics.win_rate >= config.min_win_rate;
let drawdown_passed = metrics.max_drawdown_pct <= config.max_drawdown;
let overall_passed = sharpe_passed && win_rate_passed && drawdown_passed;
let mut reasons = Vec::new();
if !sharpe_passed {
reasons.push(format!(
"Sharpe ratio {:.2} < {:.2} (required)",
metrics.sharpe_ratio, config.min_sharpe
));
}
if !win_rate_passed {
reasons.push(format!(
"Win rate {:.1}% < {:.1}% (required)",
metrics.win_rate, config.min_win_rate
));
}
if !drawdown_passed {
reasons.push(format!(
"Max drawdown {:.1}% > {:.1}% (limit)",
metrics.max_drawdown_pct, config.max_drawdown
));
}
let verdict = if overall_passed {
Verdict::ProductionReady
} else {
Verdict::Failed { reasons }
};
ValidationResult {
checkpoint_name,
baseline_name,
metrics,
baseline_metrics,
success_criteria: SuccessCriteria {
min_sharpe: config.min_sharpe,
min_win_rate: config.min_win_rate,
max_drawdown: config.max_drawdown,
sharpe_passed,
win_rate_passed,
drawdown_passed,
overall_passed,
},
verdict,
}
}
/// Print validation report to console
fn print_report(result: &ValidationResult) {
println!("\n╔══════════════════════════════════════════════════════════════════════╗");
println!("║ DQN BACKTEST VALIDATION REPORT ║");
println!("╚══════════════════════════════════════════════════════════════════════╝");
println!();
// Checkpoint info
println!("═══ Checkpoint ═══");
println!(" Primary: {}", result.checkpoint_name);
if let Some(ref baseline) = result.baseline_name {
println!(" Baseline: {}", baseline);
}
println!();
// Performance metrics
println!("═══ Performance Metrics ═══");
println!(
" Total Return: {:>8.2}%",
result.metrics.total_return_pct
);
println!(" Sharpe Ratio: {:>8.2}", result.metrics.sharpe_ratio);
println!(
" Max Drawdown: {:>8.2}%",
result.metrics.max_drawdown_pct
);
println!(" Win Rate: {:>8.1}%", result.metrics.win_rate);
println!(" Total Trades: {:>8}", result.metrics.total_trades);
println!(" Avg Trade PnL: {:>8.2}", result.metrics.avg_trade_pnl);
println!(" Final Equity: {:>8.2}", result.metrics.final_equity);
println!();
// Baseline comparison (if available)
if let Some(ref baseline) = result.baseline_metrics {
println!("═══ Baseline Comparison ═══");
let sharpe_diff = result.metrics.sharpe_ratio - baseline.sharpe_ratio;
let return_diff = result.metrics.total_return_pct - baseline.total_return_pct;
let drawdown_diff = baseline.max_drawdown_pct - result.metrics.max_drawdown_pct;
let win_rate_diff = result.metrics.win_rate - baseline.win_rate;
println!(
" Sharpe Ratio: {:>8.2}{:>8.2} ({:+.2})",
baseline.sharpe_ratio, result.metrics.sharpe_ratio, sharpe_diff
);
println!(
" Total Return: {:>8.2}% → {:>8.2}% ({:+.2}%)",
baseline.total_return_pct, result.metrics.total_return_pct, return_diff
);
println!(
" Max Drawdown: {:>8.2}% → {:>8.2}% ({:+.2}%)",
baseline.max_drawdown_pct, result.metrics.max_drawdown_pct, drawdown_diff
);
println!(
" Win Rate: {:>8.1}% → {:>8.1}% ({:+.1}%)",
baseline.win_rate, result.metrics.win_rate, win_rate_diff
);
println!();
}
// Success criteria
println!("═══ Success Criteria ═══");
println!(
" {} Sharpe Ratio ≥ {:.2}: {}",
if result.success_criteria.sharpe_passed {
""
} else {
""
},
result.success_criteria.min_sharpe,
result.metrics.sharpe_ratio
);
println!(
" {} Win Rate ≥ {:.1}%: {:.1}%",
if result.success_criteria.win_rate_passed {
""
} else {
""
},
result.success_criteria.min_win_rate,
result.metrics.win_rate
);
println!(
" {} Max Drawdown ≤ {:.1}%: {:.1}%",
if result.success_criteria.drawdown_passed {
""
} else {
""
},
result.success_criteria.max_drawdown,
result.metrics.max_drawdown_pct
);
println!();
// Verdict
println!("═══ Verdict ═══");
match &result.verdict {
Verdict::ProductionReady => {
println!(" ✅ PRODUCTION READY");
println!(" Checkpoint meets all success criteria");
},
Verdict::Failed { reasons } => {
println!(" ❌ FAILED VALIDATION");
println!(" Reasons:");
for reason in reasons {
println!("{}", reason);
}
},
}
println!();
}
/// Generate markdown report
fn generate_markdown(result: &ValidationResult) -> String {
let mut md = String::new();
md.push_str("# DQN Backtest Validation Report\n\n");
// Checkpoint info
md.push_str("## Checkpoint\n\n");
md.push_str(&format!("**Primary**: `{}`\n", result.checkpoint_name));
if let Some(ref baseline) = result.baseline_name {
md.push_str(&format!("**Baseline**: `{}`\n", baseline));
}
md.push_str("\n");
// Performance metrics
md.push_str("## Performance Metrics\n\n");
md.push_str("| Metric | Value |\n");
md.push_str("|--------|-------|\n");
md.push_str(&format!(
"| Total Return | {:.2}% |\n",
result.metrics.total_return_pct
));
md.push_str(&format!(
"| Sharpe Ratio | {:.2} |\n",
result.metrics.sharpe_ratio
));
md.push_str(&format!(
"| Max Drawdown | {:.2}% |\n",
result.metrics.max_drawdown_pct
));
md.push_str(&format!("| Win Rate | {:.1}% |\n", result.metrics.win_rate));
md.push_str(&format!(
"| Total Trades | {} |\n",
result.metrics.total_trades
));
md.push_str(&format!(
"| Avg Trade PnL | {:.2} |\n",
result.metrics.avg_trade_pnl
));
md.push_str(&format!(
"| Final Equity | {:.2} |\n",
result.metrics.final_equity
));
md.push_str("\n");
// Baseline comparison
if let Some(ref baseline) = result.baseline_metrics {
md.push_str("## Baseline Comparison\n\n");
md.push_str("| Metric | Baseline | Primary | Change |\n");
md.push_str("|--------|----------|---------|--------|\n");
let sharpe_diff = result.metrics.sharpe_ratio - baseline.sharpe_ratio;
let return_diff = result.metrics.total_return_pct - baseline.total_return_pct;
let drawdown_diff = baseline.max_drawdown_pct - result.metrics.max_drawdown_pct;
let win_rate_diff = result.metrics.win_rate - baseline.win_rate;
md.push_str(&format!(
"| Sharpe Ratio | {:.2} | {:.2} | {:+.2} |\n",
baseline.sharpe_ratio, result.metrics.sharpe_ratio, sharpe_diff
));
md.push_str(&format!(
"| Total Return | {:.2}% | {:.2}% | {:+.2}% |\n",
baseline.total_return_pct, result.metrics.total_return_pct, return_diff
));
md.push_str(&format!(
"| Max Drawdown | {:.2}% | {:.2}% | {:+.2}% |\n",
baseline.max_drawdown_pct, result.metrics.max_drawdown_pct, drawdown_diff
));
md.push_str(&format!(
"| Win Rate | {:.1}% | {:.1}% | {:+.1}% |\n",
baseline.win_rate, result.metrics.win_rate, win_rate_diff
));
md.push_str("\n");
}
// Success criteria
md.push_str("## Success Criteria\n\n");
md.push_str(&format!(
"- {} **Sharpe Ratio ≥ {:.2}**: {:.2}\n",
if result.success_criteria.sharpe_passed {
""
} else {
""
},
result.success_criteria.min_sharpe,
result.metrics.sharpe_ratio
));
md.push_str(&format!(
"- {} **Win Rate ≥ {:.1}%**: {:.1}%\n",
if result.success_criteria.win_rate_passed {
""
} else {
""
},
result.success_criteria.min_win_rate,
result.metrics.win_rate
));
md.push_str(&format!(
"- {} **Max Drawdown ≤ {:.1}%**: {:.1}%\n",
if result.success_criteria.drawdown_passed {
""
} else {
""
},
result.success_criteria.max_drawdown,
result.metrics.max_drawdown_pct
));
md.push_str("\n");
// Verdict
md.push_str("## Verdict\n\n");
match &result.verdict {
Verdict::ProductionReady => {
md.push_str("✅ **PRODUCTION READY**\n\n");
md.push_str(
"Checkpoint meets all success criteria and is ready for production deployment.\n",
);
},
Verdict::Failed { reasons } => {
md.push_str("❌ **FAILED VALIDATION**\n\n");
md.push_str("Checkpoint failed the following criteria:\n\n");
for reason in reasons {
md.push_str(&format!("- {}\n", reason));
}
},
}
md
}
// ============================================================================
// Main Orchestrator
// ============================================================================
#[tokio::main]
async fn main() -> Result<()> {
// Parse CLI options
let config = BacktestConfig::parse();
// Setup logging
let level = if config.verbose {
tracing::Level::DEBUG
} else {
tracing::Level::INFO
};
let subscriber = FmtSubscriber::builder().with_max_level(level).finish();
tracing::subscriber::set_global_default(subscriber)
.context("Failed to set tracing subscriber")?;
// Print banner
info!("╔══════════════════════════════════════════════════════════════════════╗");
info!("║ DQN Backtest Validation Pipeline - v1.0.0 ║");
info!(
"║ Timestamp: {} ║",
chrono::Local::now().format("%Y-%m-%d %H:%M:%S")
);
info!("╚══════════════════════════════════════════════════════════════════════╝");
info!("");
// Validate configuration
info!("🔍 Validating configuration...");
config
.validate()
.context("Configuration validation failed")?;
info!("✅ Configuration validated");
info!("");
// Create device
let device = match config.device.as_str() {
"cpu" => {
info!("🖥️ Using CPU device");
Device::Cpu
},
"cuda" => {
info!("🎮 Using CUDA device");
Device::new_cuda(0).context("CUDA unavailable. Use --device cpu or --device auto")?
},
"auto" => match Device::cuda_if_available(0) {
Ok(cuda_device) => {
info!("🎮 Using CUDA device (auto-detected)");
cuda_device
},
Err(_) => {
warn!("⚠️ CUDA unavailable, falling back to CPU");
Device::Cpu
},
},
_ => unreachable!(),
};
info!("");
let total_start = Instant::now();
// Phase 1: Load data
info!("📂 Phase 1: Loading validation data");
let data_start = Instant::now();
let (mut features, _timestamps, bars) =
load_parquet_data_with_timestamps(&config.data, config.warmup_bars)
.context("Failed to load validation data")?;
info!(
"✅ Loaded {} bars ({:.2}s)",
bars.len(),
data_start.elapsed().as_secs_f64()
);
info!("");
// Phase 1.5: Preprocessing
info!("🔬 Phase 1.5: Preprocessing data");
let close_prices: Vec<f32> = bars.iter().map(|b| b.close as f32).collect();
let close_tensor = Tensor::from_slice(&close_prices, (close_prices.len(),), &device)
.context("Failed to create tensor")?;
let preprocess_config = PreprocessConfig {
window_size: 50,
clip_sigma: 5.0,
use_log_returns: true,
};
let preprocessed =
preprocess_prices(&close_tensor, preprocess_config).context("Preprocessing failed")?;
let preprocessed_vec: Vec<f32> = preprocessed
.to_vec1()
.context("Failed to convert tensor to vec")?;
// Update features with preprocessed close prices
for (i, feature_vec) in features.iter_mut().enumerate() {
if i < preprocessed_vec.len() {
feature_vec[3] = preprocessed_vec[i] as f64; // Index 3 is close price
}
}
info!("✅ Preprocessing complete");
info!("");
// Phase 2: Load checkpoints
info!("📦 Phase 2: Loading checkpoints");
let mut primary_dqn = load_checkpoint(&config.checkpoint, &device)?;
let mut baseline_dqn = if let Some(ref baseline_path) = config.baseline {
Some(load_checkpoint(baseline_path, &device)?)
} else {
None
};
info!("");
// Phase 3: Run backtests
info!("🔄 Phase 3: Running backtests");
// Primary backtest
let primary_metrics = run_backtest(&mut primary_dqn, &features, &bars, config.initial_capital)?;
// Baseline backtest (if provided)
let baseline_metrics = if let Some(ref mut baseline) = baseline_dqn {
info!("");
info!("🔄 Running baseline backtest...");
Some(run_backtest(
baseline,
&features,
&bars,
config.initial_capital,
)?)
} else {
None
};
info!("");
// Phase 4: Validate results
info!("📋 Phase 4: Validating results");
let result = validate_results(
config.checkpoint.display().to_string(),
config.baseline.as_ref().map(|p| p.display().to_string()),
primary_metrics,
baseline_metrics,
&config,
);
info!("");
// Phase 5: Generate reports
info!("📝 Phase 5: Generating reports");
// Console output (always)
print_report(&result);
// JSON output (if requested)
if let Some(ref json_path) = config.output_json {
let json = serde_json::to_string_pretty(&result).context("Failed to serialize to JSON")?;
std::fs::write(json_path, json)
.context(format!("Failed to write JSON: {}", json_path.display()))?;
info!("✅ JSON report saved: {}", json_path.display());
}
// Markdown output (if requested)
if let Some(ref md_path) = config.output_markdown {
let markdown = generate_markdown(&result);
std::fs::write(md_path, markdown)
.context(format!("Failed to write markdown: {}", md_path.display()))?;
info!("✅ Markdown report saved: {}", md_path.display());
}
let total_elapsed = total_start.elapsed();
info!("");
info!("╔══════════════════════════════════════════════════════════════════════╗");
info!("║ VALIDATION COMPLETE ║");
info!("╚══════════════════════════════════════════════════════════════════════╝");
info!(" Total runtime: {:.2}s", total_elapsed.as_secs_f64());
info!("");
// Exit with appropriate code
match result.verdict {
Verdict::ProductionReady => {
info!("✅ EXIT CODE 0: Production ready");
std::process::exit(0);
},
Verdict::Failed { .. } => {
warn!("❌ EXIT CODE 1: Validation failed");
std::process::exit(1);
},
}
}