perf(ci): enable Kaniko layer caching + Docker Hub auth on all builds

- Add --cache=true --cache-repo to all 12 Kaniko builds
- Cache Docker layers in Scaleway CR (rg.fr-par.scw.cloud/foxhunt-ci/cache)
- Add Docker Hub auth to devcontainer + infra-runner prepare jobs
- First build populates cache; subsequent builds skip base image pulls

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-02-25 23:20:44 +01:00
parent 022036cb96
commit 267240530d
11 changed files with 326 additions and 1365 deletions

View File

@@ -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"

View File

@@ -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"

View File

@@ -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<Value> {
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<Value> {
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(())
}

View File

@@ -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<String>,
/// 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<f64> = result.all_trials.iter().map(|e| -e.objective).collect();
let mean_sharpe: f64 = sharpe_values.iter().sum::<f64>() / sharpe_values.len() as f64;
let variance: f64 = sharpe_values
.iter()
.map(|s| (s - mean_sharpe).powi(2))
.sum::<f64>()
/ 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(())
}

View File

@@ -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<String>,
/// 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<PathBuf>,
}
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<f64> = result.all_trials.iter().map(|t| t.objective).collect();
let rewards: Vec<f64> = objectives.iter().map(|o| -o).collect();
let mean_reward = rewards.iter().sum::<f64>() / rewards.len() as f64;
let variance = rewards
.iter()
.map(|r| (r - mean_reward).powi(2))
.sum::<f64>()
/ 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(())
}

View File

@@ -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<String>,
/// 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
}

View File

@@ -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<String>,
/// 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::<f64>()
/ result.all_trials.len() as f64;
let variance: f64 = result
.all_trials
.iter()
.map(|e| (e.objective - mean_loss).powi(2))
.sum::<f64>()
/ 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(())
}

View File

@@ -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<String>,
/// 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
}

View File

@@ -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

View File

@@ -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"

View File

@@ -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"
)
# =============================================================================