//! Hyperopt Runner for DQN/PPO on Real Databento Market Data //! //! Runs hyperparameter optimization using Particle Swarm Optimization (PSO) for //! DQN, PPO, or both models on downloaded Databento futures data. This binary is //! part of the walk-forward real-data training pipeline. //! //! ## Usage //! //! ```bash //! # Run DQN hyperopt only (10 trials, 10 epochs each) //! SQLX_OFFLINE=true cargo run -p ml --example hyperopt_baseline --release -- \ //! --model dqn --trials 10 --epochs 10 \ //! --data-dir data/cache/futures-baseline //! //! # Run PPO hyperopt only //! SQLX_OFFLINE=true cargo run -p ml --example hyperopt_baseline --release -- \ //! --model ppo --trials 20 --epochs 15 \ //! --data-dir data/cache/futures-baseline //! //! # Run both models (default) //! SQLX_OFFLINE=true cargo run -p ml --example hyperopt_baseline --release -- \ //! --data-dir data/cache/futures-baseline //! ``` //! //! ## Output //! //! Results are written as JSON to `--output` (default: `ml/trained_models/hyperopt_results.json`). //! //! ```json //! { //! "dqn": { "best_objective": 0.123, "best_params": {...}, "trials": 20, "elapsed_secs": 45.3 }, //! "ppo": { "best_objective": 0.456, "best_params": {...}, "trials": 20, "elapsed_secs": 67.8 } //! } //! ``` use anyhow::{Context, Result}; use clap::Parser; use serde_json::Value; use std::path::PathBuf; use std::time::Instant; use tracing::{error, info, warn, Level}; use ml::hyperopt::adapters::dqn::DQNTrainer; use ml::hyperopt::adapters::ppo::PPOTrainer; use ml::hyperopt::paths::{generate_run_id, TrainingPaths}; use ml::hyperopt::ArgminOptimizer; /// Hyperparameter optimization runner for DQN/PPO on Databento market data #[derive(Parser, Debug)] #[command(name = "hyperopt-baseline")] #[command(about = "Run hyperparameter optimization for DQN/PPO on real Databento futures data")] struct Args { /// Model to optimize: "dqn", "ppo", or "both" #[arg(long, default_value = "both")] model: String, /// Number of PSO trials per model #[arg(long, default_value = "20")] trials: usize, /// Number of initial LHS (Latin Hypercube Sampling) samples #[arg(long, default_value = "5")] n_initial: usize, /// Training epochs per trial (DQN) / episodes per trial (PPO) #[arg(long, default_value = "10")] epochs: usize, /// Path to downloaded DBN data directory #[arg(long, default_value = "data/cache/futures-baseline")] data_dir: PathBuf, /// Output path for JSON results #[arg(long, default_value = "ml/trained_models/hyperopt_results.json")] output: PathBuf, /// Random seed for reproducibility #[arg(long, default_value = "42")] seed: u64, /// Base directory for training run outputs (checkpoints, logs, metrics) #[arg(long, default_value = "/tmp/ml_training")] base_dir: String, } /// Result entry for one model's hyperopt run fn build_model_result( best_objective: f64, best_params_json: Value, num_trials: usize, elapsed_secs: f64, ) -> Value { serde_json::json!({ "best_objective": best_objective, "best_params": best_params_json, "trials": num_trials, "elapsed_secs": elapsed_secs, }) } fn run_dqn_hyperopt(args: &Args) -> Result { info!("========================================"); info!(" DQN Hyperparameter Optimization"); info!("========================================"); let run_id = generate_run_id("hyperopt-dqn"); let training_paths = TrainingPaths::new(&args.base_dir, "dqn", &run_id); info!("Run ID: {}", run_id); info!("Data directory: {}", args.data_dir.display()); info!("Epochs per trial: {}", args.epochs); info!("Trials: {}", args.trials); let trainer = DQNTrainer::new(&args.data_dir, args.epochs) .context("Failed to create DQN trainer")? .with_training_paths(training_paths); let optimizer = ArgminOptimizer::builder() .max_trials(args.trials) .n_initial(args.n_initial) .seed(args.seed) .build(); let start = Instant::now(); let result = optimizer .optimize(trainer) .context("DQN hyperopt optimization failed")?; let elapsed = start.elapsed().as_secs_f64(); info!("DQN hyperopt complete:"); info!(" Best objective: {:.6}", result.best_objective); info!(" Total trials: {}", result.all_trials.len()); info!(" Elapsed: {:.1}s", elapsed); let best_params_json = serde_json::to_value(&result.best_params) .ok() .unwrap_or(Value::Null); Ok(build_model_result( result.best_objective, best_params_json, result.all_trials.len(), elapsed, )) } fn run_ppo_hyperopt(args: &Args) -> Result { info!("========================================"); info!(" PPO Hyperparameter Optimization"); info!("========================================"); let run_id = generate_run_id("hyperopt-ppo"); let training_paths = TrainingPaths::new(&args.base_dir, "ppo", &run_id); info!("Run ID: {}", run_id); info!("Data directory: {}", args.data_dir.display()); info!("Episodes per trial: {}", args.epochs); info!("Trials: {}", args.trials); let trainer = PPOTrainer::new(&args.data_dir, args.epochs) .context("Failed to create PPO trainer")? .with_training_paths(training_paths); let optimizer = ArgminOptimizer::builder() .max_trials(args.trials) .n_initial(args.n_initial) .seed(args.seed) .build(); let start = Instant::now(); let result = optimizer .optimize(trainer) .context("PPO hyperopt optimization failed")?; let elapsed = start.elapsed().as_secs_f64(); info!("PPO hyperopt complete:"); info!(" Best objective: {:.6}", result.best_objective); info!(" Total trials: {}", result.all_trials.len()); info!(" Elapsed: {:.1}s", elapsed); let best_params_json = serde_json::to_value(&result.best_params) .ok() .unwrap_or(Value::Null); Ok(build_model_result( result.best_objective, best_params_json, result.all_trials.len(), elapsed, )) } fn main() -> Result<()> { // Initialize tracing tracing_subscriber::fmt() .with_max_level(Level::INFO) .with_target(false) .init(); let args = Args::parse(); info!("========================================"); info!(" Hyperopt Baseline Runner"); info!("========================================"); info!("Model: {}", args.model); info!("Trials: {}", args.trials); info!("Initial LHS samples: {}", args.n_initial); info!("Epochs/episodes per trial: {}", args.epochs); info!("Data directory: {}", args.data_dir.display()); info!("Output: {}", args.output.display()); info!("Seed: {}", args.seed); // Verify data directory exists if !args.data_dir.exists() { anyhow::bail!( "Data directory not found: {}. Run `download_baseline` first.", args.data_dir.display() ); } // Verify trials > n_initial (ArgminOptimizer requirement) if args.trials <= args.n_initial { anyhow::bail!( "trials ({}) must be greater than n_initial ({})", args.trials, args.n_initial ); } // Create output directory if let Some(parent) = args.output.parent() { std::fs::create_dir_all(parent) .with_context(|| format!("Failed to create output directory: {}", parent.display()))?; } let run_dqn = args.model == "dqn" || args.model == "both"; let run_ppo = args.model == "ppo" || args.model == "both"; if !run_dqn && !run_ppo { anyhow::bail!( "Invalid --model value '{}'. Must be 'dqn', 'ppo', or 'both'.", args.model ); } let mut results = serde_json::Map::new(); // Run DQN hyperopt if run_dqn { match run_dqn_hyperopt(&args) { Ok(dqn_result) => { results.insert("dqn".to_string(), dqn_result); }, Err(e) => { error!("DQN hyperopt failed: {:#}", e); warn!("Continuing with remaining models..."); results.insert( "dqn".to_string(), serde_json::json!({ "error": format!("{:#}", e) }), ); }, } } // Run PPO hyperopt if run_ppo { match run_ppo_hyperopt(&args) { Ok(ppo_result) => { results.insert("ppo".to_string(), ppo_result); }, Err(e) => { error!("PPO hyperopt failed: {:#}", e); warn!("Continuing..."); results.insert( "ppo".to_string(), serde_json::json!({ "error": format!("{:#}", e) }), ); }, } } // Write results to JSON let output_json = Value::Object(results); let output_str = serde_json::to_string_pretty(&output_json) .context("Failed to serialize results to JSON")?; std::fs::write(&args.output, &output_str) .with_context(|| format!("Failed to write results to {}", args.output.display()))?; info!("========================================"); info!(" Results saved to: {}", args.output.display()); info!("========================================"); info!("{}", output_str); Ok(()) }