#![allow( clippy::assertions_on_constants, clippy::assertions_on_result_states, clippy::clone_on_copy, clippy::decimal_literal_representation, clippy::doc_markdown, clippy::empty_line_after_doc_comments, clippy::field_reassign_with_default, clippy::get_unwrap, clippy::identity_op, clippy::inconsistent_digit_grouping, clippy::indexing_slicing, clippy::integer_division, clippy::len_zero, clippy::let_underscore_must_use, clippy::manual_div_ceil, clippy::manual_let_else, clippy::manual_range_contains, clippy::modulo_arithmetic, clippy::needless_range_loop, clippy::non_ascii_literal, clippy::redundant_clone, clippy::shadow_reuse, clippy::shadow_same, clippy::shadow_unrelated, clippy::single_match_else, clippy::str_to_string, clippy::string_slice, clippy::tests_outside_test_module, clippy::too_many_lines, clippy::unnecessary_wraps, clippy::unseparated_literal_suffix, clippy::use_debug, clippy::useless_vec, clippy::wildcard_enum_match_arm, clippy::else_if_without_else, clippy::expect_used, clippy::missing_const_for_fn, clippy::similar_names, clippy::type_complexity, clippy::collapsible_else_if, clippy::doc_lazy_continuation, clippy::items_after_test_module, clippy::map_clone, clippy::multiple_unsafe_ops_per_block, clippy::unwrap_or_default, clippy::assign_op_pattern, clippy::needless_borrow, clippy::println_empty_string, clippy::unnecessary_cast, clippy::used_underscore_binding, clippy::create_dir, clippy::implicit_saturating_sub, clippy::exit, clippy::expect_fun_call, clippy::too_many_arguments, clippy::unnecessary_map_or, clippy::unwrap_used, dead_code, unused_imports, unused_variables, clippy::cloned_ref_to_slice_refs, clippy::neg_multiply, clippy::while_let_loop, clippy::bool_assert_comparison, clippy::excessive_precision, clippy::trivially_copy_pass_by_ref, clippy::op_ref, clippy::redundant_closure, clippy::unnecessary_lazy_evaluations, clippy::if_then_some_else_none, clippy::unnecessary_to_owned, clippy::single_component_path_imports, )] //! Hyperopt Runner for Supervised Models on DBN Data //! //! Runs hyperparameter optimization using Particle Swarm Optimization (PSO) for //! any of the 8 supervised models: TFT, Mamba2, Liquid, TGGN, TLOB, KAN, xLSTM, //! and Diffusion. Uses the same --data-dir / --symbol interface as the RL hyperopt //! and training binaries. //! //! ## Usage //! //! ```bash //! # Run TFT hyperopt on ES futures //! hyperopt_baseline_supervised --model tft --data-dir /data/ES.FUT --trials 20 --epochs 20 //! //! # Run Mamba2 hyperopt //! hyperopt_baseline_supervised --model mamba2 --data-dir /data/NQ.FUT --trials 20 --epochs 10 //! //! # Run a specific model //! hyperopt_baseline_supervised --model liquid --data-dir /data/ES.FUT //! //! # Run TFT + Mamba2 (backward-compatible alias) //! hyperopt_baseline_supervised --model both --data-dir /data/ES.FUT //! //! # Run all 8 supervised models //! hyperopt_baseline_supervised --model all --data-dir /data/ES.FUT //! ``` //! //! ## 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}; use common::metrics::{server as metrics_server, training_metrics}; use ml::hyperopt::adapters::diffusion::DiffusionTrainer; use ml::hyperopt::adapters::kan::KANTrainer; use ml::hyperopt::adapters::liquid::LiquidTrainer; use ml::hyperopt::adapters::mamba2::Mamba2Trainer; use ml::hyperopt::adapters::tft::TFTTrainer; use ml::hyperopt::adapters::tggn::TGGNTrainer; use ml::hyperopt::adapters::tlob::TLOBTrainer; use ml::hyperopt::adapters::xlstm::XLSTMTrainer; 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, liquid, tggn, tlob, kan, xlstm, diffusion, both, all)")] struct Args { /// Model to optimize: "tft", "mamba2", "liquid", "tggn", "tlob", "kan", /// "xlstm", "diffusion", "both" (tft+mamba2), or "all" (all 8 models) #[arg(long, default_value = "both")] model: String, /// Directory containing .dbn.zst OHLCV data files (env: FOXHUNT_DATA_DIR) #[arg(long, env = "FOXHUNT_DATA_DIR")] data_dir: PathBuf, /// 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, } 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, }) } #[allow(clippy::cognitive_complexity)] 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!("Data dir: {}", args.data_dir.display()); info!("Epochs per trial: {}", args.epochs); info!("Trials: {}", args.trials); let mut trainer = TFTTrainer::new(&args.data_dir, args.epochs) .context("Failed to create TFT trainer")? .with_early_stopping(args.early_stopping_patience) .with_training_paths(training_paths); if let Err(e) = trainer.preload_data() { warn!("TFT data preload failed ({}), trials will load from disk", e); } else { info!("Training data preloaded for all {} trials", args.trials); } let optimizer = ArgminOptimizer::builder() .max_trials(args.trials) .n_initial(args.n_initial) .seed(args.seed) .model_name("tft") .build(); training_metrics::set_hyperopt_trial("tft", 0.0, args.trials as f64); let start = Instant::now(); let result = optimizer .optimize(trainer) .context("TFT hyperopt optimization failed")?; let elapsed = start.elapsed().as_secs_f64(); training_metrics::set_hyperopt_trial("tft", result.all_trials.len() as f64, args.trials as f64); training_metrics::set_hyperopt_best_objective("tft", result.best_objective); training_metrics::set_hyperopt_elapsed("tft", elapsed); 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, )) } #[allow(clippy::cognitive_complexity)] 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!("Data dir: {}", args.data_dir.display()); info!("Epochs per trial: {}", args.epochs); info!("Trials: {}", args.trials); let mut trainer = Mamba2Trainer::new(&args.data_dir, args.epochs) .context("Failed to create Mamba2 trainer")? .with_training_paths(training_paths); if let Err(e) = trainer.preload_data() { warn!("Mamba2 data preload failed ({}), trials will load from disk", e); } else { info!("Training data preloaded for all {} trials", args.trials); } let optimizer = ArgminOptimizer::builder() .max_trials(args.trials) .n_initial(args.n_initial) .seed(args.seed) .model_name("mamba2") .build(); training_metrics::set_hyperopt_trial("mamba2", 0.0, args.trials as f64); let start = Instant::now(); let result = optimizer .optimize(trainer) .context("Mamba2 hyperopt optimization failed")?; let elapsed = start.elapsed().as_secs_f64(); training_metrics::set_hyperopt_trial("mamba2", result.all_trials.len() as f64, args.trials as f64); training_metrics::set_hyperopt_best_objective("mamba2", result.best_objective); training_metrics::set_hyperopt_elapsed("mamba2", elapsed); 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, )) } #[allow(clippy::cognitive_complexity)] fn run_liquid_hyperopt(args: &Args) -> Result { info!("========================================"); info!(" Liquid Hyperparameter Optimization"); info!("========================================"); let run_id = generate_run_id("hyperopt-liquid"); let training_paths = TrainingPaths::new(&args.base_dir, "liquid", &run_id); info!("Run ID: {}", run_id); info!("Data dir: {}", args.data_dir.display()); info!("Epochs per trial: {}", args.epochs); info!("Trials: {}", args.trials); let mut trainer = LiquidTrainer::new(&args.data_dir, args.epochs) .context("Failed to create Liquid trainer")? .with_early_stopping(args.early_stopping_patience) .with_training_paths(training_paths); if let Err(e) = trainer.preload_data() { warn!("Liquid data preload failed ({}), trials will load from disk", e); } else { info!("Training data preloaded for all {} trials", args.trials); } let optimizer = ArgminOptimizer::builder() .max_trials(args.trials) .n_initial(args.n_initial) .seed(args.seed) .model_name("liquid") .build(); training_metrics::set_hyperopt_trial("liquid", 0.0, args.trials as f64); let start = Instant::now(); let result = optimizer .optimize(trainer) .context("Liquid hyperopt optimization failed")?; let elapsed = start.elapsed().as_secs_f64(); training_metrics::set_hyperopt_trial("liquid", result.all_trials.len() as f64, args.trials as f64); training_metrics::set_hyperopt_best_objective("liquid", result.best_objective); training_metrics::set_hyperopt_elapsed("liquid", elapsed); info!("Liquid 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, )) } #[allow(clippy::cognitive_complexity)] fn run_tggn_hyperopt(args: &Args) -> Result { info!("========================================"); info!(" TGGN Hyperparameter Optimization"); info!("========================================"); let run_id = generate_run_id("hyperopt-tggn"); let training_paths = TrainingPaths::new(&args.base_dir, "tggn", &run_id); info!("Run ID: {}", run_id); info!("Data dir: {}", args.data_dir.display()); info!("Epochs per trial: {}", args.epochs); info!("Trials: {}", args.trials); let mut trainer = TGGNTrainer::new(&args.data_dir, args.epochs) .context("Failed to create TGGN trainer")? .with_early_stopping(args.early_stopping_patience) .with_training_paths(training_paths); if let Err(e) = trainer.preload_data() { warn!("TGGN data preload failed ({}), trials will load from disk", e); } else { info!("Training data preloaded for all {} trials", args.trials); } let optimizer = ArgminOptimizer::builder() .max_trials(args.trials) .n_initial(args.n_initial) .seed(args.seed) .model_name("tggn") .build(); training_metrics::set_hyperopt_trial("tggn", 0.0, args.trials as f64); let start = Instant::now(); let result = optimizer .optimize(trainer) .context("TGGN hyperopt optimization failed")?; let elapsed = start.elapsed().as_secs_f64(); training_metrics::set_hyperopt_trial("tggn", result.all_trials.len() as f64, args.trials as f64); training_metrics::set_hyperopt_best_objective("tggn", result.best_objective); training_metrics::set_hyperopt_elapsed("tggn", elapsed); info!("TGGN 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, )) } #[allow(clippy::cognitive_complexity)] fn run_tlob_hyperopt(args: &Args) -> Result { info!("========================================"); info!(" TLOB Hyperparameter Optimization"); info!("========================================"); let run_id = generate_run_id("hyperopt-tlob"); let training_paths = TrainingPaths::new(&args.base_dir, "tlob", &run_id); info!("Run ID: {}", run_id); info!("Data dir: {}", args.data_dir.display()); info!("Epochs per trial: {}", args.epochs); info!("Trials: {}", args.trials); let mut trainer = TLOBTrainer::new(&args.data_dir, args.epochs) .context("Failed to create TLOB trainer")? .with_early_stopping(args.early_stopping_patience) .with_training_paths(training_paths); if let Err(e) = trainer.preload_data() { warn!("TLOB data preload failed ({}), trials will load from disk", e); } else { info!("Training data preloaded for all {} trials", args.trials); } let optimizer = ArgminOptimizer::builder() .max_trials(args.trials) .n_initial(args.n_initial) .seed(args.seed) .model_name("tlob") .build(); training_metrics::set_hyperopt_trial("tlob", 0.0, args.trials as f64); let start = Instant::now(); let result = optimizer .optimize(trainer) .context("TLOB hyperopt optimization failed")?; let elapsed = start.elapsed().as_secs_f64(); training_metrics::set_hyperopt_trial("tlob", result.all_trials.len() as f64, args.trials as f64); training_metrics::set_hyperopt_best_objective("tlob", result.best_objective); training_metrics::set_hyperopt_elapsed("tlob", elapsed); info!("TLOB 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, )) } #[allow(clippy::cognitive_complexity)] fn run_kan_hyperopt(args: &Args) -> Result { info!("========================================"); info!(" KAN Hyperparameter Optimization"); info!("========================================"); let run_id = generate_run_id("hyperopt-kan"); let training_paths = TrainingPaths::new(&args.base_dir, "kan", &run_id); info!("Run ID: {}", run_id); info!("Data dir: {}", args.data_dir.display()); info!("Epochs per trial: {}", args.epochs); info!("Trials: {}", args.trials); let mut trainer = KANTrainer::new(&args.data_dir, args.epochs) .context("Failed to create KAN trainer")? .with_early_stopping(args.early_stopping_patience) .with_training_paths(training_paths); if let Err(e) = trainer.preload_data() { warn!("KAN data preload failed ({}), trials will load from disk", e); } else { info!("Training data preloaded for all {} trials", args.trials); } let optimizer = ArgminOptimizer::builder() .max_trials(args.trials) .n_initial(args.n_initial) .seed(args.seed) .model_name("kan") .build(); training_metrics::set_hyperopt_trial("kan", 0.0, args.trials as f64); let start = Instant::now(); let result = optimizer .optimize(trainer) .context("KAN hyperopt optimization failed")?; let elapsed = start.elapsed().as_secs_f64(); training_metrics::set_hyperopt_trial("kan", result.all_trials.len() as f64, args.trials as f64); training_metrics::set_hyperopt_best_objective("kan", result.best_objective); training_metrics::set_hyperopt_elapsed("kan", elapsed); info!("KAN 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, )) } #[allow(clippy::cognitive_complexity)] fn run_xlstm_hyperopt(args: &Args) -> Result { info!("========================================"); info!(" xLSTM Hyperparameter Optimization"); info!("========================================"); let run_id = generate_run_id("hyperopt-xlstm"); let training_paths = TrainingPaths::new(&args.base_dir, "xlstm", &run_id); info!("Run ID: {}", run_id); info!("Data dir: {}", args.data_dir.display()); info!("Epochs per trial: {}", args.epochs); info!("Trials: {}", args.trials); let mut trainer = XLSTMTrainer::new(&args.data_dir, args.epochs) .context("Failed to create xLSTM trainer")? .with_early_stopping(args.early_stopping_patience) .with_training_paths(training_paths); if let Err(e) = trainer.preload_data() { warn!("xLSTM data preload failed ({}), trials will load from disk", e); } else { info!("Training data preloaded for all {} trials", args.trials); } let optimizer = ArgminOptimizer::builder() .max_trials(args.trials) .n_initial(args.n_initial) .seed(args.seed) .model_name("xlstm") .build(); training_metrics::set_hyperopt_trial("xlstm", 0.0, args.trials as f64); let start = Instant::now(); let result = optimizer .optimize(trainer) .context("xLSTM hyperopt optimization failed")?; let elapsed = start.elapsed().as_secs_f64(); training_metrics::set_hyperopt_trial("xlstm", result.all_trials.len() as f64, args.trials as f64); training_metrics::set_hyperopt_best_objective("xlstm", result.best_objective); training_metrics::set_hyperopt_elapsed("xlstm", elapsed); info!("xLSTM 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, )) } #[allow(clippy::cognitive_complexity)] fn run_diffusion_hyperopt(args: &Args) -> Result { info!("========================================"); info!(" Diffusion Hyperparameter Optimization"); info!("========================================"); let run_id = generate_run_id("hyperopt-diffusion"); let training_paths = TrainingPaths::new(&args.base_dir, "diffusion", &run_id); info!("Run ID: {}", run_id); info!("Data dir: {}", args.data_dir.display()); info!("Epochs per trial: {}", args.epochs); info!("Trials: {}", args.trials); let mut trainer = DiffusionTrainer::new(&args.data_dir, args.epochs) .context("Failed to create Diffusion trainer")? .with_early_stopping(args.early_stopping_patience) .with_training_paths(training_paths); if let Err(e) = trainer.preload_data() { warn!("Diffusion data preload failed ({}), trials will load from disk", e); } else { info!("Training data preloaded for all {} trials", args.trials); } let optimizer = ArgminOptimizer::builder() .max_trials(args.trials) .n_initial(args.n_initial) .seed(args.seed) .model_name("diffusion") .build(); training_metrics::set_hyperopt_trial("diffusion", 0.0, args.trials as f64); let start = Instant::now(); let result = optimizer .optimize(trainer) .context("Diffusion hyperopt optimization failed")?; let elapsed = start.elapsed().as_secs_f64(); training_metrics::set_hyperopt_trial("diffusion", result.all_trials.len() as f64, args.trials as f64); training_metrics::set_hyperopt_best_objective("diffusion", result.best_objective); training_metrics::set_hyperopt_elapsed("diffusion", elapsed); info!("Diffusion 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, )) } const VALID_MODELS: &[&str] = &[ "tft", "mamba2", "liquid", "tggn", "tlob", "kan", "xlstm", "diffusion", ]; #[allow(clippy::cognitive_complexity)] fn main() -> Result<()> { // Initialize tracing with optional OTLP export to Tempo let otlp_endpoint = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").ok(); if let Err(e) = common::observability::init_observability( "hyperopt_baseline_supervised", otlp_endpoint.as_deref(), ) { eprintln!("Observability init failed (non-fatal): {e}"); } training_metrics::init(); metrics_server::start_metrics_server(9094); training_metrics::set_active_workers(1.0); let mut args = Args::parse(); // Signal hyperopt mode active — will be cleared at exit let hyperopt_model_label = args.model.clone(); training_metrics::set_hyperopt_mode(&hyperopt_model_label, true); info!("========================================"); info!(" Hyperopt Baseline Supervised Runner"); info!("========================================"); info!("Model: {}", args.model); info!("Data dir: {}", args.data_dir.display()); 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); if !args.data_dir.exists() { anyhow::bail!( "Data directory not found: {}. Provide a valid path via --data-dir.", args.data_dir.display() ); } // Resolve model selection: "both" = tft+mamba2 (backward compat), "all" = all 8 models let models: Vec<&str> = if args.model == "both" { vec!["tft", "mamba2"] } else if args.model == "all" { VALID_MODELS.to_vec() } else if VALID_MODELS.contains(&args.model.as_str()) { vec![args.model.as_str()] } else { anyhow::bail!( "Invalid --model value '{}'. Must be one of: {}, 'both', or 'all'.", args.model, VALID_MODELS.join(", ") ); }; info!("Models to optimize: {:?}", models); // Enforce minimum 5 trials AND trials > n_initial // (ArgminOptimizer needs at least n_initial LHS rounds + 1 TPE-guided trial) const MIN_TRIALS: usize = 5; let effective_min = MIN_TRIALS.max(args.n_initial + 1); if args.trials < effective_min { info!( "Trials {} below minimum {} (floor={}, n_initial {}+1={}) — clamping", args.trials, effective_min, MIN_TRIALS, args.n_initial, args.n_initial + 1 ); args.trials = effective_min; } 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(); for model_name in &models { let result = match *model_name { "tft" => run_tft_hyperopt(&args), "mamba2" => run_mamba2_hyperopt(&args), "liquid" => run_liquid_hyperopt(&args), "tggn" => run_tggn_hyperopt(&args), "tlob" => run_tlob_hyperopt(&args), "kan" => run_kan_hyperopt(&args), "xlstm" => run_xlstm_hyperopt(&args), "diffusion" => run_diffusion_hyperopt(&args), other => { error!("Unknown model: {}", other); continue; } }; match result { Ok(model_result) => { results.insert(model_name.to_string(), model_result); } Err(e) => { error!("{} hyperopt failed: {:#}", model_name, e); warn!("Continuing with remaining models..."); results.insert( model_name.to_string(), serde_json::json!({ "error": format!("{:#}", e) }), ); } } } 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); training_metrics::set_hyperopt_mode(&hyperopt_model_label, false); training_metrics::set_active_workers(0.0); // Push final metrics to pushgateway so they persist after pod termination if let Err(e) = metrics_server::push_to_gateway(None, "hyperopt_baseline_supervised") { warn!("Failed to push metrics to gateway (non-fatal): {e}"); } Ok(()) }