Files
foxhunt/crates/ml/examples/hyperopt_baseline_supervised.rs
jgrusewski 267240530d 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>
2026-02-25 23:20:44 +01:00

295 lines
9.2 KiB
Rust

//! 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(())
}