Files
foxhunt/ml/examples/simple_dqn_eval.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

248 lines
7.6 KiB
Rust

//! Simple DQN Trading Evaluation
//!
//! Minimal evaluation script that loads a trained DQN model and evaluates
//! its trading performance on test data using the integrated evaluation engine.
//!
//! # Usage
//! ```bash
//! cargo run -p ml --example simple_dqn_eval --release --features cuda -- \
//! --model ml/trained_models/dqn_best_model.safetensors \
//! --data test_data/ES_FUT_180d.parquet
//! ```
use anyhow::{Context, Result};
use candle_core::{Device, Tensor};
use clap::Parser;
use ml::data_loaders::load_parquet_data;
use ml::dqn::dqn::{WorkingDQN, WorkingDQNConfig};
use ml::evaluation::engine::{Action, EvaluationEngine};
use ml::evaluation::metrics::{OHLCVBar, PerformanceMetrics};
use ml::features::extraction::compute_dqn_features;
use std::path::PathBuf;
use tracing::{info, warn};
#[derive(Parser)]
struct Args {
/// Path to trained model checkpoint
#[arg(long, default_value = "ml/trained_models/dqn_best_model.safetensors")]
model: PathBuf,
/// Path to test data (Parquet)
#[arg(long, default_value = "test_data/ES_FUT_180d.parquet")]
data: PathBuf,
/// Initial capital
#[arg(long, default_value_t = 100000.0)]
capital: f32,
/// Warmup bars to skip
#[arg(long, default_value_t = 50)]
warmup: usize,
}
fn main() -> Result<()> {
// Initialize logging
tracing_subscriber::fmt()
.with_max_level(tracing::Level::INFO)
.init();
let args = Args::parse();
info!("=== Trial #2 DQN Model Evaluation ===");
info!("Model: {}", args.model.display());
info!("Data: {}", args.data.display());
info!("Capital: ${}", args.capital);
// Step 1: Load parquet data
info!("Loading test data...");
let (features, bars) = load_parquet_data(&args.data, args.warmup)
.with_context(|| format!("Failed to load {}", args.data.display()))?;
info!("Loaded {} bars ({} features)", bars.len(), features.len());
// Validate data
if features.is_empty() || bars.len() < args.warmup {
anyhow::bail!(
"Insufficient data: {} bars, {} warmup required",
bars.len(),
args.warmup
);
}
// Step 2: Create device
let device = Device::cuda_if_available(0).context("Failed to create compute device")?;
info!("Device: {:?}", device);
// Step 3: Load DQN model
info!("Loading DQN model from {}...", args.model.display());
// Create config matching training hyperparameters
let config = WorkingDQNConfig {
state_dim: 54, // Feature dimension
action_dim: 3, // BUY, HOLD, SELL
hidden_dim: 256,
learning_rate: 0.000156,
gamma: 0.97,
epsilon: 0.01, // Greedy during evaluation
epsilon_decay: 0.995,
epsilon_min: 0.01,
batch_size: 100,
buffer_capacity: 642214,
target_update_freq: 10,
hold_penalty_weight: 1.0,
movement_threshold: 0.02,
gradient_clip_norm: 10.0,
leaky_relu_alpha: 0.01,
tau: 0.005,
warmup_steps: 0,
};
let mut dqn = WorkingDQN::new(config, device.clone()).context("Failed to create DQN model")?;
// Load weights from checkpoint
dqn.load(&args.model)
.with_context(|| format!("Failed to load model from {}", args.model.display()))?;
info!("✅ Model loaded successfully");
// Step 4: Run evaluation
info!("Running backtest evaluation...");
let mut engine = EvaluationEngine::new(args.capital);
for (idx, (feature, bar)) in features.iter().zip(bars.iter()).enumerate() {
// Convert feature to DQN state (54-dim)
let state = Tensor::from_slice(feature.as_slice(), (1, 54), &device)
.context("Failed to create state tensor")?;
// Get DQN action (greedy, no exploration during eval)
let action_idx = dqn
.select_action_greedy(&state)
.context("Failed to select action")?;
let action = Action::from(action_idx);
// Process action through evaluation engine
let ohlcv_bar = OHLCVBar {
timestamp: bar.timestamp,
open: bar.open,
high: bar.high,
low: bar.low,
close: bar.close,
volume: bar.volume,
};
engine.process_bar(idx, &ohlcv_bar, action);
// Log every 1000 bars
if (idx + 1) % 1000 == 0 {
info!("Processed {}/{} bars", idx + 1, features.len());
}
}
// Close any remaining position
if let Some(last_bar) = bars.last() {
let ohlcv_bar = OHLCVBar {
timestamp: last_bar.timestamp,
open: last_bar.open,
high: last_bar.high,
low: last_bar.low,
close: last_bar.close,
volume: last_bar.volume,
};
engine.close_position(features.len() - 1, &ohlcv_bar);
}
// Step 5: Calculate metrics
info!("Calculating performance metrics...");
let metrics = PerformanceMetrics::from_trades(&engine.trades, args.capital, &bars);
let action_dist = engine.get_action_distribution();
// Step 6: Print report
println!("\n{}", "=".repeat(70));
println!("TRIAL #2 DQN MODEL EVALUATION REPORT");
println!("{}", "=".repeat(70));
println!();
println!("MODEL DETAILS:");
println!(" Checkpoint: {}", args.model.display());
println!(" Training Epochs: 100 (best at epoch 61)");
println!(" Validation Loss: 8,017.93");
println!();
println!("HYPERPARAMETERS:");
println!(" Learning Rate: 0.000156");
println!(" Batch Size: 100");
println!(" Gamma: 0.97");
println!(" Buffer Size: 642,214");
println!(" Hold Penalty: 1.0");
println!();
println!("BACKTEST PERFORMANCE:");
println!(" Total Return: {:.2}%", metrics.total_return_pct);
println!(" Sharpe Ratio: {:.2}", metrics.sharpe_ratio);
println!(" Max Drawdown: {:.2}%", metrics.max_drawdown_pct);
println!(" Win Rate: {:.2}%", metrics.win_rate * 100.0);
println!(" Total Trades: {}", metrics.total_trades);
println!(" Avg Trade P&L: ${:.2}", metrics.avg_trade_pnl);
println!(" Final Equity: ${:.2}", metrics.final_equity);
println!(" Total P&L: ${:.2}", metrics.total_pnl);
println!();
println!("ACTION DISTRIBUTION:");
println!(
" BUY: {:>6} ({:>5.2}%)",
action_dist.buy_count, action_dist.buy_pct
);
println!(
" SELL: {:>6} ({:>5.2}%)",
action_dist.sell_count, action_dist.sell_pct
);
println!(
" HOLD: {:>6} ({:>5.2}%)",
action_dist.hold_count, action_dist.hold_pct
);
println!();
println!("PRODUCTION READINESS:");
let sharpe_ok = metrics.sharpe_ratio >= 1.5;
let win_rate_ok = metrics.win_rate >= 0.55;
let drawdown_ok = metrics.max_drawdown_pct <= 20.0;
let profitable = metrics.total_pnl > 0.0;
println!(
" {} Sharpe Ratio >= 1.5: {:.2}",
if sharpe_ok { "" } else { "" },
metrics.sharpe_ratio
);
println!(
" {} Win Rate >= 55%: {:.2}%",
if win_rate_ok { "" } else { "" },
metrics.win_rate * 100.0
);
println!(
" {} Max Drawdown <= 20%: {:.2}%",
if drawdown_ok { "" } else { "" },
metrics.max_drawdown_pct
);
println!(
" {} Profitable: ${:.2}",
if profitable { "" } else { "" },
metrics.total_pnl
);
println!();
let production_ready = sharpe_ok && win_rate_ok && drawdown_ok && profitable;
if production_ready {
println!("✅ PRODUCTION READY - All criteria met!");
} else {
println!("⚠️ NOT PRODUCTION READY - Some criteria not met");
}
println!("{}", "=".repeat(70));
println!();
Ok(())
}