diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c1828b2db..1f53c8ac1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -61,6 +61,7 @@ build-ci-builder: - /kaniko/executor --context "${CI_PROJECT_DIR}" --dockerfile "${CI_PROJECT_DIR}/infra/docker/Dockerfile.ci-builder" + --cache=true --cache-repo="${REGISTRY}/cache" --destination "${CI_BUILDER_IMAGE}" # -------------------------------------------------------------------------- @@ -83,12 +84,16 @@ build-devcontainer: allow_failure: true before_script: - mkdir -p /kaniko/.docker - - | - echo "{\"auths\":{\"rg.fr-par.scw.cloud\":{\"username\":\"nologin\",\"password\":\"${SCW_SECRET_KEY}\"}}}" > /kaniko/.docker/config.json + - >- + echo "{\"auths\":{ + \"rg.fr-par.scw.cloud\":{\"username\":\"nologin\",\"password\":\"${SCW_SECRET_KEY}\"}, + \"https://index.docker.io/v1/\":{\"username\":\"${DOCKERHUB_USERNAME}\",\"password\":\"${DOCKERHUB_TOKEN}\"} + }}" > /kaniko/.docker/config.json script: - /kaniko/executor --context "${CI_PROJECT_DIR}" --dockerfile "${CI_PROJECT_DIR}/.devcontainer/Dockerfile" + --cache=true --cache-repo="${REGISTRY}/cache" --destination "rg.fr-par.scw.cloud/foxhunt-ci/devcontainer:${CI_COMMIT_SHA}" --destination "rg.fr-par.scw.cloud/foxhunt-ci/devcontainer:latest" @@ -112,12 +117,16 @@ build-infra-runner: allow_failure: true before_script: - mkdir -p /kaniko/.docker - - | - echo "{\"auths\":{\"rg.fr-par.scw.cloud\":{\"username\":\"nologin\",\"password\":\"${SCW_SECRET_KEY}\"}}}" > /kaniko/.docker/config.json + - >- + echo "{\"auths\":{ + \"rg.fr-par.scw.cloud\":{\"username\":\"nologin\",\"password\":\"${SCW_SECRET_KEY}\"}, + \"https://index.docker.io/v1/\":{\"username\":\"${DOCKERHUB_USERNAME}\",\"password\":\"${DOCKERHUB_TOKEN}\"} + }}" > /kaniko/.docker/config.json script: - /kaniko/executor --context "${CI_PROJECT_DIR}" --dockerfile "${CI_PROJECT_DIR}/infra/docker/Dockerfile.infra-runner" + --cache=true --cache-repo="${REGISTRY}/cache" --destination "${INFRA_RUNNER_IMAGE}" # Base template for Rust jobs — pre-baked CI builder from Scaleway CR @@ -251,6 +260,7 @@ build-trading-service: --build-arg AWS_ACCESS_KEY_ID=${SCW_ACCESS_KEY} --build-arg AWS_SECRET_ACCESS_KEY=${SCW_SECRET_KEY} --build-arg SCCACHE_ENDPOINT=${SCCACHE_ENDPOINT} + --cache=true --cache-repo="${REGISTRY}/cache" --destination "${REGISTRY}/trading_service:${CI_COMMIT_SHA}" --destination "${REGISTRY}/trading_service:latest" @@ -265,6 +275,7 @@ build-api-gateway: --build-arg AWS_ACCESS_KEY_ID=${SCW_ACCESS_KEY} --build-arg AWS_SECRET_ACCESS_KEY=${SCW_SECRET_KEY} --build-arg SCCACHE_ENDPOINT=${SCCACHE_ENDPOINT} + --cache=true --cache-repo="${REGISTRY}/cache" --destination "${REGISTRY}/api_gateway:${CI_COMMIT_SHA}" --destination "${REGISTRY}/api_gateway:latest" @@ -279,6 +290,7 @@ build-broker-gateway: --build-arg AWS_ACCESS_KEY_ID=${SCW_ACCESS_KEY} --build-arg AWS_SECRET_ACCESS_KEY=${SCW_SECRET_KEY} --build-arg SCCACHE_ENDPOINT=${SCCACHE_ENDPOINT} + --cache=true --cache-repo="${REGISTRY}/cache" --destination "${REGISTRY}/broker_gateway_service:${CI_COMMIT_SHA}" --destination "${REGISTRY}/broker_gateway_service:latest" @@ -293,6 +305,7 @@ build-ml-training: --build-arg AWS_ACCESS_KEY_ID=${SCW_ACCESS_KEY} --build-arg AWS_SECRET_ACCESS_KEY=${SCW_SECRET_KEY} --build-arg SCCACHE_ENDPOINT=${SCCACHE_ENDPOINT} + --cache=true --cache-repo="${REGISTRY}/cache" --destination "${REGISTRY}/ml_training_service:${CI_COMMIT_SHA}" --destination "${REGISTRY}/ml_training_service:latest" @@ -307,6 +320,7 @@ build-backtesting: --build-arg AWS_ACCESS_KEY_ID=${SCW_ACCESS_KEY} --build-arg AWS_SECRET_ACCESS_KEY=${SCW_SECRET_KEY} --build-arg SCCACHE_ENDPOINT=${SCCACHE_ENDPOINT} + --cache=true --cache-repo="${REGISTRY}/cache" --destination "${REGISTRY}/backtesting_service:${CI_COMMIT_SHA}" --destination "${REGISTRY}/backtesting_service:latest" @@ -321,6 +335,7 @@ build-trading-agent: --build-arg AWS_ACCESS_KEY_ID=${SCW_ACCESS_KEY} --build-arg AWS_SECRET_ACCESS_KEY=${SCW_SECRET_KEY} --build-arg SCCACHE_ENDPOINT=${SCCACHE_ENDPOINT} + --cache=true --cache-repo="${REGISTRY}/cache" --destination "${REGISTRY}/trading_agent_service:${CI_COMMIT_SHA}" --destination "${REGISTRY}/trading_agent_service:latest" @@ -335,6 +350,7 @@ build-data-acquisition: --build-arg AWS_ACCESS_KEY_ID=${SCW_ACCESS_KEY} --build-arg AWS_SECRET_ACCESS_KEY=${SCW_SECRET_KEY} --build-arg SCCACHE_ENDPOINT=${SCCACHE_ENDPOINT} + --cache=true --cache-repo="${REGISTRY}/cache" --destination "${REGISTRY}/data_acquisition_service:${CI_COMMIT_SHA}" --destination "${REGISTRY}/data_acquisition_service:latest" @@ -344,6 +360,7 @@ build-web-gateway: - /kaniko/executor --context "${CI_PROJECT_DIR}" --dockerfile "${CI_PROJECT_DIR}/infra/docker/Dockerfile.web-gateway" + --cache=true --cache-repo="${REGISTRY}/cache" --destination "${REGISTRY}/web-gateway:${CI_COMMIT_SHA}" --destination "${REGISTRY}/web-gateway:latest" @@ -357,6 +374,7 @@ build-training: --build-arg AWS_ACCESS_KEY_ID=${SCW_ACCESS_KEY} --build-arg AWS_SECRET_ACCESS_KEY=${SCW_SECRET_KEY} --build-arg SCCACHE_ENDPOINT=${SCCACHE_ENDPOINT} + --cache=true --cache-repo="${REGISTRY}/cache" --destination "${REGISTRY}/training:${CI_COMMIT_SHA}" --destination "${REGISTRY}/training:latest" diff --git a/crates/ml/examples/hyperopt_baseline.rs b/crates/ml/examples/hyperopt_baseline_rl.rs similarity index 99% rename from crates/ml/examples/hyperopt_baseline.rs rename to crates/ml/examples/hyperopt_baseline_rl.rs index 1ffa05465..148305982 100644 --- a/crates/ml/examples/hyperopt_baseline.rs +++ b/crates/ml/examples/hyperopt_baseline_rl.rs @@ -1,4 +1,4 @@ -//! Hyperopt Runner for DQN/PPO on Real Databento Market Data +//! Hyperopt RL 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 @@ -47,7 +47,7 @@ use ml::hyperopt::ArgminOptimizer; /// Hyperparameter optimization runner for DQN/PPO on Databento market data #[derive(Parser, Debug)] -#[command(name = "hyperopt-baseline")] +#[command(name = "hyperopt-baseline-rl")] #[command(about = "Run hyperparameter optimization for DQN/PPO on real Databento futures data")] struct Args { /// Model to optimize: "dqn", "ppo", or "both" diff --git a/crates/ml/examples/hyperopt_baseline_supervised.rs b/crates/ml/examples/hyperopt_baseline_supervised.rs new file mode 100644 index 000000000..9e97f71dd --- /dev/null +++ b/crates/ml/examples/hyperopt_baseline_supervised.rs @@ -0,0 +1,294 @@ +//! Hyperopt Runner for Supervised Models on Parquet Data +//! +//! Runs hyperparameter optimization using Particle Swarm Optimization (PSO) for +//! TFT, MAMBA-2, or both models. This binary is the supervised counterpart to +//! `hyperopt_baseline_rl` (which handles DQN/PPO on DBN data). +//! +//! ## Usage +//! +//! ```bash +//! # Run TFT hyperopt +//! SQLX_OFFLINE=true cargo run -p ml --example hyperopt_baseline_supervised --release -- \ +//! --model tft --parquet-file data/ES_FUT_180d.parquet \ +//! --trials 20 --epochs 20 +//! +//! # Run Mamba2 hyperopt +//! SQLX_OFFLINE=true cargo run -p ml --example hyperopt_baseline_supervised --release -- \ +//! --model mamba2 --parquet-file data/ES_FUT_180d.parquet \ +//! --trials 20 --epochs 10 +//! +//! # Run both models +//! SQLX_OFFLINE=true cargo run -p ml --example hyperopt_baseline_supervised --release -- \ +//! --model both --parquet-file data/ES_FUT_180d.parquet +//! ``` +//! +//! ## Output +//! +//! Results are written as JSON to `--output` (default: `ml/trained_models/hyperopt_results.json`). + +#![allow(unused_crate_dependencies)] + +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::mamba2::Mamba2Trainer; +use ml::hyperopt::adapters::tft::TFTTrainer; +use ml::hyperopt::paths::{generate_run_id, TrainingPaths}; +use ml::hyperopt::ArgminOptimizer; + +/// Hyperparameter optimization runner for supervised models +#[derive(Parser, Debug)] +#[command(name = "hyperopt-baseline-supervised")] +#[command(about = "Run hyperparameter optimization for supervised models (TFT, Mamba2)")] +struct Args { + /// Model to optimize: "tft", "mamba2", or "both" + #[arg(long, default_value = "both")] + model: String, + + /// Path to Parquet file with OHLCV data + #[arg(long)] + parquet_file: 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 + #[arg(long, default_value = "20")] + epochs: usize, + + /// 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, + + /// Early stopping patience (epochs without improvement) + #[arg(long, default_value = "10")] + early_stopping_patience: usize, +} + +/// 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_tft_hyperopt(args: &Args) -> Result { + info!("========================================"); + info!(" TFT Hyperparameter Optimization"); + info!("========================================"); + + let run_id = generate_run_id("hyperopt-tft"); + let training_paths = TrainingPaths::new(&args.base_dir, "tft", &run_id); + + info!("Run ID: {}", run_id); + info!("Parquet file: {}", args.parquet_file); + info!("Epochs per trial: {}", args.epochs); + info!("Trials: {}", args.trials); + + let trainer = TFTTrainer::new(&args.parquet_file, args.epochs) + .context("Failed to create TFT trainer")? + .with_early_stopping(args.early_stopping_patience) + .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("TFT hyperopt optimization failed")?; + let elapsed = start.elapsed().as_secs_f64(); + + info!("TFT 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_mamba2_hyperopt(args: &Args) -> Result { + info!("========================================"); + info!(" Mamba2 Hyperparameter Optimization"); + info!("========================================"); + + let run_id = generate_run_id("hyperopt-mamba2"); + let training_paths = TrainingPaths::new(&args.base_dir, "mamba2", &run_id); + + info!("Run ID: {}", run_id); + info!("Parquet file: {}", args.parquet_file); + info!("Epochs per trial: {}", args.epochs); + info!("Trials: {}", args.trials); + + let trainer = Mamba2Trainer::new(&args.parquet_file, args.epochs) + .context("Failed to create Mamba2 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("Mamba2 hyperopt optimization failed")?; + let elapsed = start.elapsed().as_secs_f64(); + + info!("Mamba2 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<()> { + tracing_subscriber::fmt() + .with_max_level(Level::INFO) + .with_target(false) + .init(); + + let args = Args::parse(); + + info!("========================================"); + info!(" Hyperopt Baseline Supervised Runner"); + info!("========================================"); + info!("Model: {}", args.model); + info!("Parquet file: {}", args.parquet_file); + info!("Trials: {}", args.trials); + info!("Initial LHS samples: {}", args.n_initial); + info!("Epochs per trial: {}", args.epochs); + info!("Output: {}", args.output.display()); + info!("Seed: {}", args.seed); + + // Validate parquet file exists + if !std::path::Path::new(&args.parquet_file).exists() { + anyhow::bail!( + "Parquet file not found: {}. Provide a valid path via --parquet-file.", + args.parquet_file + ); + } + + // Validate model selection + let run_tft = args.model == "tft" || args.model == "both"; + let run_mamba2 = args.model == "mamba2" || args.model == "both"; + + if !run_tft && !run_mamba2 { + anyhow::bail!( + "Invalid --model value '{}'. Must be 'tft', 'mamba2', or 'both'.", + args.model + ); + } + + // Verify trials > n_initial + 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 mut results = serde_json::Map::new(); + + if run_tft { + match run_tft_hyperopt(&args) { + Ok(tft_result) => { + results.insert("tft".to_string(), tft_result); + } + Err(e) => { + error!("TFT hyperopt failed: {:#}", e); + warn!("Continuing with remaining models..."); + results.insert( + "tft".to_string(), + serde_json::json!({ "error": format!("{:#}", e) }), + ); + } + } + } + + if run_mamba2 { + match run_mamba2_hyperopt(&args) { + Ok(mamba2_result) => { + results.insert("mamba2".to_string(), mamba2_result); + } + Err(e) => { + error!("Mamba2 hyperopt failed: {:#}", e); + warn!("Continuing..."); + results.insert( + "mamba2".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")?; + + 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(()) +} diff --git a/crates/ml/examples/hyperopt_continuous_ppo_demo.rs b/crates/ml/examples/hyperopt_continuous_ppo_demo.rs deleted file mode 100644 index 99f6c1285..000000000 --- a/crates/ml/examples/hyperopt_continuous_ppo_demo.rs +++ /dev/null @@ -1,266 +0,0 @@ -//! Continuous PPO Hyperparameter Optimization Demo -//! -//! This example demonstrates the continuous PPO hyperparameter optimization adapter -//! using the generic egobox optimization framework. -//! -//! # Usage -//! -//! ```bash -//! # Run with default settings (5 trials, 10 epochs) -//! cargo run -p ml --example hyperopt_continuous_ppo_demo --release --features cuda -- \ -//! --parquet-file test_data/ES_FUT_180d.parquet -//! -//! # Custom trials and epochs -//! cargo run -p ml --example hyperopt_continuous_ppo_demo --release --features cuda -- \ -//! --parquet-file test_data/ES_FUT_180d.parquet \ -//! --trials 30 \ -//! --epochs 50 -//! ``` -//! -//! # Expected Output -//! -//! - Real continuous PPO training with market data -//! - Varying Sharpe ratios across trials -//! - Convergence visible (best Sharpe improves) -//! - Logs showing actual PPO training steps -//! - GPU utilization (if CUDA available) - -use anyhow::Result; -use clap::Parser; -use std::path::PathBuf; -use tracing::{info, Level}; - -use ml::hyperopt::adapters::continuous_ppo::{ContinuousPPOParams, ContinuousPPOTrainer}; -use ml::hyperopt::paths::{generate_run_id, TrainingPaths}; -use ml::hyperopt::traits::ParameterSpace; -use ml::hyperopt::EgoboxOptimizer; - -/// CLI arguments -#[derive(Parser, Debug)] -#[command( - name = "hyperopt_continuous_ppo_demo", - about = "Continuous PPO hyperparameter optimization demonstration" -)] -struct Args { - /// Path to Parquet file with OHLCV data - #[arg(long, help = "Path to Parquet file with OHLCV data")] - parquet_file: String, - - /// Number of optimization trials - #[arg(long, default_value = "5", help = "Number of optimization trials")] - trials: usize, - - /// Epochs per trial - #[arg(long, default_value = "10", help = "Training epochs per trial")] - epochs: usize, - - /// Base directory for training outputs - #[arg( - long, - default_value = "/tmp/ml_training", - help = "Base directory for training outputs" - )] - base_dir: PathBuf, - - /// Run ID (auto-generated if not provided) - #[arg(long, help = "Unique run ID (YYYYMMDD_HHMMSS_type if not provided)")] - run_id: Option, - - /// Run type for auto-generated run ID - #[arg( - long, - default_value = "hyperopt_continuous_ppo", - help = "Run type for auto-generated run ID" - )] - run_type: String, -} - -fn main() -> Result<()> { - // Initialize tracing - tracing_subscriber::fmt() - .with_max_level(Level::INFO) - .with_target(false) - .with_thread_ids(false) - .init(); - - info!("╔═══════════════════════════════════════════════════════════╗"); - info!("║ Continuous PPO Hyperparameter Optimization Demo ║"); - info!("╚═══════════════════════════════════════════════════════════╝"); - info!(""); - - // Parse arguments - let args = Args::parse(); - - info!("Configuration:"); - info!(" Trials: {}", args.trials); - info!(" Epochs per trial: {}", args.epochs); - info!(" Parquet file: {}", args.parquet_file); - info!(""); - - // Validate Parquet file exists - let parquet_path = std::path::Path::new(&args.parquet_file); - if !parquet_path.exists() { - anyhow::bail!("Parquet file not found: {}", args.parquet_file); - } - - // Generate run ID if not provided - let run_id = args - .run_id - .unwrap_or_else(|| generate_run_id(&args.run_type)); - - // Create training paths - let training_paths = TrainingPaths::new(&args.base_dir, "continuous_ppo", &run_id); - - // Create all directories - training_paths - .create_all() - .map_err(|e| anyhow::anyhow!("Failed to create training directories: {}", e))?; - - info!("Training Paths:"); - info!(" Base directory: {:?}", args.base_dir); - info!(" Run ID: {}", run_id); - info!(" Run directory: {:?}", training_paths.run_dir()); - info!(" Checkpoints: {:?}", training_paths.checkpoints_dir()); - info!(""); - - // Create continuous PPO trainer with training paths - let trainer = ContinuousPPOTrainer::new(&args.parquet_file, args.epochs)? - .with_training_paths(training_paths); - - info!("Parameter Space:"); - let names = ContinuousPPOParams::param_names(); - let cont_bounds = ContinuousPPOParams::continuous_bounds(); - let int_bounds = ContinuousPPOParams::integer_bounds(); - let cat_choices = ContinuousPPOParams::categorical_choices(); - - info!(" Continuous Parameters:"); - for (i, name) in names.iter().take(cont_bounds.len()).enumerate() { - let (min, max) = cont_bounds[i]; - info!(" {}: [{:.6}, {:.6}]", name, min, max); - } - - info!(" Integer Parameters:"); - for (i, name) in names - .iter() - .skip(cont_bounds.len()) - .take(int_bounds.len()) - .enumerate() - { - let (min, max) = int_bounds[i]; - info!(" {}: [{}, {}]", name, min, max); - } - - info!(" Categorical Parameters:"); - for (i, name) in names - .iter() - .skip(cont_bounds.len() + int_bounds.len()) - .take(cat_choices.len()) - .enumerate() - { - let choices = &cat_choices[i]; - info!(" {}: {:?}", name, choices); - } - info!(""); - - // Create optimizer - let optimizer = EgoboxOptimizer::with_trials(args.trials, 3); - - info!("Starting optimization..."); - info!(""); - - // Run optimization - let result = optimizer.optimize(trainer)?; - - info!(""); - info!("╔═══════════════════════════════════════════════════════════╗"); - info!("║ Optimization Complete ║"); - info!("╚═══════════════════════════════════════════════════════════╝"); - info!(""); - info!("Best Parameters:"); - info!(" Policy LR: {:.6}", result.best_params.policy_lr); - info!(" Value LR: {:.6}", result.best_params.value_lr); - info!( - " Action bounds: [{:.2}, {:.2}]", - result.best_params.action_min, result.best_params.action_max - ); - info!(" Init log std: {:.2}", result.best_params.init_log_std); - info!(" Learnable std: {}", result.best_params.learnable_std); - info!(" Clip epsilon: {:.3}", result.best_params.clip_epsilon); - info!(" Entropy coeff: {:.6}", result.best_params.entropy_coeff); - info!(" GAE lambda: {:.3}", result.best_params.gae_lambda); - info!(" Gamma: {:.3}", result.best_params.gamma); - info!(" Batch size: {}", result.best_params.batch_size); - info!(" Num epochs: {}", result.best_params.num_epochs); - info!(""); - info!( - "Best Objective (Sharpe ratio): {:.6}", - -result.best_objective - ); // Negated (optimizer minimizes) - info!("Total Evaluations: {}", result.all_trials.len()); - info!(""); - - // Print trial history for convergence analysis - info!("Trial History:"); - info!("┌───────┬──────────────────┬──────────────────┬──────────────────┐"); - info!("│ Trial │ Policy LR │ Value LR │ Sharpe Ratio │"); - info!("├───────┼──────────────────┼──────────────────┼──────────────────┤"); - - for trial in &result.all_trials { - info!( - "│ {:5} │ {:16.6} │ {:16.6} │ {:16.6} │", - trial.trial_num, - trial.params.policy_lr, - trial.params.value_lr, - -trial.objective // Negate to show actual Sharpe - ); - } - - info!("└───────┴──────────────────┴──────────────────┴──────────────────┘"); - info!(""); - - // Compute convergence metrics - if result.all_trials.len() >= 2 { - let first_sharpe = -result.all_trials[0].objective; - let best_sharpe = -result.best_objective; - let improvement = ((best_sharpe - first_sharpe) / first_sharpe.abs().max(1.0)) * 100.0; - - info!("Convergence Analysis:"); - info!(" First Trial Sharpe: {:.6}", first_sharpe); - info!(" Best Trial Sharpe: {:.6}", best_sharpe); - info!(" Improvement: {:.2}%", improvement); - info!(""); - - // Compute variance in Sharpe values - let sharpe_values: Vec = result.all_trials.iter().map(|e| -e.objective).collect(); - let mean_sharpe: f64 = sharpe_values.iter().sum::() / sharpe_values.len() as f64; - let variance: f64 = sharpe_values - .iter() - .map(|s| (s - mean_sharpe).powi(2)) - .sum::() - / sharpe_values.len() as f64; - let std_dev = variance.sqrt(); - let coeff_var = (std_dev / mean_sharpe.abs().max(0.001)) * 100.0; - - info!("Sharpe Variance Analysis:"); - info!(" Mean Sharpe: {:.6}", mean_sharpe); - info!(" Std Dev: {:.6}", std_dev); - info!(" Coefficient of Variation: {:.2}%", coeff_var); - info!(""); - - if coeff_var < 5.0 { - info!( - "⚠️ WARNING: Low Sharpe variance ({:.2}%) suggests mock metrics", - coeff_var - ); - } else { - info!( - "✓ Sharpe variance ({:.2}%) confirms real training", - coeff_var - ); - } - } - - info!("✓ Continuous PPO hyperparameter optimization demo complete"); - - Ok(()) -} diff --git a/crates/ml/examples/hyperopt_dqn_demo.rs b/crates/ml/examples/hyperopt_dqn_demo.rs deleted file mode 100644 index f79ea36eb..000000000 --- a/crates/ml/examples/hyperopt_dqn_demo.rs +++ /dev/null @@ -1,297 +0,0 @@ -//! DQN Hyperparameter Optimization Demo -//! -//! This example demonstrates how to use the argmin-based hyperparameter -//! optimization framework with DQN. It runs a small-scale optimization -//! to show the complete workflow with REAL training (not mock metrics). -//! -//! ## Usage -//! -//! ```bash -//! # Quick test with small DBN directory (5-10 minutes) -//! cargo run -p ml --example hyperopt_dqn_demo --release --features cuda -- \ -//! --dbn-data-dir test_data/real/databento/ml_training_small \ -//! --trials 3 \ -//! --epochs 5 -//! -//! # Production run with full optimization (1-2 hours) -//! cargo run -p ml --example hyperopt_dqn_demo --release --features cuda -- \ -//! --dbn-data-dir test_data/real/databento/ml_training \ -//! --trials 30 \ -//! --epochs 50 -//! ``` -//! -//! ## Output -//! -//! The example will: -//! 1. Initialize DQN trainer with specified Parquet file -//! 2. Run argmin optimization with Nelder-Mead simplex -//! 3. Display trial results including loss and parameter values -//! 4. Report best hyperparameters found -//! 5. Show convergence and top trials -//! -//! ## Verification -//! -//! This example uses REAL training via `InternalDQNTrainer`, not mock metrics. -//! You should see: -//! - Loss values VARY across trials (not identical) -//! - Training takes time (not instant) -//! - GPU utilization visible (if CUDA available) -//! - Convergence over trials (best loss improves) - -use anyhow::Result; -use clap::Parser; -use ml::hyperopt::adapters::dqn::DQNTrainer; -use ml::hyperopt::paths::{generate_run_id, TrainingPaths}; -use std::path::PathBuf; -use ml::hyperopt::ArgminOptimizer; -use tracing::{info, Level}; -use tracing_subscriber; - -#[derive(Parser, Debug)] -#[command(name = "DQN Hyperparameter Optimization Demo")] -#[command(about = "Demonstrates argmin-based hyperparameter optimization for DQN")] -struct Args { - /// Path to Parquet file with OHLCV data - #[arg(long)] - parquet_file: String, - - /// Number of optimization trials (default: 10) - #[arg(long, default_value = "10")] - trials: usize, - - /// Number of training epochs per trial (default: 20) - #[arg(long, default_value = "20")] - epochs: usize, - - /// Number of initial random samples (default: 2) - #[arg(long, default_value = "2")] - n_initial: usize, - - /// Random seed for reproducibility (default: 42) - #[arg(long, default_value = "42")] - seed: u64, - - /// Base directory for training outputs (default: /tmp/ml_training) - #[arg(long, default_value = "/tmp/ml_training")] - base_dir: String, - - /// Run ID for organizing outputs (default: auto-generated) - #[arg(long)] - run_id: Option, - - /// Run type for run ID generation (default: hyperopt) - #[arg(long, default_value = "hyperopt")] - run_type: String, - - /// Early stopping plateau window (epochs to check for improvement) - #[arg(long, default_value = "5")] - early_stopping_plateau_window: usize, - - /// Early stopping minimum epochs (minimum epochs before early stopping can trigger) - /// Default: 1000 (effectively disabled - Wave 7 validation proved early stopping kills 8-10 profitable trials) - #[arg(long, default_value = "1000")] - early_stopping_min_epochs: usize, - - /// Optional path to feature cache directory for faster hyperopt - /// - /// Pre-compute cache with: cargo run -p ml --example cache_dqn_features - /// - /// Expected speedup: 2m 25s → <1s per trial (99% reduction) - #[arg(long)] - feature_cache_dir: Option, -} - -fn estimate_runtime(trials: usize, epochs: usize) -> usize { - // DQN training is faster than MAMBA-2 - // Rough estimate: ~0.5 min per trial per 10 epochs - let mins_per_trial = (epochs as f64 / 10.0) * 0.5; - (trials as f64 * mins_per_trial).ceil() as usize -} - -fn main() -> Result<()> { - // Initialize tracing - tracing_subscriber::fmt() - .with_max_level(Level::INFO) - .with_target(false) - .init(); - - // Parse arguments - let args = Args::parse(); - - info!("========================================"); - info!("DQN Hyperparameter Optimization Demo"); - info!("========================================"); - info!("Configuration:"); - info!(" Parquet file: {}", args.parquet_file); - info!(" Trials: {}", args.trials); - info!(" Epochs per trial: {}", args.epochs); - info!(" Initial samples: {}", args.n_initial); - info!(" Random seed: {}", args.seed); - info!(" Base directory: {}", args.base_dir); - info!(""); - - // Validate Parquet file exists - let parquet_path = std::path::Path::new(&args.parquet_file); - if !parquet_path.exists() { - anyhow::bail!("Parquet file not found: {}", args.parquet_file); - } - - // Generate run ID - let run_id = args - .run_id - .unwrap_or_else(|| generate_run_id(&args.run_type)); - info!("Run ID: {}", run_id); - - // Create training paths - let training_paths = TrainingPaths::new(&args.base_dir, "dqn", &run_id); - info!("Training paths:"); - info!(" Run directory: {:?}", training_paths.run_dir()); - info!(" Checkpoints: {:?}", training_paths.checkpoints_dir()); - info!(" Hyperopt: {:?}", training_paths.hyperopt_dir()); - info!(""); - - // Create trainer with parquet file path directly - info!( - "Creating DQN trainer with parquet file: {}", - args.parquet_file - ); - let mut trainer = DQNTrainer::new(&args.parquet_file, args.epochs)? - .with_early_stopping( - args.early_stopping_plateau_window, - args.early_stopping_min_epochs, - ); - - // Enable feature cache if provided - if let Some(cache_dir) = args.feature_cache_dir { - info!("🚀 Enabling feature cache: {:?}", cache_dir); - trainer = trainer.with_feature_cache(cache_dir); - } - - let trainer = trainer.with_training_paths(training_paths); - - // Create optimizer - info!("Initializing argmin optimizer..."); - let optimizer = ArgminOptimizer::builder() - .max_trials(args.trials) - .n_initial(args.n_initial) - .seed(args.seed) - .build(); - - // Run optimization - info!(""); - info!("Starting optimization (this may take a while)..."); - info!( - "Expected runtime: ~{} minutes", - estimate_runtime(args.trials, args.epochs) - ); - info!(""); - info!("VERIFICATION CHECKS:"); - info!(" ✓ Each trial should take >1 second (real training)"); - info!(" ✓ Loss values should VARY across trials"); - info!(" ✓ Best loss should improve over trials"); - info!(" ✓ GPU utilization should be visible (if CUDA available)"); - info!(""); - - let result = optimizer.optimize(trainer)?; - - // Display results - info!(""); - info!("========================================"); - info!("Optimization Complete!"); - info!("========================================"); - info!(""); - info!("Best Hyperparameters:"); - info!(" Learning rate: {:.6}", result.best_params.learning_rate); - info!(" Batch size: {}", result.best_params.batch_size); - info!(" Gamma: {:.3}", result.best_params.gamma); - info!(" Buffer size: {}", result.best_params.buffer_size); - info!(""); - info!("Performance:"); - info!(" Best episode reward: {:.6}", -result.best_objective); // Negate to get actual reward - info!(" Total trials: {}", result.all_trials.len()); - - // Find convergence trial (where best was found) - let convergence_trial = result - .all_trials - .iter() - .position(|t| (t.objective - result.best_objective).abs() < 1e-10) - .unwrap_or(0); - info!(" Convergence: {} trials to best", convergence_trial + 1); - info!(""); - - // Show top 5 trials (sorted by reward descending = objective ascending) - if result.all_trials.len() >= 5 { - info!("Top 5 Trials (by episode reward):"); - let mut sorted_trials = result.all_trials.clone(); - sorted_trials.sort_by(|a, b| a.objective.partial_cmp(&b.objective).unwrap()); - - for (i, trial) in sorted_trials.iter().take(5).enumerate() { - info!( - " {}. Reward: {:.6} (LR: {:.6}, BS: {}, Gamma: {:.3})", - i + 1, - -trial.objective, // Negate to show actual reward - trial.params.learning_rate, - trial.params.batch_size, - trial.params.gamma - ); - } - info!(""); - } - - // Validation check: Verify reward variance (objectives are negated rewards) - let objectives: Vec = result.all_trials.iter().map(|t| t.objective).collect(); - let rewards: Vec = objectives.iter().map(|o| -o).collect(); - let mean_reward = rewards.iter().sum::() / rewards.len() as f64; - let variance = rewards - .iter() - .map(|r| (r - mean_reward).powi(2)) - .sum::() - / rewards.len() as f64; - let std_dev = variance.sqrt(); - - info!("VERIFICATION RESULTS:"); - info!(" Mean reward: {:.6}", mean_reward); - info!(" Std deviation: {:.6}", std_dev); - info!( - " Min reward: {:.6}", - rewards.iter().cloned().fold(f64::NEG_INFINITY, f64::max) - ); - info!( - " Max reward: {:.6}", - rewards.iter().cloned().fold(f64::INFINITY, f64::min) - ); - info!(""); - - if std_dev < 1e-6 { - info!("⚠️ WARNING: Reward values are identical across trials!"); - info!(" This suggests mock metrics are being used instead of real training."); - info!(" Expected: std_dev > 0.001 for real training"); - } else { - info!("✅ VERIFIED: Reward values vary across trials (real training confirmed)"); - info!( - " Coefficient of variation: {:.2}%", - (std_dev / mean_reward.abs()) * 100.0 - ); - } - info!(""); - - // Calculate improvement over default - let default_reward = rewards[0]; // First trial uses near-default params - let best_reward = -result.best_objective; - let improvement_pct = ((best_reward - default_reward) / default_reward.abs()) * 100.0; - - info!("Improvement:"); - info!(" Initial (near-default): {:.6}", default_reward); - info!(" Best (optimized): {:.6}", best_reward); - info!(" Improvement: {:.2}%", improvement_pct); - info!(""); - - info!("========================================"); - info!("Next Steps:"); - info!(" 1. Review hyperparameters above"); - info!(" 2. Run full optimization with --trials 30 --epochs 50"); - info!(" 3. Deploy best params to production DQN config"); - info!("========================================"); - - Ok(()) -} diff --git a/crates/ml/examples/hyperopt_mamba2_demo.rs b/crates/ml/examples/hyperopt_mamba2_demo.rs deleted file mode 100644 index 1da3c4af9..000000000 --- a/crates/ml/examples/hyperopt_mamba2_demo.rs +++ /dev/null @@ -1,228 +0,0 @@ -//! MAMBA-2 Hyperparameter Optimization Demo -//! -//! This example demonstrates how to use the argmin-based hyperparameter -//! optimization framework with MAMBA-2. It runs a small-scale optimization -//! to show the complete workflow. -//! -//! ## Usage -//! -//! ```bash -//! # Local training with small dataset (default /tmp) -//! cargo run -p ml --example hyperopt_mamba2_demo --release --features cuda -- \ -//! --parquet-file test_data/ES_FUT_small.parquet \ -//! --trials 4 --epochs 3 -//! -//! # Runpod training with custom base dir -//! ./hyperopt_mamba2_demo \ -//! --parquet-file /runpod-volume/datasets/parquet/futures/ES_FUT_180d.parquet \ -//! --base-dir /runpod-volume \ -//! --trials 30 --epochs 50 -//! -//! # Resume from specific run -//! ./hyperopt_mamba2_demo \ -//! --base-dir /runpod-volume \ -//! --run-id 20251028_223000_hyperopt \ -//! --parquet-file test_data/ES_FUT_180d.parquet \ -//! --trials 30 --epochs 50 -//! ``` -//! -//! ## Output -//! -//! The example will: -//! 1. Initialize MAMBA-2 trainer with specified Parquet file -//! 2. Run argmin optimization with Nelder-Mead simplex -//! 3. Display trial results including loss and parameter values -//! 4. Report best hyperparameters found -//! 5. Show expected improvement vs default parameters - -use anyhow::Result; -use clap::Parser; -use ml::hyperopt::adapters::mamba2::Mamba2Trainer; -use ml::hyperopt::paths::{generate_run_id, TrainingPaths}; -use ml::hyperopt::ArgminOptimizer; -use tracing::{info, Level}; -use tracing_subscriber; - -#[derive(Parser, Debug)] -#[command(name = "MAMBA-2 Hyperparameter Optimization Demo")] -#[command(about = "Demonstrates argmin-based hyperparameter optimization for MAMBA-2")] -struct Args { - /// Path to Parquet file with OHLCV data - #[arg(long)] - parquet_file: String, - - /// Number of optimization trials (default: 10) - #[arg(long, default_value = "10")] - trials: usize, - - /// Number of training epochs per trial (default: 20) - #[arg(long, default_value = "20")] - epochs: usize, - - /// Number of initial random samples (default: 3) - #[arg(long, default_value = "3")] - n_initial: usize, - - /// Random seed for reproducibility (default: 42) - #[arg(long, default_value = "42")] - seed: u64, - - /// Minimum batch size (default: 4) - #[arg(long, default_value = "4")] - batch_size_min: usize, - - /// Maximum batch size for GPU memory constraints (default: 96 for RTX A4000 16GB) - /// Examples: RTX 3050 Ti 4GB = 32, RTX A4000 16GB = 96, RTX 4090 24GB = 256 - #[arg(long, default_value = "96")] - batch_size_max: usize, - - /// Base directory for training outputs (e.g., /runpod-volume) - #[arg(long, default_value = "/tmp/ml_training")] - base_dir: String, - - /// Run ID (auto-generated if not provided) - #[arg(long)] - run_id: Option, - - /// Run type (hyperopt, production, test) - #[arg(long, default_value = "hyperopt")] - run_type: String, - - /// Early stopping patience (epochs without improvement before stopping) - #[arg(long, default_value = "5")] - early_stopping_patience: usize, - - /// Early stopping minimum epochs (minimum epochs before early stopping can trigger) - #[arg(long, default_value = "5")] - early_stopping_min_epochs: usize, -} - -fn main() -> Result<()> { - // Initialize tracing - tracing_subscriber::fmt() - .with_max_level(Level::INFO) - .with_target(false) - .init(); - - // Parse arguments - let args = Args::parse(); - - // Generate run ID if not provided - let run_id = args - .run_id - .unwrap_or_else(|| generate_run_id(&args.run_type)); - - // Create training paths - let training_paths = TrainingPaths::new(&args.base_dir, "mamba2", &run_id); - - info!("========================================"); - info!("MAMBA-2 Hyperparameter Optimization Demo"); - info!("========================================"); - info!("Configuration:"); - info!(" Parquet file: {}", args.parquet_file); - info!(" Trials: {}", args.trials); - info!(" Epochs per trial: {}", args.epochs); - info!(" Initial samples: {}", args.n_initial); - info!(" Random seed: {}", args.seed); - info!( - " Batch size bounds: [{}, {}]", - args.batch_size_min, args.batch_size_max - ); - info!(""); - info!("Training Paths:"); - info!(" Run ID: {}", run_id); - info!(" Base directory: {}", args.base_dir); - info!(" Run directory: {:?}", training_paths.run_dir()); - info!(" Checkpoints: {:?}", training_paths.checkpoints_dir()); - info!(" Logs: {:?}", training_paths.logs_dir()); - info!(" Hyperopt: {:?}", training_paths.hyperopt_dir()); - info!(""); - - // Create trainer with training paths - info!("Creating MAMBA-2 trainer..."); - let trainer = Mamba2Trainer::new(&args.parquet_file, args.epochs)? - .with_batch_size_bounds(args.batch_size_min as f64, args.batch_size_max as f64) - .with_early_stopping(args.early_stopping_patience, args.early_stopping_min_epochs) - .with_training_paths(training_paths); - - // Create optimizer - info!("Initializing argmin optimizer..."); - let optimizer = ArgminOptimizer::builder() - .max_trials(args.trials) - .n_initial(args.n_initial) - .seed(args.seed) - .build(); - - // Run optimization - info!(""); - info!("Starting optimization (this may take a while)..."); - info!( - "Expected runtime: ~{} minutes", - estimate_runtime(args.trials, args.epochs) - ); - info!(""); - - let result = optimizer.optimize(trainer)?; - - // Display results - info!(""); - info!("========================================"); - info!("Optimization Complete!"); - info!("========================================"); - info!(""); - info!("Best Hyperparameters:"); - info!(" Learning rate: {:.6}", result.best_params.learning_rate); - info!(" Batch size: {}", result.best_params.batch_size); - info!(" Dropout: {:.3}", result.best_params.dropout); - info!(" Weight decay: {:.6}", result.best_params.weight_decay); - info!(""); - info!("Performance:"); - info!(" Best validation loss: {:.6}", result.best_objective); - info!(" Total trials: {}", result.all_trials.len()); - - // Find convergence trial (where best was found) - let convergence_trial = result - .all_trials - .iter() - .position(|t| (t.objective - result.best_objective).abs() < 1e-10) - .unwrap_or(0); - info!(" Convergence: {} trials to best", convergence_trial + 1); - info!(""); - - // Show top 5 trials - if result.all_trials.len() >= 5 { - info!("Top 5 Trials:"); - let mut sorted_trials = result.all_trials.clone(); - sorted_trials.sort_by(|a, b| a.objective.partial_cmp(&b.objective).unwrap()); - - for (i, trial) in sorted_trials.iter().take(5).enumerate() { - info!( - " {}. Loss: {:.6} (LR: {:.6}, BS: {}, Dropout: {:.3})", - i + 1, - trial.objective, - trial.params.learning_rate, - trial.params.batch_size, - trial.params.dropout - ); - } - } - - info!(""); - info!("========================================"); - info!("Next Steps:"); - info!("========================================"); - info!("1. Use best parameters for production training"); - info!("2. Run longer optimization (50+ trials) for better results"); - info!("3. Validate on holdout dataset"); - info!("4. Deploy optimized model to trading system"); - - Ok(()) -} - -/// Estimate runtime based on trials and epochs -fn estimate_runtime(trials: usize, epochs: usize) -> usize { - // Rough estimate: 2 min per 50 epochs on RTX 3050 Ti - let minutes_per_trial = (epochs as f64 / 50.0) * 2.0; - let total_minutes = (trials as f64 * minutes_per_trial).ceil() as usize; - total_minutes -} diff --git a/crates/ml/examples/hyperopt_ppo_demo.rs b/crates/ml/examples/hyperopt_ppo_demo.rs deleted file mode 100644 index 3a5e542e4..000000000 --- a/crates/ml/examples/hyperopt_ppo_demo.rs +++ /dev/null @@ -1,245 +0,0 @@ -//! PPO Hyperparameter Optimization Demo -//! -//! This example demonstrates the PPO hyperparameter optimization adapter -//! using the generic egobox optimization framework. -//! -//! # Usage -//! -//! ```bash -//! # Run with default settings (3 trials, 1000 episodes) -//! cargo run -p ml --example hyperopt_ppo_demo --release --features cuda -//! -//! # Custom trials and episodes -//! cargo run -p ml --example hyperopt_ppo_demo --release --features cuda -- \ -//! --trials 5 \ -//! --episodes 500 -//! ``` -//! -//! # Expected Output -//! -//! - Real PPO training with synthetic trajectories -//! - Varying loss values across trials (not hardcoded) -//! - Convergence visible (best metric improves) -//! - Logs showing actual PPO training steps -//! - GPU utilization (if CUDA available) - -use anyhow::Result; -use clap::Parser; -use std::path::PathBuf; -use tracing::{info, Level}; - -use ml::hyperopt::adapters::ppo::{PPOParams, PPOTrainer}; -use ml::hyperopt::paths::{generate_run_id, TrainingPaths}; -use ml::hyperopt::traits::ParameterSpace; -use ml::hyperopt::EgoboxOptimizer; - -/// CLI arguments -#[derive(Parser, Debug)] -#[command( - name = "hyperopt_ppo_demo", - about = "PPO hyperparameter optimization demonstration" -)] -struct Args { - /// Number of optimization trials - #[arg(long, default_value = "3", help = "Number of optimization trials")] - trials: usize, - - /// Episodes per trial - #[arg(long, default_value = "1000", help = "Training episodes per trial")] - episodes: usize, - - /// Path to Parquet file with OHLCV data - #[arg(long, help = "Path to Parquet file with OHLCV data")] - parquet_file: String, - - /// Base directory for training outputs - #[arg( - long, - default_value = "/tmp/ml_training", - help = "Base directory for training outputs" - )] - base_dir: PathBuf, - - /// Run ID (auto-generated if not provided) - #[arg(long, help = "Unique run ID (YYYYMMDD_HHMMSS_type if not provided)")] - run_id: Option, - - /// Run type for auto-generated run ID - #[arg( - long, - default_value = "hyperopt", - help = "Run type for auto-generated run ID" - )] - run_type: String, - - /// Early stopping patience (epochs without improvement before stopping) - #[arg(long, default_value = "5")] - early_stopping_patience: usize, - - /// Early stopping minimum epochs (minimum epochs before early stopping can trigger) - #[arg(long, default_value = "5")] - early_stopping_min_epochs: usize, -} - -fn main() -> Result<()> { - // Initialize tracing - tracing_subscriber::fmt() - .with_max_level(Level::INFO) - .with_target(false) - .with_thread_ids(false) - .init(); - - info!("╔═══════════════════════════════════════════════════════════╗"); - info!("║ PPO Hyperparameter Optimization Demo ║"); - info!("╚═══════════════════════════════════════════════════════════╝"); - info!(""); - - // Parse arguments - let args = Args::parse(); - - info!("Configuration:"); - info!(" Trials: {}", args.trials); - info!(" Episodes per trial: {}", args.episodes); - info!(" Parquet file: {}", args.parquet_file); - info!(""); - - // Generate run ID if not provided - let run_id = args - .run_id - .unwrap_or_else(|| generate_run_id(&args.run_type)); - - // Create training paths - let training_paths = TrainingPaths::new(&args.base_dir, "ppo", &run_id); - - // Create all directories - training_paths - .create_all() - .map_err(|e| anyhow::anyhow!("Failed to create training directories: {}", e))?; - - info!("Training Paths:"); - info!(" Base directory: {:?}", args.base_dir); - info!(" Run ID: {}", run_id); - info!(" Run directory: {:?}", training_paths.run_dir()); - info!(" Checkpoints: {:?}", training_paths.checkpoints_dir()); - info!(""); - - // Validate Parquet file exists and extract directory - let parquet_path = std::path::Path::new(&args.parquet_file); - if !parquet_path.exists() { - anyhow::bail!("Parquet file not found: {}", args.parquet_file); - } - - let data_dir = parquet_path - .parent() - .ok_or_else(|| anyhow::anyhow!("Failed to extract directory from parquet file path"))?; - - // Create PPO trainer with training paths - let trainer = PPOTrainer::new(data_dir, args.episodes)? - .with_early_stopping(args.early_stopping_patience, args.early_stopping_min_epochs) - .with_training_paths(training_paths); - - info!("Parameter Space:"); - let names = PPOParams::param_names(); - let bounds = PPOParams::continuous_bounds(); - for (name, (min, max)) in names.iter().zip(bounds.iter()) { - info!(" {}: [{:.6}, {:.6}]", name, min, max); - } - info!(""); - - // Create optimizer - let optimizer = EgoboxOptimizer::with_trials(args.trials, 3); - - info!("Starting optimization..."); - info!(""); - - // Run optimization - let result = optimizer.optimize(trainer)?; - - info!(""); - info!("╔═══════════════════════════════════════════════════════════╗"); - info!("║ Optimization Complete ║"); - info!("╚═══════════════════════════════════════════════════════════╝"); - info!(""); - info!("Best Parameters:"); - info!( - " Policy LR: {:.6}", - result.best_params.policy_learning_rate - ); - info!(" Value LR: {:.6}", result.best_params.value_learning_rate); - info!(" Clip epsilon: {:.3}", result.best_params.clip_epsilon); - info!( - " Value loss coeff: {:.3}", - result.best_params.value_loss_coeff - ); - info!(" Entropy coeff: {:.6}", result.best_params.entropy_coeff); - info!(""); - info!( - "Best Objective (combined loss): {:.6}", - result.best_objective - ); - info!("Total Evaluations: {}", result.all_trials.len()); - info!(""); - - // Print trial history for convergence analysis - info!("Trial History:"); - info!("┌───────┬──────────────────┬──────────────────┬──────────────────┐"); - info!("│ Trial │ Policy LR │ Value LR │ Combined Loss │"); - info!("├───────┼──────────────────┼──────────────────┼──────────────────┤"); - - for trial in &result.all_trials { - info!( - "│ {:5} │ {:16.6} │ {:16.6} │ {:16.6} │", - trial.trial_num, - trial.params.policy_learning_rate, - trial.params.value_learning_rate, - trial.objective - ); - } - - info!("└───────┴──────────────────┴──────────────────┴──────────────────┘"); - info!(""); - - // Compute convergence metrics - if result.all_trials.len() >= 2 { - let first_loss = result.all_trials[0].objective; - let best_loss = result.best_objective; - let improvement = ((first_loss - best_loss) / first_loss) * 100.0; - - info!("Convergence Analysis:"); - info!(" First Trial Loss: {:.6}", first_loss); - info!(" Best Trial Loss: {:.6}", best_loss); - info!(" Improvement: {:.2}%", improvement); - info!(""); - - // Compute variance in loss values - let mean_loss: f64 = result.all_trials.iter().map(|e| e.objective).sum::() - / result.all_trials.len() as f64; - let variance: f64 = result - .all_trials - .iter() - .map(|e| (e.objective - mean_loss).powi(2)) - .sum::() - / result.all_trials.len() as f64; - let std_dev = variance.sqrt(); - let coeff_var = (std_dev / mean_loss) * 100.0; - - info!("Loss Variance Analysis:"); - info!(" Mean Loss: {:.6}", mean_loss); - info!(" Std Dev: {:.6}", std_dev); - info!(" Coefficient of Variation: {:.2}%", coeff_var); - info!(""); - - if coeff_var < 5.0 { - info!( - "⚠️ WARNING: Low loss variance ({:.2}%) suggests mock metrics", - coeff_var - ); - } else { - info!("✓ Loss variance ({:.2}%) confirms real training", coeff_var); - } - } - - info!("✓ PPO hyperparameter optimization demo complete"); - - Ok(()) -} diff --git a/crates/ml/examples/hyperopt_tft_demo.rs b/crates/ml/examples/hyperopt_tft_demo.rs deleted file mode 100644 index 72672f320..000000000 --- a/crates/ml/examples/hyperopt_tft_demo.rs +++ /dev/null @@ -1,312 +0,0 @@ -//! TFT Hyperparameter Optimization Demo -//! -//! This example demonstrates how to use the argmin-based hyperparameter -//! optimization framework with Temporal Fusion Transformer (TFT). It runs -//! a small-scale optimization to show the complete workflow. -//! -//! ## Usage -//! -//! ```bash -//! # Local training with small dataset (default /tmp) -//! cargo run -p ml --example hyperopt_tft_demo --release --features cuda -- \ -//! --parquet-file test_data/ES_FUT_180d.parquet \ -//! --trials 10 \ -//! --epochs 20 -//! -//! # Runpod training with custom base dir -//! ./hyperopt_tft_demo \ -//! --parquet-file /runpod-volume/datasets/parquet/futures/ES_FUT_180d.parquet \ -//! --base-dir /runpod-volume \ -//! --trials 50 \ -//! --epochs 50 -//! -//! # Resume from specific run -//! ./hyperopt_tft_demo \ -//! --base-dir /runpod-volume \ -//! --run-id 20251028_223000_hyperopt \ -//! --parquet-file test_data/ES_FUT_180d.parquet \ -//! --trials 50 \ -//! --epochs 50 -//! ``` -//! -//! ## Output -//! -//! The example will: -//! 1. Initialize TFT trainer with specified Parquet file -//! 2. Run argmin optimization with Particle Swarm -//! 3. Display trial results including loss and parameter values -//! 4. Report best hyperparameters found -//! 5. Show expected improvement vs default parameters - -use anyhow::Result; -use clap::Parser; -use ml::hyperopt::adapters::tft::TFTTrainer; -use ml::hyperopt::paths::{generate_run_id, TrainingPaths}; -use ml::hyperopt::ArgminOptimizer; -use tracing::{info, Level}; -use tracing_subscriber; - -#[derive(Parser, Debug)] -#[command(name = "TFT Hyperparameter Optimization Demo")] -#[command(about = "Demonstrates argmin-based hyperparameter optimization for TFT")] -struct Args { - /// Path to Parquet file with OHLCV data - #[arg(long)] - parquet_file: String, - - /// Number of optimization trials (default: 10) - #[arg(long, default_value = "10")] - trials: usize, - - /// Number of training epochs per trial (default: 20) - #[arg(long, default_value = "20")] - epochs: usize, - - /// Number of initial random samples (default: 3) - #[arg(long, default_value = "3")] - n_initial: usize, - - /// Random seed for reproducibility (default: 42) - #[arg(long, default_value = "42")] - seed: u64, - - /// Minimum batch size (default: 16) - #[arg(long, default_value = "16")] - batch_size_min: usize, - - /// Maximum batch size for GPU memory constraints (default: 128 for RTX A4000 16GB) - /// Examples: RTX 3050 Ti 4GB = 64, RTX A4000 16GB = 128, RTX 4090 24GB = 256 - #[arg(long, default_value = "128")] - batch_size_max: usize, - - /// Base directory for training outputs (e.g., /runpod-volume) - #[arg(long, default_value = "/tmp/ml_training")] - base_dir: String, - - /// Run ID (auto-generated if not provided) - #[arg(long)] - run_id: Option, - - /// Run type (hyperopt, production, test) - #[arg(long, default_value = "hyperopt")] - run_type: String, - - /// Early stopping patience (epochs without improvement before stopping) - #[arg(long, default_value = "10")] - early_stopping_patience: usize, -} - -fn main() -> Result<()> { - // Initialize tracing - tracing_subscriber::fmt() - .with_max_level(Level::INFO) - .with_target(false) - .init(); - - // Parse arguments - let args = Args::parse(); - - // Generate run ID if not provided - let run_id = args - .run_id - .unwrap_or_else(|| generate_run_id(&args.run_type)); - - // Create training paths - let training_paths = TrainingPaths::new(&args.base_dir, "tft", &run_id); - - info!("========================================"); - info!("TFT Hyperparameter Optimization Demo"); - info!("========================================"); - info!("Configuration:"); - info!(" Parquet file: {}", args.parquet_file); - info!(" Trials: {}", args.trials); - info!(" Epochs per trial: {}", args.epochs); - info!(" Initial samples: {}", args.n_initial); - info!(" Random seed: {}", args.seed); - info!( - " Batch size bounds: [{}, {}]", - args.batch_size_min, args.batch_size_max - ); - info!(""); - info!("Training Paths:"); - info!(" Run ID: {}", run_id); - info!(" Base directory: {}", args.base_dir); - info!(" Run directory: {:?}", training_paths.run_dir()); - info!(" Checkpoints: {:?}", training_paths.checkpoints_dir()); - info!(" Logs: {:?}", training_paths.logs_dir()); - info!(" Hyperopt: {:?}", training_paths.hyperopt_dir()); - info!(""); - - // Create trainer with training paths - info!("Creating TFT trainer..."); - let trainer = TFTTrainer::new(&args.parquet_file, args.epochs)? - .with_early_stopping(args.early_stopping_patience) - .with_training_paths(training_paths); - - info!("TFT Configuration:"); - info!(" Input features: 54 (Wave C + Wave D)"); - info!(" Sequence length: 60"); - info!(" Prediction horizon: 10"); - info!(" Quantiles: 3 (0.1, 0.5, 0.9)"); - info!(""); - - // Create optimizer - info!("Initializing argmin optimizer..."); - let optimizer = ArgminOptimizer::builder() - .max_trials(args.trials) - .n_initial(args.n_initial) - .seed(args.seed) - .build(); - - // Run optimization - info!(""); - info!("Starting optimization (this may take a while)..."); - info!( - "Expected runtime: ~{} minutes", - estimate_runtime(args.trials, args.epochs) - ); - info!(""); - - let result = optimizer.optimize(trainer)?; - - // Display results - info!(""); - info!("========================================"); - info!("Optimization Complete!"); - info!("========================================"); - info!(""); - info!("Best Hyperparameters:"); - info!(" Learning rate: {:.6}", result.best_params.learning_rate); - info!(" Batch size: {}", result.best_params.batch_size); - info!(" Hidden size: {}", result.best_params.hidden_size); - info!(" Attention heads: {}", result.best_params.num_heads); - info!(" Dropout: {:.3}", result.best_params.dropout); - info!(""); - info!("Performance:"); - info!(" Best validation loss: {:.6}", result.best_objective); - info!(" Total trials: {}", result.all_trials.len()); - - // Find convergence trial (where best was found) - let convergence_trial = result - .all_trials - .iter() - .position(|t| (t.objective - result.best_objective).abs() < 1e-10) - .unwrap_or(0); - info!(" Convergence: {} trials to best", convergence_trial + 1); - info!(""); - - // Show top 5 trials - if result.all_trials.len() >= 5 { - info!("Top 5 Trials:"); - let mut sorted_trials = result.all_trials.clone(); - sorted_trials.sort_by(|a, b| a.objective.partial_cmp(&b.objective).unwrap()); - - for (i, trial) in sorted_trials.iter().take(5).enumerate() { - info!( - " {}. Loss: {:.6} (LR: {:.6}, BS: {}, Hidden: {}, Heads: {})", - i + 1, - trial.objective, - trial.params.learning_rate, - trial.params.batch_size, - trial.params.hidden_size, - trial.params.num_heads - ); - } - } - - info!(""); - info!("========================================"); - info!("Architecture Insights:"); - info!("========================================"); - - // Analyze best parameters - let best = &result.best_params; - - // Calculate model complexity - let complexity_score = (best.hidden_size as f64 * best.num_heads as f64) / 1000.0; - let complexity_level = if complexity_score < 2.0 { - "Light" - } else if complexity_score < 4.0 { - "Balanced" - } else { - "Heavy" - }; - - info!( - "Model Complexity: {} (score: {:.2})", - complexity_level, complexity_score - ); - info!(" Hidden dimension: {} features", best.hidden_size); - info!(" Attention heads: {} heads", best.num_heads); - info!( - " Head dimension: {} features/head", - best.hidden_size / best.num_heads - ); - info!(""); - - // Regularization analysis - let regularization_level = if best.dropout < 0.1 { - "Low" - } else if best.dropout < 0.2 { - "Medium" - } else { - "High" - }; - - info!("Regularization: {}", regularization_level); - info!(" Dropout rate: {:.1}%", best.dropout * 100.0); - info!(""); - - // Training characteristics - info!("Training Characteristics:"); - info!( - " Learning rate: {:.6} ({})", - best.learning_rate, - if best.learning_rate < 5e-5 { - "Conservative" - } else if best.learning_rate < 2e-4 { - "Balanced" - } else { - "Aggressive" - } - ); - info!( - " Batch size: {} (GPU memory: ~{}MB)", - best.batch_size, - estimate_gpu_memory(best.batch_size, best.hidden_size) - ); - info!(""); - - info!("========================================"); - info!("Next Steps:"); - info!("========================================"); - info!("1. Use best parameters for production training"); - info!("2. Run longer optimization (50+ trials) for better results"); - info!("3. Validate on holdout dataset"); - info!("4. Deploy optimized model to trading system"); - info!( - "5. Consider hidden_size={} as your production baseline", - best.hidden_size - ); - - Ok(()) -} - -/// Estimate runtime based on trials and epochs -fn estimate_runtime(trials: usize, epochs: usize) -> usize { - // Rough estimate: 2 min per 50 epochs on RTX 3050 Ti for TFT - let minutes_per_trial = (epochs as f64 / 50.0) * 2.0; - let total_minutes = (trials as f64 * minutes_per_trial).ceil() as usize; - total_minutes -} - -/// Estimate GPU memory usage for a given configuration -fn estimate_gpu_memory(batch_size: usize, hidden_size: usize) -> usize { - // Rough estimate: base (200MB) + sequence memory - // TFT has encoder-decoder architecture with attention - let base_memory = 200; - let sequence_memory = (batch_size * hidden_size * 60 * 8) / 1_000_000; // 60 seq length, 8 bytes/float - let attention_memory = (batch_size * 60 * 60 * 4) / 1_000_000; // attention matrix - - base_memory + sequence_memory + attention_memory -} diff --git a/infra/docker/Dockerfile.training b/infra/docker/Dockerfile.training index 3efe60904..87e4e79e9 100644 --- a/infra/docker/Dockerfile.training +++ b/infra/docker/Dockerfile.training @@ -59,17 +59,15 @@ COPY testing ./testing ENV SQLX_OFFLINE=true ENV CUDA_COMPUTE_CAP=90 -# Build training binaries with CUDA support (2 unified baselines + eval + hyperopt) +# Build training + hyperopt binaries with CUDA support RUN cargo build --release -p ml --features ml/cuda \ --example train_baseline_rl \ --example train_baseline_supervised \ --example evaluate_baseline \ - --example hyperopt_dqn_demo \ - --example hyperopt_ppo_demo \ - --example hyperopt_tft_demo \ - --example hyperopt_mamba2_demo \ + --example hyperopt_baseline_rl \ + --example hyperopt_baseline_supervised \ && mkdir -p /build/out \ - && for bin in train_baseline_rl train_baseline_supervised evaluate_baseline hyperopt_dqn_demo hyperopt_ppo_demo hyperopt_tft_demo hyperopt_mamba2_demo; do \ + && for bin in train_baseline_rl train_baseline_supervised evaluate_baseline hyperopt_baseline_rl hyperopt_baseline_supervised; do \ cp target/release/examples/${bin} /build/out/ && strip /build/out/${bin}; \ done diff --git a/infra/k8s/training/job-template.yaml b/infra/k8s/training/job-template.yaml index d3bb73150..2e4422f83 100644 --- a/infra/k8s/training/job-template.yaml +++ b/infra/k8s/training/job-template.yaml @@ -33,7 +33,8 @@ spec: # train_baseline_rl (for dqn, ppo) # train_baseline_supervised (for tft, mamba2, tggn, tlob, liquid, kan, xlstm, diffusion) # evaluate_baseline - # hyperopt_dqn_demo, hyperopt_ppo_demo, hyperopt_tft_demo, hyperopt_mamba2_demo + # hyperopt_baseline_rl (for dqn, ppo) + # hyperopt_baseline_supervised (for tft, mamba2) command: ["/usr/local/bin/$(TRAINING_BINARY)"] args: - "--symbol=ES.FUT" diff --git a/scripts/build_docker_images.sh b/scripts/build_docker_images.sh index e7de64846..5d5bfbaab 100755 --- a/scripts/build_docker_images.sh +++ b/scripts/build_docker_images.sh @@ -47,10 +47,8 @@ DOCKER_REGISTRY="jgrusewski" IMAGE_NAME="foxhunt-hyperopt" DEFAULT_DOCKERFILE="Dockerfile.foxhunt-build" EXPECTED_BINARIES=( - "hyperopt_mamba2_demo" - "hyperopt_dqn_demo" - "hyperopt_ppo_demo" - "hyperopt_tft_demo" + "hyperopt_baseline_rl" + "hyperopt_baseline_supervised" ) # =============================================================================