Created ml/src/types/ohlcv.rs as the single source of truth for OHLCVBar (DateTime<Utc> timestamp, f64 OHLCV fields). Replaced all 13 duplicate definitions across features/, regime/, real_data_loader, and evaluation/ with imports from crate::types::OHLCVBar. Key changes: - New: ml/src/types/mod.rs + ohlcv.rs with canonical OHLCVBar (derives: Debug, Clone, Copy, PartialEq, Serialize, Deserialize + Default) - Renamed: evaluation::metrics::OHLCVBar → OHLCVBarF32 (genuinely different type: f32 fields, i64 timestamp for compact backtesting) - Eliminated all import aliases (ExtractionOHLCVBar, RegimeOHLCVBar, PriceOHLCVBar, VolumeOHLCVBar) in dbn_sequence_loader.rs and pipeline.rs - Renamed regime::orchestrator::Bar → OHLCVBar (same fields, just aliased) - Updated 39 files total (13 definitions removed, imports normalized) 1883 lib tests passing, compilation clean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
213 lines
7.9 KiB
Rust
213 lines
7.9 KiB
Rust
//! Evaluate production DQN checkpoint
|
|
//!
|
|
//! This script loads the trained checkpoint and runs backtest evaluation
|
|
//! to extract Sharpe ratio and other performance metrics.
|
|
|
|
use anyhow::{Context, Result};
|
|
use candle_core::{Device, Tensor};
|
|
use ml::data_loaders::load_parquet_data;
|
|
use ml::dqn::dqn::WorkingDQN;
|
|
use ml::evaluation::engine::{Action, EvaluationEngine};
|
|
use ml::evaluation::metrics::{OHLCVBarF32, PerformanceMetrics};
|
|
use std::path::Path;
|
|
use tracing::info;
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
// Initialize logging
|
|
tracing_subscriber::fmt()
|
|
.with_max_level(tracing::Level::INFO)
|
|
.init();
|
|
|
|
let checkpoint_path = "/home/jgrusewski/Work/foxhunt/ml/trained_models/dqn_epoch_100.safetensors";
|
|
let data_path = "/home/jgrusewski/Work/foxhunt/test_data/ES_FUT_180d.parquet";
|
|
let initial_capital = 100000.0;
|
|
|
|
info!("=== Production DQN Checkpoint Evaluation ===");
|
|
info!("Checkpoint: {}", checkpoint_path);
|
|
info!("Data: {}", data_path);
|
|
info!("Capital: ${}", initial_capital);
|
|
|
|
// Step 1: Load parquet data (returns Vec<[f64; 128]>)
|
|
info!("Loading validation data...");
|
|
let data = load_parquet_data(Path::new(data_path), 50)
|
|
.context("Failed to load parquet data")?;
|
|
|
|
info!("Loaded {} samples from parquet", data.len());
|
|
|
|
// Step 2: Load DQN model from checkpoint
|
|
info!("Loading DQN model from checkpoint...");
|
|
let device = Device::cuda_if_available(0)?;
|
|
info!("Device: {:?}", device);
|
|
|
|
// Create DQN with default config (architecture must match saved model)
|
|
use ml::dqn::dqn::WorkingDQNConfig;
|
|
let config = WorkingDQNConfig {
|
|
state_dim: 54,
|
|
num_actions: 45,
|
|
hidden_dims: vec![256, 128, 64],
|
|
learning_rate: 0.000037,
|
|
gamma: 0.974,
|
|
epsilon_start: 0.1,
|
|
epsilon_end: 0.01,
|
|
epsilon_decay: 0.995,
|
|
replay_buffer_capacity: 94016,
|
|
batch_size: 114,
|
|
target_update_frequency: 10,
|
|
tau: 0.001,
|
|
warmup_steps: 0,
|
|
save_interval: 10,
|
|
device: device.clone(),
|
|
use_double_dqn: true,
|
|
use_dueling: true,
|
|
use_noisy_nets: true,
|
|
use_per: true,
|
|
per_alpha: 0.504,
|
|
per_beta_start: 0.239,
|
|
per_beta_end: 1.0,
|
|
per_beta_frames: 100000,
|
|
categorical_v_min: -1.6,
|
|
categorical_v_max: 2.1,
|
|
categorical_num_atoms: 200,
|
|
n_step: 2,
|
|
gradient_clip: 10.0,
|
|
eval_interval: 10,
|
|
eval_episodes: 5,
|
|
};
|
|
|
|
let mut dqn = WorkingDQN::new(config)?;
|
|
|
|
// Load weights from checkpoint
|
|
dqn.load_from_safetensors(checkpoint_path)?;
|
|
info!("Model loaded successfully");
|
|
|
|
// Step 3: Run backtest evaluation
|
|
info!("Running backtest evaluation...");
|
|
let mut engine = EvaluationEngine::new(initial_capital);
|
|
let mut ohlcv_bars = Vec::with_capacity(data.len());
|
|
|
|
for (bar_idx, feature_array) in data.iter().enumerate() {
|
|
// Extract close price from features (index 3 in the 128-dim feature array)
|
|
let close_price = feature_array[3] as f32;
|
|
|
|
// Convert feature array to Tensor (54-dim state expected by DQN)
|
|
// We need to pad/truncate the 128-dim array to 54-dim
|
|
let mut state_vec = vec![0.0f32; 54];
|
|
for (i, &val) in feature_array.iter().take(128).enumerate() {
|
|
state_vec[i] = val as f32;
|
|
}
|
|
|
|
let state = Tensor::from_vec(state_vec, (1, 54), &device)
|
|
.context("Failed to create state tensor")?;
|
|
|
|
// Get action from DQN (greedy, no exploration)
|
|
let action_idx = dqn.select_action(&state, 0.0)?; // epsilon=0.0 for greedy
|
|
let action = Action::from(action_idx);
|
|
|
|
// Create OHLCV bar
|
|
let bar = OHLCVBarF32 {
|
|
timestamp: bar_idx as i64,
|
|
open: close_price,
|
|
high: close_price,
|
|
low: close_price,
|
|
close: close_price,
|
|
volume: 0.0,
|
|
};
|
|
|
|
// Process bar through evaluation engine
|
|
engine.process_bar(bar_idx, &bar, action);
|
|
ohlcv_bars.push(bar);
|
|
|
|
// Log progress every 1000 bars
|
|
if (bar_idx + 1) % 1000 == 0 {
|
|
info!("Processed {}/{} bars", bar_idx + 1, data.len());
|
|
}
|
|
}
|
|
|
|
// Close any open position
|
|
if let Some(last_bar) = ohlcv_bars.last() {
|
|
engine.close_position(ohlcv_bars.len() - 1, last_bar);
|
|
}
|
|
|
|
// Step 4: Calculate performance metrics
|
|
info!("Calculating performance metrics...");
|
|
let metrics = PerformanceMetrics::from_trades(&engine.trades, initial_capital, &ohlcv_bars);
|
|
let action_dist = engine.get_action_distribution();
|
|
|
|
// Step 5: Print comprehensive report
|
|
println!("\n{}", "=".repeat(70));
|
|
println!("PRODUCTION DQN MODEL EVALUATION REPORT");
|
|
println!("{}", "=".repeat(70));
|
|
println!();
|
|
|
|
println!("MODEL DETAILS:");
|
|
println!(" Checkpoint: {}", checkpoint_path);
|
|
println!(" Training: 100 epochs, 65.4 minutes, $0.27 cost");
|
|
println!(" Final Loss: -0.088, Q-value: 0.888");
|
|
println!();
|
|
|
|
println!("VALIDATION TARGET (Trial #2):");
|
|
println!(" Sharpe Ratio: 1.2464");
|
|
println!(" Win Rate: 53.59%");
|
|
println!(" Max Drawdown: 0.13%");
|
|
println!(" Total Return: 1.71%");
|
|
println!();
|
|
|
|
println!("BACKTEST PERFORMANCE:");
|
|
println!(" Total Return: {:.2}%", metrics.total_return_pct);
|
|
println!(" Sharpe Ratio: {:.4}", metrics.sharpe_ratio);
|
|
println!(" Sortino Ratio: {:.4}", metrics.sortino_ratio);
|
|
println!(" Calmar Ratio: {:.4}", metrics.calmar_ratio);
|
|
println!(" Omega Ratio: {:.4}", metrics.omega_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!();
|
|
|
|
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!();
|
|
|
|
// Compare to validation target
|
|
println!("COMPARISON TO VALIDATION TARGET:");
|
|
let sharpe_diff = ((metrics.sharpe_ratio - 1.2464) / 1.2464 * 100.0).abs();
|
|
let win_rate_diff = ((metrics.win_rate * 100.0 - 53.59) / 53.59 * 100.0).abs();
|
|
let dd_diff = ((metrics.max_drawdown_pct - 0.13) / 0.13 * 100.0).abs();
|
|
|
|
println!(" Sharpe: {:.4} vs 1.2464 ({:+.1}%)", metrics.sharpe_ratio,
|
|
(metrics.sharpe_ratio - 1.2464) / 1.2464 * 100.0);
|
|
println!(" Win Rate: {:.2}% vs 53.59% ({:+.1}%)", metrics.win_rate * 100.0,
|
|
(metrics.win_rate * 100.0 - 53.59) / 53.59 * 100.0);
|
|
println!(" Drawdown: {:.2}% vs 0.13% ({:+.1}%)", metrics.max_drawdown_pct,
|
|
(metrics.max_drawdown_pct - 0.13) / 0.13 * 100.0);
|
|
println!();
|
|
|
|
// Determine success/failure
|
|
let sharpe_ok = metrics.sharpe_ratio >= 1.05; // ±15% tolerance
|
|
let sharpe_close = metrics.sharpe_ratio >= 0.85; // ±30% tolerance
|
|
|
|
println!("PRODUCTION READINESS:");
|
|
if sharpe_ok {
|
|
println!(" ✅ SUCCESS: Sharpe {:.4} >= 1.05 (within ±15% of 1.2464)", metrics.sharpe_ratio);
|
|
println!(" ✅ Hyperopt validated");
|
|
println!(" ✅ Ready for production deployment");
|
|
} else if sharpe_close {
|
|
println!(" ⚠️ PARTIAL SUCCESS: Sharpe {:.4} in range [0.85, 1.05]", metrics.sharpe_ratio);
|
|
println!(" ⚠️ Acceptable variance from target (±30%)");
|
|
println!(" ⚠️ Monitor closely in production");
|
|
} else {
|
|
println!(" ❌ FAILURE: Sharpe {:.4} < 0.85 (>30% below target)", metrics.sharpe_ratio);
|
|
println!(" ❌ Significant mismatch from validation");
|
|
println!(" ❌ Do NOT deploy to production");
|
|
}
|
|
|
|
println!("{}", "=".repeat(70));
|
|
println!();
|
|
|
|
Ok(())
|
|
}
|