#![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, )] //! Integration tests for DQN hyperopt JSON export functionality //! //! Tests the automatic export of best trial hyperparameters to JSON //! during hyperopt campaigns for production deployment. //! //! NOTE: These tests require GPU and training data, so they are marked //! with #[ignore]. Run with: cargo test --test dqn_hyperopt_json_export_test -- --ignored use anyhow::Result; use serde_json; use std::fs; use std::path::{Path, PathBuf}; use ml::hyperopt::adapters::dqn::{BestTrialExport, DQNTrainer}; use ml::hyperopt::ArgminOptimizer; use tracing::info; #[cfg(test)] mod hyperopt_json_export_tests { use super::*; /// Helper: Clean up any test JSON files fn cleanup_test_jsons(pattern: &str) { let results_dir = Path::new("ml/hyperopt_results"); if !results_dir.exists() { return; } if let Ok(entries) = fs::read_dir(results_dir) { for entry in entries.flatten() { let path = entry.path(); if let Some(name) = path.file_name().and_then(|n| n.to_str()) { if name.contains(pattern) && name.ends_with(".json") { let _ = fs::remove_file(&path); info!(path = ?path, "Cleaned up test file"); } } } } } /// Helper: Find JSON file matching pattern in hyperopt_results/ fn find_json_file(pattern: &str) -> Option { let results_dir = Path::new("ml/hyperopt_results"); if !results_dir.exists() { return None; } if let Ok(entries) = fs::read_dir(results_dir) { for entry in entries.flatten() { let path = entry.path(); if let Some(name) = path.file_name().and_then(|n| n.to_str()) { if name.contains(pattern) && name.ends_with(".json") { return Some(path); } } } } None } /// Test 1: Mini hyperopt saves best trial JSON /// /// Runs 3-trial hyperopt with 10 epochs each, verifies JSON export #[test] #[ignore] // Requires GPU and training data fn test_hyperopt_saves_best_trial_json() -> Result<()> { info!("TEST 1: Hyperopt saves best trial JSON"); // Cleanup any previous test files cleanup_test_jsons("test_mini_"); // Create DQN trainer with minimal epochs for speed let trainer = DQNTrainer::new( "test_data/ES_FUT_180d.parquet", 10, // epochs per trial )?; // Run mini hyperopt (3 trials) info!("Running 3-trial hyperopt campaign"); let optimizer = ArgminOptimizer::with_trials(3, 1); let result = optimizer.optimize(trainer)?; info!(best_objective = result.best_objective, "Hyperopt complete"); // Verify JSON file was created let json_file = find_json_file("best_trial_sharpe_") .expect("Best trial JSON should be created"); info!(path = ?json_file, "Found JSON file"); // Load and verify the JSON let json_content = fs::read_to_string(&json_file)?; let trial: BestTrialExport = serde_json::from_str(&json_content)?; // Verify metadata exists assert!(trial.trial_number > 0, "Trial number should be > 0"); assert!(trial.trial_number <= 3, "Trial number should be <= 3 (only 3 trials)"); assert!(!trial.timestamp.is_empty(), "Timestamp should not be empty"); assert!(trial.gradient_clip_norm > 0.0, "Gradient clip norm should be > 0"); // Verify hyperparameters are present (all 21 fields) let params = trial.hyperparameters; assert!(params.learning_rate > 0.0, "Learning rate should be > 0"); assert!(params.batch_size > 0, "Batch size should be > 0"); assert!(params.gamma > 0.0 && params.gamma < 1.0, "Gamma should be in (0, 1)"); assert!(params.buffer_size > 0, "Buffer size should be > 0"); info!(trial_number = trial.trial_number, sharpe = trial.sharpe, "JSON file created with best trial"); // Cleanup cleanup_test_jsons("best_trial_sharpe_"); Ok(()) } /// Test 2: Hyperopt updates JSON on new best trial /// /// Verifies that when a better trial is found, the JSON is updated #[test] #[ignore] // Requires GPU and training data fn test_hyperopt_updates_json_on_new_best() -> Result<()> { info!("TEST 2: Hyperopt updates JSON on new best"); cleanup_test_jsons("best_trial_sharpe_"); let trainer = DQNTrainer::new( "test_data/ES_FUT_180d.parquet", 10, // epochs per trial )?; // Run 5 trials to increase chance of finding better trial info!("Running 5-trial hyperopt campaign"); let optimizer = ArgminOptimizer::with_trials(5, 1); let _ = optimizer.optimize(trainer)?; // Load the final JSON let json_file = find_json_file("best_trial_sharpe_") .expect("Best trial JSON should exist"); let json_content = fs::read_to_string(&json_file)?; let final_trial: BestTrialExport = serde_json::from_str(&json_content)?; // Verify the JSON contains the best trial from the campaign // (Note: We can't predict which trial number will be best, but we can // verify the data structure is valid and updated) assert!(final_trial.trial_number > 0 && final_trial.trial_number <= 5, "Best trial number should be in range [1, 5]"); info!(trial_number = final_trial.trial_number, sharpe = final_trial.sharpe, "Final best trial"); // Verify the file was written at least once let metadata = fs::metadata(&json_file)?; assert!(metadata.len() > 100, "JSON file should have substantial content"); cleanup_test_jsons("best_trial_sharpe_"); Ok(()) } /// Test 3: JSON roundtrip - hyperopt output can be reloaded /// /// Saves a trial, loads it back, verifies all parameters match exactly #[test] #[ignore] // Requires GPU and training data fn test_hyperopt_json_roundtrip() -> Result<()> { info!("TEST 3: JSON roundtrip consistency"); cleanup_test_jsons("best_trial_sharpe_"); let trainer = DQNTrainer::new( "test_data/ES_FUT_180d.parquet", 10, )?; info!("Running 2-trial hyperopt campaign"); let optimizer = ArgminOptimizer::with_trials(2, 1); let _ = optimizer.optimize(trainer)?; // Load the JSON that was saved let json_file = find_json_file("best_trial_sharpe_") .expect("Best trial JSON should exist"); let json_content = fs::read_to_string(&json_file)?; let saved_trial: BestTrialExport = serde_json::from_str(&json_content)?; // Serialize it again let roundtrip_json = serde_json::to_string_pretty(&saved_trial)?; // Deserialize the roundtrip version let roundtrip_trial: BestTrialExport = serde_json::from_str(&roundtrip_json)?; // Verify exact match on all fields assert_eq!(saved_trial.trial_number, roundtrip_trial.trial_number, "Trial number should match exactly"); assert_eq!(saved_trial.sharpe, roundtrip_trial.sharpe, "Sharpe should match exactly"); assert_eq!(saved_trial.win_rate, roundtrip_trial.win_rate, "Win rate should match exactly"); assert_eq!(saved_trial.max_drawdown, roundtrip_trial.max_drawdown, "Max drawdown should match exactly"); assert_eq!(saved_trial.total_return, roundtrip_trial.total_return, "Total return should match exactly"); assert_eq!(saved_trial.gradient_clip_norm, roundtrip_trial.gradient_clip_norm, "Gradient clip norm should match exactly"); // Verify all 21 hyperparameters match let saved_params = saved_trial.hyperparameters; let roundtrip_params = roundtrip_trial.hyperparameters; assert_eq!(saved_params.learning_rate, roundtrip_params.learning_rate); assert_eq!(saved_params.batch_size, roundtrip_params.batch_size); assert_eq!(saved_params.gamma, roundtrip_params.gamma); assert_eq!(saved_params.buffer_size, roundtrip_params.buffer_size); assert_eq!(saved_params.max_position_absolute, roundtrip_params.max_position_absolute); assert_eq!(saved_params.huber_delta, roundtrip_params.huber_delta); assert_eq!(saved_params.entropy_coefficient, roundtrip_params.entropy_coefficient); assert_eq!(saved_params.transaction_cost_multiplier, roundtrip_params.transaction_cost_multiplier); assert_eq!(saved_params.per_alpha, roundtrip_params.per_alpha); assert_eq!(saved_params.per_beta_start, roundtrip_params.per_beta_start); assert_eq!(saved_params.dueling_hidden_dim, roundtrip_params.dueling_hidden_dim); assert_eq!(saved_params.n_steps, roundtrip_params.n_steps); assert_eq!(saved_params.tau, roundtrip_params.tau); assert_eq!(saved_params.num_atoms, roundtrip_params.num_atoms); assert_eq!(saved_params.v_min, roundtrip_params.v_min); assert_eq!(saved_params.v_max, roundtrip_params.v_max); assert_eq!(saved_params.noisy_sigma_init, roundtrip_params.noisy_sigma_init); assert_eq!(saved_params.minimum_profit_factor, roundtrip_params.minimum_profit_factor); info!("All 21 hyperparameters + metadata survive roundtrip with perfect fidelity"); cleanup_test_jsons("best_trial_sharpe_"); Ok(()) } /// Test 4: JSON contains all required metadata fields /// /// Validates that exported JSON has comprehensive metadata for production use #[test] #[ignore] // Requires GPU and training data fn test_hyperopt_json_contains_all_metadata() -> Result<()> { info!("TEST 4: JSON metadata completeness"); cleanup_test_jsons("best_trial_sharpe_"); let trainer = DQNTrainer::new( "test_data/ES_FUT_180d.parquet", 10, )?; info!("Running 2-trial hyperopt campaign"); let optimizer = ArgminOptimizer::with_trials(2, 1); let _ = optimizer.optimize(trainer)?; let json_file = find_json_file("best_trial_sharpe_") .expect("Best trial JSON should exist"); // Load as raw JSON to verify field presence let json_content = fs::read_to_string(&json_file)?; let raw_json: serde_json::Value = serde_json::from_str(&json_content)?; // Verify top-level fields assert!(raw_json.get("trial_number").is_some(), "Should have trial_number"); assert!(raw_json.get("sharpe").is_some(), "Should have sharpe"); assert!(raw_json.get("win_rate").is_some(), "Should have win_rate"); assert!(raw_json.get("max_drawdown").is_some(), "Should have max_drawdown"); assert!(raw_json.get("total_return").is_some(), "Should have total_return"); assert!(raw_json.get("timestamp").is_some(), "Should have timestamp"); assert!(raw_json.get("gradient_clip_norm").is_some(), "Should have gradient_clip_norm"); assert!(raw_json.get("hyperparameters").is_some(), "Should have hyperparameters"); // Verify timestamp is ISO 8601 format let timestamp = raw_json["timestamp"].as_str() .expect("Timestamp should be a string"); assert!(timestamp.contains('T') && timestamp.contains('Z'), "Timestamp should be ISO 8601 format: {}", timestamp); // Verify hyperparameters object has all 21 fields let hyperparams = raw_json["hyperparameters"].as_object() .expect("Hyperparameters should be an object"); let required_fields = vec![ "learning_rate", "batch_size", "gamma", "buffer_size", "max_position_absolute", "huber_delta", "entropy_coefficient", "transaction_cost_multiplier", "use_per", "per_alpha", "per_beta_start", "use_dueling", "dueling_hidden_dim", "n_steps", "tau", "use_distributional", "num_atoms", "v_min", "v_max", "use_noisy_nets", "noisy_sigma_init", "minimum_profit_factor", "w_dsr", "w_pnl", "w_dd", "w_idle", "dd_threshold", "loss_aversion", "time_decay_rate", ]; for field in required_fields { assert!(hyperparams.contains_key(field), "Hyperparameters should contain field: {}", field); } info!("JSON contains all 8 metadata fields + 21 hyperparameter fields"); cleanup_test_jsons("best_trial_sharpe_"); Ok(()) } /// Test 5: Verify filename contains Sharpe ratio #[test] #[ignore] // Requires GPU and training data fn test_json_filename_contains_sharpe() -> Result<()> { info!("TEST 5: JSON filename includes Sharpe"); cleanup_test_jsons("best_trial_sharpe_"); let trainer = DQNTrainer::new( "test_data/ES_FUT_180d.parquet", 10, )?; info!("Running 2-trial hyperopt campaign"); let optimizer = ArgminOptimizer::with_trials(2, 1); let _ = optimizer.optimize(trainer)?; let json_file = find_json_file("best_trial_sharpe_") .expect("Best trial JSON should exist"); // Verify filename format let filename = json_file.file_name() .and_then(|n| n.to_str()) .expect("Should have valid filename"); assert!(filename.starts_with("best_trial_sharpe_"), "Filename should start with 'best_trial_sharpe_': {}", filename); assert!(filename.ends_with(".json"), "Filename should end with '.json': {}", filename); // Load JSON to verify Sharpe in filename matches content let json_content = fs::read_to_string(&json_file)?; let trial: BestTrialExport = serde_json::from_str(&json_content)?; // Extract Sharpe from filename (format: best_trial_sharpe_X.XXXX.json) let sharpe_str = filename .strip_prefix("best_trial_sharpe_") .and_then(|s| s.strip_suffix(".json")) .expect("Should be able to extract Sharpe from filename"); let filename_sharpe: f64 = sharpe_str.parse() .expect(&format!("Should be able to parse Sharpe from filename: {}", sharpe_str)); // Verify filename Sharpe matches JSON content (within floating point tolerance) let sharpe_diff = (filename_sharpe - trial.sharpe).abs(); assert!(sharpe_diff < 0.0001, "Filename Sharpe ({}) should match JSON Sharpe ({}) within 0.0001", filename_sharpe, trial.sharpe); info!(filename_sharpe, trial_sharpe = trial.sharpe, "Filename Sharpe matches JSON Sharpe"); cleanup_test_jsons("best_trial_sharpe_"); Ok(()) } }