From 3d7d9c3d032d05e290aec87f65de59b0ee41cddb Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Tue, 31 Mar 2026 01:30:33 +0200 Subject: [PATCH] fix: update 9 test files for 14D DQNParams restructure Tests referenced old DQNParams fields (learning_rate, batch_size, ensemble_size, etc.) that were absorbed into family intensity scalars. Rewrote all affected tests to validate the 14D search space layout, intensity bounds [0.0, 2.0], and round-trip serialization. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/ml/src/hyperopt/adapters/dqn.rs | 59 ------- crates/ml/src/trainers/dqn/trainer/tests.rs | 43 +++-- .../ml/tests/dqn_action_collapse_fix_test.rs | 38 ++--- .../ml/tests/dqn_hyperopt_json_export_test.rs | 60 +++---- crates/ml/tests/dqn_hyperopt_test.rs | 25 ++- .../tests/dqn_transaction_cost_bug2_test.rs | 53 +++--- crates/ml/tests/ensemble_hyperopt_tests.rs | 161 ++++++++---------- crates/ml/tests/hyperopt_kelly_params_test.rs | 118 ++++++------- crates/ml/tests/ofi_features_test.rs | 108 ++++++++++++ .../tests/production_training_smoke_test.rs | 71 ++++---- 10 files changed, 373 insertions(+), 363 deletions(-) diff --git a/crates/ml/src/hyperopt/adapters/dqn.rs b/crates/ml/src/hyperopt/adapters/dqn.rs index 64a6cd1d2..9bd127c39 100644 --- a/crates/ml/src/hyperopt/adapters/dqn.rs +++ b/crates/ml/src/hyperopt/adapters/dqn.rs @@ -2991,65 +2991,6 @@ mod tests { assert!(params.validate_for_hft_trendfollowing().is_ok()); } - #[test] - fn test_validate_rejects_invalid_gamma() { - let params = DQNParams { - gamma: 1.5, - ..DQNParams::default() - }; - let err = params.validate_for_hft_trendfollowing().unwrap_err(); - assert!(err.contains("gamma"), "error should mention gamma: {err}"); - - // Zero atoms - let params_zero = DQNParams { - num_atoms: 0, - ..DQNParams::default() - }; - assert!(params_zero.validate_for_hft_trendfollowing().is_err()); - } - - #[test] - fn test_validate_rejects_zero_batch_size() { - let params = DQNParams { - batch_size: 0, - ..DQNParams::default() - }; - let err = params.validate_for_hft_trendfollowing().unwrap_err(); - assert!(err.contains("batch_size"), "error should mention batch_size: {err}"); - } - - #[test] - fn test_validate_rejects_invalid_learning_rate() { - // Zero LR - let params_zero = DQNParams { - learning_rate: 0.0, - ..DQNParams::default() - }; - assert!(params_zero.validate_for_hft_trendfollowing().is_err()); - - // Negative LR - let params_neg = DQNParams { - learning_rate: -1e-4, - ..DQNParams::default() - }; - assert!(params_neg.validate_for_hft_trendfollowing().is_err()); - - // LR too high (> 0.1) - let params_high = DQNParams { - learning_rate: 0.2, - ..DQNParams::default() - }; - let err = params_high.validate_for_hft_trendfollowing().unwrap_err(); - assert!(err.contains("learning_rate"), "error should mention learning_rate: {err}"); - - // LR at upper bound (0.1) should pass - let params_boundary = DQNParams { - learning_rate: 0.1, - ..DQNParams::default() - }; - assert!(params_boundary.validate_for_hft_trendfollowing().is_ok()); - } - #[test] fn test_validate_rejects_invalid_gamma() { // Zero gamma diff --git a/crates/ml/src/trainers/dqn/trainer/tests.rs b/crates/ml/src/trainers/dqn/trainer/tests.rs index 641093add..351aea685 100644 --- a/crates/ml/src/trainers/dqn/trainer/tests.rs +++ b/crates/ml/src/trainers/dqn/trainer/tests.rs @@ -575,42 +575,37 @@ fn test_c3_search_space_is_14d() { assert!(!names.contains(&"cvar_alpha"), "cvar_alpha should be fixed, not in search space"); } -/// Verify noisy_epsilon_floor is fixed to 0.10 (prevents action collapse). +/// Verify exploration_intensity defaults to 1.0 (neutral scaling of exploration params). +/// noisy_epsilon_floor and count_bonus_coefficient are now fixed in DQNHyperparameters, +/// not in DQNParams. #[test] -fn test_noisy_epsilon_floor_fixed() { +fn test_exploration_intensity_default() { let params = crate::hyperopt::adapters::dqn::DQNParams::default(); assert!( - (params.noisy_epsilon_floor - 0.10).abs() < 1e-6, - "noisy_epsilon_floor must default to 0.10 (prevents action collapse)" + (params.exploration_intensity - 1.0).abs() < 1e-6, + "exploration_intensity must default to 1.0 (neutral)" ); } -/// Verify exploration params are fixed after C2 cleanup. +/// Verify DQNParams 14D roundtrip preserves all family intensities. #[test] -fn test_c2_exploration_params_fixed() { - let params = crate::hyperopt::adapters::dqn::DQNParams::default(); - assert!( - params.curiosity_weight.abs() < f64::EPSILON, - "curiosity_weight must be fixed at 0.0" - ); - assert!( - (params.noisy_epsilon_floor - 0.10).abs() < 1e-6, - "noisy_epsilon_floor must be fixed at 0.10" - ); - assert!( - (params.count_bonus_coefficient - 0.05).abs() < 1e-6, - "count_bonus_coefficient should be 0.05 (UCB exploration enabled)" - ); +fn test_14d_roundtrip_preserves_intensities() { + use crate::hyperopt::traits::ParameterSpace; - // Roundtrip through from_continuous should preserve fixed values + let params = crate::hyperopt::adapters::dqn::DQNParams::default(); let continuous = params.to_continuous(); let recovered = crate::hyperopt::adapters::dqn::DQNParams::from_continuous(&continuous).unwrap(); + assert!( - recovered.curiosity_weight.abs() < f64::EPSILON, - "curiosity_weight must remain 0.0 after roundtrip" + (recovered.exploration_intensity - params.exploration_intensity).abs() < 1e-6, + "exploration_intensity must survive roundtrip" ); assert!( - (recovered.count_bonus_coefficient - 0.05).abs() < 1e-6, - "count_bonus_coefficient must remain 0.05 after roundtrip" + (recovered.learning_intensity - params.learning_intensity).abs() < 1e-6, + "learning_intensity must survive roundtrip" + ); + assert!( + (recovered.ensemble_intensity - params.ensemble_intensity).abs() < 1e-6, + "ensemble_intensity must survive roundtrip" ); } diff --git a/crates/ml/tests/dqn_action_collapse_fix_test.rs b/crates/ml/tests/dqn_action_collapse_fix_test.rs index ec6e45ce1..357afd1ed 100644 --- a/crates/ml/tests/dqn_action_collapse_fix_test.rs +++ b/crates/ml/tests/dqn_action_collapse_fix_test.rs @@ -212,14 +212,14 @@ fn test_idle_penalty_in_gpu_composite_reward() { // ── 4. Hyperopt 26D search space includes cql_alpha ──────────────────────── #[test] -fn test_hyperopt_22d_search_space() { +fn test_hyperopt_14d_search_space() { use ml::hyperopt::adapters::dqn::DQNParams; use ml::hyperopt::traits::ParameterSpace; let bounds = DQNParams::continuous_bounds(); assert_eq!( - bounds.len(), 22, - "Search space should be 22D (consolidated from 46D), got {}D", + bounds.len(), 14, + "Search space should be 14D (3 breakouts + 5 core + 6 gen families), got {}D", bounds.len() ); @@ -231,28 +231,27 @@ fn test_hyperopt_22d_search_space() { names.len(), bounds.len() ); - // cql_alpha is now FIXED at default (not in search space) + // cql_alpha is fixed at TOML default (not in search space) assert!( !names.contains(&"cql_alpha"), - "cql_alpha should be fixed (not in 22D search space)" + "cql_alpha should be fixed (not in 14D search space)" ); } #[test] -fn test_hyperopt_v_max_in_search_space() { +fn test_hyperopt_architecture_intensity_in_search_space() { use ml::hyperopt::adapters::dqn::DQNParams; use ml::hyperopt::traits::ParameterSpace; let bounds = DQNParams::continuous_bounds(); let names = DQNParams::param_names(); - // v_max is at index 14 in the 22D space with range [5, 300] - let v_max_idx = names.iter().position(|&n| n == "v_max") - .expect("v_max should be in param_names"); - let (lo, hi) = bounds[v_max_idx]; - assert!(lo >= 1.0 && hi <= 500.0, - "v_max bounds should be reasonable, got ({}, {})", lo, hi); - assert!(lo < hi, "v_max should have a range, got ({}, {})", lo, hi); + // v_max is now absorbed into the architecture family. Verify architecture_intensity exists. + let arch_idx = names.iter().position(|&n| n == "architecture_intensity") + .expect("architecture_intensity should be in param_names"); + let (lo, hi) = bounds[arch_idx]; + assert_eq!(lo, 0.0, "architecture_intensity lower bound should be 0.0"); + assert_eq!(hi, 2.0, "architecture_intensity upper bound should be 2.0"); } #[test] @@ -261,12 +260,11 @@ fn test_hyperopt_iqn_always_enabled() { let params = DQNParams::default(); // IQN is always enabled (iqn_lambda > 0 by default) - assert!(params.iqn_lambda > 0.0, "IQN should be enabled by default"); - assert!(params.num_quantiles >= 32, "Should have at least 32 quantiles"); + assert!(params.iqn_lambda > 0.0, "IQN should be enabled by default (iqn_lambda > 0)"); } #[test] -fn test_hyperopt_22d_round_trip() { +fn test_hyperopt_14d_round_trip() { use ml::hyperopt::adapters::dqn::DQNParams; use ml::hyperopt::traits::ParameterSpace; @@ -278,12 +276,12 @@ fn test_hyperopt_22d_round_trip() { let reconstructed = DQNParams::from_continuous(&continuous).expect("from_continuous should succeed"); - // cql_alpha is fixed at default in 22D space - let diff = (reconstructed.cql_alpha - params.cql_alpha).abs(); + // gamma should survive round-trip + let diff = (reconstructed.gamma - params.gamma).abs(); assert!( diff < 0.01, - "cql_alpha should preserve default: expected {}, got {} (diff={})", - params.cql_alpha, reconstructed.cql_alpha, diff + "gamma should preserve default: expected {}, got {} (diff={})", + params.gamma, reconstructed.gamma, diff ); } diff --git a/crates/ml/tests/dqn_hyperopt_json_export_test.rs b/crates/ml/tests/dqn_hyperopt_json_export_test.rs index 17bde390d..298936df3 100644 --- a/crates/ml/tests/dqn_hyperopt_json_export_test.rs +++ b/crates/ml/tests/dqn_hyperopt_json_export_test.rs @@ -173,12 +173,11 @@ mod hyperopt_json_export_tests { 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) + // Verify hyperparameters are present (14 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"); + assert!(params.learning_intensity >= 0.0 && params.learning_intensity <= 2.0, "learning_intensity should be in [0, 2]"); + assert!(params.iqn_lambda >= 0.0, "iqn_lambda should be >= 0"); info!(trial_number = trial.trial_number, sharpe = trial.sharpe, "JSON file created with best trial"); @@ -278,30 +277,26 @@ mod hyperopt_json_export_tests { assert_eq!(saved_trial.gradient_clip_norm, roundtrip_trial.gradient_clip_norm, "Gradient clip norm should match exactly"); - // Verify all 21 hyperparameters match + // Verify all 14 hyperparameters match (3 breakouts + 11 family intensities) 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); + assert_eq!(saved_params.iqn_lambda, roundtrip_params.iqn_lambda); + assert_eq!(saved_params.c51_warmup_epochs, roundtrip_params.c51_warmup_epochs); + assert_eq!(saved_params.learning_intensity, roundtrip_params.learning_intensity); + assert_eq!(saved_params.exploration_intensity, roundtrip_params.exploration_intensity); + assert_eq!(saved_params.replay_intensity, roundtrip_params.replay_intensity); + assert_eq!(saved_params.architecture_intensity, roundtrip_params.architecture_intensity); + assert_eq!(saved_params.risk_intensity, roundtrip_params.risk_intensity); + assert_eq!(saved_params.adversarial_intensity, roundtrip_params.adversarial_intensity); + assert_eq!(saved_params.regularization_intensity, roundtrip_params.regularization_intensity); + assert_eq!(saved_params.augmentation_intensity, roundtrip_params.augmentation_intensity); + assert_eq!(saved_params.loss_shaping_intensity, roundtrip_params.loss_shaping_intensity); + assert_eq!(saved_params.ensemble_intensity, roundtrip_params.ensemble_intensity); + assert_eq!(saved_params.causal_intensity, roundtrip_params.causal_intensity); - info!("All 21 hyperparameters + metadata survive roundtrip with perfect fidelity"); + info!("All 14 hyperparameters + metadata survive roundtrip with perfect fidelity"); cleanup_test_jsons("best_trial_sharpe_"); @@ -350,20 +345,17 @@ mod hyperopt_json_export_tests { assert!(timestamp.contains('T') && timestamp.contains('Z'), "Timestamp should be ISO 8601 format: {}", timestamp); - // Verify hyperparameters object has all 21 fields + // Verify hyperparameters object has all 14 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", + "gamma", "iqn_lambda", "c51_warmup_epochs", + "learning_intensity", "exploration_intensity", "replay_intensity", + "architecture_intensity", "risk_intensity", + "adversarial_intensity", "regularization_intensity", + "augmentation_intensity", "loss_shaping_intensity", + "ensemble_intensity", "causal_intensity", ]; for field in required_fields { @@ -371,7 +363,7 @@ mod hyperopt_json_export_tests { "Hyperparameters should contain field: {}", field); } - info!("JSON contains all 8 metadata fields + 21 hyperparameter fields"); + info!("JSON contains all 8 metadata fields + 14 hyperparameter fields"); cleanup_test_jsons("best_trial_sharpe_"); diff --git a/crates/ml/tests/dqn_hyperopt_test.rs b/crates/ml/tests/dqn_hyperopt_test.rs index 1e2e8362c..4e0bf31c0 100644 --- a/crates/ml/tests/dqn_hyperopt_test.rs +++ b/crates/ml/tests/dqn_hyperopt_test.rs @@ -209,28 +209,27 @@ fn test_dqn_hyperopt_10_trials() -> Result<()> { ); } - // 7. Best params should have sane learning rate (log-scale, typically 1e-5 to 1e-2) - let best_lr = result.best_params.learning_rate; + // 7. Best params should have sane gamma (in [0.95, 0.999]) + let best_gamma = result.best_params.gamma; assert!( - best_lr > 1e-7 && best_lr < 1.0, - "Best learning rate out of sane range: {best_lr}" + best_gamma >= 0.95 && best_gamma <= 0.999, + "Best gamma out of sane range: {best_gamma}" ); - // 8. Best params batch size should be positive and bounded - let best_bs = result.best_params.batch_size; + // 8. Best params learning_intensity should be in [0.0, 2.0] + let best_li = result.best_params.learning_intensity; assert!( - best_bs > 0 && best_bs <= 512, - "Best batch size out of range: {best_bs}" + best_li >= 0.0 && best_li <= 2.0, + "Best learning_intensity out of range: {best_li}" ); // --- Report --- info!( trials_completed, best_objective = result.best_objective, - best_lr = result.best_params.learning_rate, - best_batch_size = result.best_params.batch_size, best_gamma = result.best_params.gamma, - best_buffer_size = result.best_params.buffer_size, + best_learning_intensity = result.best_params.learning_intensity, + best_exploration_intensity = result.best_params.exploration_intensity, total_secs = elapsed.as_secs_f64(), avg_secs_per_trial = elapsed.as_secs_f64() / trials_completed as f64, "DQN HYPEROPT REPORT" @@ -240,8 +239,8 @@ fn test_dqn_hyperopt_10_trials() -> Result<()> { trial_num = trial.trial_num, objective = trial.objective, duration_secs = trial.duration_secs, - lr = trial.params.learning_rate, - batch_size = trial.params.batch_size, + gamma = trial.params.gamma, + learning_intensity = trial.params.learning_intensity, "Trial history" ); } diff --git a/crates/ml/tests/dqn_transaction_cost_bug2_test.rs b/crates/ml/tests/dqn_transaction_cost_bug2_test.rs index b33a2cacd..49b2898b5 100644 --- a/crates/ml/tests/dqn_transaction_cost_bug2_test.rs +++ b/crates/ml/tests/dqn_transaction_cost_bug2_test.rs @@ -129,27 +129,25 @@ fn test_cost_weight_is_full_weight() { info!("cost_weight = 1.0 (Bug #2 fix implemented): before=0.05 (5% of actual costs), after=1.0 (100% of actual costs)"); } -/// Test that transaction_cost_multiplier parameter still exists (NOT removed yet) +/// Test that transaction_cost_multiplier has been removed from DQNParams. /// -/// **Current**: DQNParams DOES have transaction_cost_multiplier field -/// **Expected (after fix)**: Field should be removed -/// **Status**: Bug #2 fix NOT YET IMPLEMENTED +/// After the 14D family-intensity restructuring, transaction_cost_multiplier +/// is no longer a tunable hyperparameter. Transaction costs are fixed market +/// rates from OrderType enum. #[test] fn test_transaction_cost_multiplier_removed() { - // Verify field still exists in current implementation - let params = DQNParams::default(); + // DQNParams now only has 14 fields (3 breakouts + 11 family intensities). + // transaction_cost_multiplier was removed -- verify the search space is 14D. + let bounds = DQNParams::continuous_bounds(); + assert_eq!(bounds.len(), 14, "Search space should be 14D after family consolidation"); - // Access the field to prove it exists - let tx_cost = params.transaction_cost_multiplier; - - // Verify default value is 1.0 - assert_eq!( - tx_cost, - 1.0, - "transaction_cost_multiplier should default to 1.0" + let names = DQNParams::param_names(); + assert!( + !names.contains(&"transaction_cost_multiplier"), + "transaction_cost_multiplier should NOT be in search space" ); - info!(tx_cost, "transaction_cost_multiplier field still exists in DQNParams; Bug #2 fix will remove this field"); + info!("transaction_cost_multiplier removed from DQNParams; transaction costs are fixed market rates from OrderType"); } /// Test that LimitMaker orders have lowest cost (0.05%) @@ -182,33 +180,26 @@ fn test_order_type_transaction_costs() { /// Integration test: Verify hyperopt search space dimensions /// -/// **Current State**: Search space is 18D (11D base + 6D Rainbow + 1D Bug #7) -/// **Bug #2 STATUS**: Transaction cost multiplier NOT YET REMOVED (still at dimension #8) -/// **Note**: Future Bug #2 fix will remove transaction_cost_multiplier → 17D +/// Search space is now 14D (3 breakouts + 5 core families + 6 gen families). +/// transaction_cost_multiplier was removed during family-intensity consolidation. #[test] fn test_hyperopt_search_space_reduced() { let bounds = DQNParams::continuous_bounds(); - // Verify current search space is 18D - // Breakdown: 11D base (including transaction_cost_multiplier) + 6D Rainbow + 1D minimum_profit_factor assert_eq!( bounds.len(), - 18, - "Current hyperopt search space should be 18D\n\ - Got: {}D\n\ - Breakdown: 11D base + 6D Rainbow + 1D minimum_profit_factor", + 14, + "Hyperopt search space should be 14D (3 breakouts + 5 core + 6 gen families), got {}D", bounds.len() ); - // Verify transaction_cost_multiplier is STILL in search space at dimension #8 - let tx_cost_bounds = bounds[8]; - assert_eq!( - tx_cost_bounds, - (0.5, 2.0), - "Transaction cost multiplier should be at dimension #8 with bounds (0.5, 2.0)" + let names = DQNParams::param_names(); + assert!( + !names.contains(&"transaction_cost_multiplier"), + "transaction_cost_multiplier should not be in 14D search space" ); - info!(dims = bounds.len(), "Hyperopt search space is 18D (11D base + 6D Rainbow + 1D profit); dimension #8: transaction_cost_multiplier (0.5-2.0) still present; Bug #2 fix not yet implemented"); + info!(dims = bounds.len(), "Hyperopt search space is 14D; transaction_cost_multiplier removed (fixed market rates)"); } /// Test that transaction costs correctly reflect factored action order types diff --git a/crates/ml/tests/ensemble_hyperopt_tests.rs b/crates/ml/tests/ensemble_hyperopt_tests.rs index bce97f764..83d3d1100 100644 --- a/crates/ml/tests/ensemble_hyperopt_tests.rs +++ b/crates/ml/tests/ensemble_hyperopt_tests.rs @@ -72,11 +72,12 @@ clippy::unnecessary_to_owned, clippy::single_component_path_imports, )] -//! TDD Tests for Ensemble Uncertainty Fields in Hyperopt +//! TDD Tests for Ensemble Intensity in the 14D Hyperopt Search Space //! -//! Validates that ensemble uncertainty parameters are properly set as fixed defaults -//! in the current 39D search space (ensemble params were removed from search space, -//! they are now fixed in from_continuous). +//! Validates that ensemble_intensity is properly included in the 14D search space +//! and that from_continuous / to_continuous round-trip correctly. +//! Individual ensemble params (ensemble_size, beta_variance, etc.) are now fixed +//! in DQNHyperparameters, not in DQNParams. use ml::hyperopt::adapters::dqn::DQNParams; use ml::hyperopt::ParameterSpace; @@ -84,10 +85,9 @@ use ml::trainers::dqn::DQNHyperparameters; #[test] fn test_dqn_hyperparameters_has_ensemble_fields() { - // WAVE 26 P1.4: Verify DQNHyperparameters struct has 6 new ensemble fields + // Verify DQNHyperparameters struct has ensemble fields (these live here, not in DQNParams) let hyperparams = DQNHyperparameters::conservative(); - // Check default values exist (compilation test - if fields don't exist, this won't compile) let _use_ensemble = hyperparams.use_ensemble_uncertainty; let _ensemble_size = hyperparams.ensemble_size; let _beta_variance = hyperparams.beta_variance; @@ -95,7 +95,6 @@ fn test_dqn_hyperparameters_has_ensemble_fields() { let _beta_entropy = hyperparams.beta_entropy; let _variance_cap = hyperparams.variance_cap; - // Verify conservative defaults are reasonable assert!(!hyperparams.use_ensemble_uncertainty, "Default should be disabled for conservative config"); assert_eq!(hyperparams.ensemble_size, 5, "Default ensemble size should be 5"); assert!(hyperparams.beta_variance >= 0.0 && hyperparams.beta_variance <= 1.0, "Beta variance should be in [0, 1]"); @@ -105,119 +104,103 @@ fn test_dqn_hyperparameters_has_ensemble_fields() { } #[test] -fn test_dqn_params_has_ensemble_fields() { - // WAVE 26 P1.4: Verify DQNParams struct has 5 new ensemble fields +fn test_dqn_params_has_ensemble_intensity() { + // DQNParams now has ensemble_intensity (a single scalar) instead of individual ensemble fields let params = DQNParams::default(); - - // Check default values exist (compilation test) - let _use_ensemble = params.use_ensemble_uncertainty; - let _ensemble_size = params.ensemble_size; - let _beta_variance = params.beta_variance; - let _beta_disagreement = params.beta_disagreement; - let _beta_entropy = params.beta_entropy; - - // Verify defaults match conservative config - assert!(!params.use_ensemble_uncertainty, "Default should be disabled"); - assert_eq!(params.ensemble_size, 5.0, "Default ensemble size should be 5.0"); - assert!(params.beta_variance > 0.0, "Beta variance should be positive"); - assert!(params.beta_disagreement > 0.0, "Beta disagreement should be positive"); - assert!(params.beta_entropy > 0.0, "Beta entropy should be positive"); + assert!( + params.ensemble_intensity >= 0.0 && params.ensemble_intensity <= 2.0, + "ensemble_intensity should be in [0.0, 2.0], got {}", + params.ensemble_intensity + ); + assert!( + (params.ensemble_intensity - 1.0).abs() < 1e-6, + "Default ensemble_intensity should be 1.0 (neutral)" + ); } #[test] fn test_ensemble_search_space_bounds() { - // Ensemble params (ensemble_size, beta_variance, etc.) are no longer in the - // continuous search space — they are fixed in from_continuous(). Verify that - // the 39D search space is properly sized and ensemble defaults are set. + // 14D search space: 3 breakouts + 5 core families + 6 gen families let bounds = DQNParams::continuous_bounds(); - assert_eq!(bounds.len(), 39, "Search space should have 39 continuous parameters total (C8: +8 reward/policy weights)"); + assert_eq!(bounds.len(), 14, "Search space should have 14 continuous parameters"); - // Ensemble params are NOT in bounds — they're fixed in from_continuous. - // Verify a DQNParams default still has correct ensemble defaults. - let params = DQNParams::default(); - assert!(!params.use_ensemble_uncertainty, "Default: ensemble disabled"); - assert_eq!(params.ensemble_size, 5.0, "Default ensemble size should be 5.0"); - assert!(params.beta_variance > 0.0, "Beta variance should be positive"); - assert!(params.beta_disagreement > 0.0, "Beta disagreement should be positive"); - assert!(params.beta_entropy > 0.0, "Beta entropy should be positive"); + // ensemble_intensity is at index 12 + let names = DQNParams::param_names(); + let ens_idx = names.iter().position(|&n| n == "ensemble_intensity") + .expect("ensemble_intensity should be in param_names"); + assert_eq!(ens_idx, 12, "ensemble_intensity should be at index 12"); + + let (lo, hi) = bounds[ens_idx]; + assert_eq!(lo, 0.0, "ensemble_intensity lower bound should be 0.0"); + assert_eq!(hi, 2.0, "ensemble_intensity upper bound should be 2.0"); } #[test] -fn test_from_continuous_validates_ensemble_params() { - // Ensemble params are now fixed in from_continuous() (not in 39D search space). - // Verify that from_continuous produces correct fixed ensemble defaults. +fn test_from_continuous_sets_ensemble_intensity() { + // from_continuous with midpoint values let bounds = DQNParams::continuous_bounds(); let x: Vec = bounds.iter().map(|(lo, hi)| (lo + hi) / 2.0).collect(); - assert_eq!(x.len(), 39, "Should produce 39D midpoint vector"); + assert_eq!(x.len(), 14, "Should produce 14D midpoint vector"); let params = DQNParams::from_continuous(&x).expect("Should convert valid parameters"); - // Ensemble parameters are fixed to conservative defaults - assert_eq!(params.ensemble_size, 5.0, "Ensemble size should be fixed to 5.0"); - assert_eq!(params.beta_variance, 0.5, "Beta variance should be fixed to 0.5"); - assert_eq!(params.beta_disagreement, 0.5, "Beta disagreement should be fixed to 0.5"); - assert_eq!(params.beta_entropy, 0.2, "Beta entropy should be fixed to 0.2"); - assert!(!params.use_ensemble_uncertainty, "Ensemble uncertainty should be disabled"); + // ensemble_intensity should be the midpoint of [0.0, 2.0] = 1.0 + assert!( + (params.ensemble_intensity - 1.0).abs() < 1e-6, + "ensemble_intensity should be 1.0 at midpoint, got {}", + params.ensemble_intensity + ); } #[test] -fn test_from_continuous_clamps_ensemble_params() { - // Ensemble parameters are now fixed (not in search space), so clamping - // is not relevant. Instead verify that from_continuous rejects wrong-length input. - let x = vec![0.0; 40]; // Wrong length (should be 39) +fn test_from_continuous_rejects_wrong_length() { + // from_continuous rejects wrong-length input + let x = vec![0.0; 15]; // Wrong length (should be 14) let result = DQNParams::from_continuous(&x); - assert!(result.is_err(), "Should reject wrong-length (40) input"); + assert!(result.is_err(), "Should reject wrong-length (15) input"); - let x2 = vec![0.0; 31]; // Also wrong length + let x2 = vec![0.0; 13]; // Also wrong length let result2 = DQNParams::from_continuous(&x2); - assert!(result2.is_err(), "Should reject wrong-length (31) input"); + assert!(result2.is_err(), "Should reject wrong-length (13) input"); - // Correct length (39) with valid midpoints should succeed + // Correct length (14) with valid midpoints should succeed let bounds = DQNParams::continuous_bounds(); - let x38: Vec = bounds.iter().map(|(lo, hi)| (lo + hi) / 2.0).collect(); - let params = DQNParams::from_continuous(&x38).expect("Should accept 39D input"); - // Ensemble params are fixed regardless of input - assert_eq!(params.ensemble_size, 5.0, "Ensemble size fixed to 5.0"); + let x14: Vec = bounds.iter().map(|(lo, hi)| (lo + hi) / 2.0).collect(); + let params = DQNParams::from_continuous(&x14).expect("Should accept 14D input"); + assert!( + (params.ensemble_intensity - 1.0).abs() < 1e-6, + "ensemble_intensity should be 1.0 at midpoint" + ); } #[test] -fn test_to_continuous_includes_reward_weights() { - // C8: Verify that to_continuous() includes 7 composite reward weights (indices 31-37) +fn test_to_continuous_roundtrip() { + // Verify to_continuous includes all 14 fields and round-trips correctly let mut params = DQNParams::default(); - - // Set reward weights to specific values - params.w_dsr = 1.5; - params.w_pnl = 0.5; - params.w_dd = 2.0; - params.w_idle = 0.05; - params.dd_threshold = 0.04; - params.loss_aversion = 2.0; - params.time_decay_rate = 0.001; + params.ensemble_intensity = 1.5; + params.causal_intensity = 0.8; let continuous = params.to_continuous(); - assert_eq!(continuous.len(), 39, "Continuous representation should have 39 values"); + assert_eq!(continuous.len(), 14, "Continuous representation should have 14 values"); - // Verify reward weight positions (indices 31-37) - assert_eq!(continuous[31], 1.5, "w_dsr should be at position 31"); - assert_eq!(continuous[32], 0.5, "w_pnl should be at position 32"); - assert_eq!(continuous[33], 2.0, "w_dd should be at position 33"); - assert_eq!(continuous[34], 0.05, "w_idle should be at position 34"); - assert_eq!(continuous[35], 0.04, "dd_threshold should be at position 35"); - assert_eq!(continuous[36], 2.0, "loss_aversion should be at position 36"); - assert_eq!(continuous[37], 0.001, "time_decay_rate should be at position 37"); - - // Ensemble params are NOT in the continuous vector (they're fixed) - // Verify the vector doesn't include them - assert_eq!(continuous.len(), 39); + let recovered = DQNParams::from_continuous(&continuous).expect("Should recover from continuous"); + assert!( + (recovered.ensemble_intensity - 1.5).abs() < 1e-6, + "ensemble_intensity should survive roundtrip: expected 1.5, got {}", + recovered.ensemble_intensity + ); + assert!( + (recovered.causal_intensity - 0.8).abs() < 1e-6, + "causal_intensity should survive roundtrip: expected 0.8, got {}", + recovered.causal_intensity + ); } #[test] -fn test_ensemble_size_fixed_in_from_continuous() { - // Ensemble size is now fixed at 5.0 in from_continuous() — not in search space. - // Verify that from_continuous always produces ensemble_size=5.0 regardless of input. - let bounds = DQNParams::continuous_bounds(); - let x: Vec = bounds.iter().map(|(lo, hi)| (lo + hi) / 2.0).collect(); - let params = DQNParams::from_continuous(&x).expect("Should convert"); - assert_eq!(params.ensemble_size, 5.0, "Ensemble size should always be fixed to 5.0"); - assert!(!params.use_ensemble_uncertainty, "Ensemble uncertainty should be disabled"); +fn test_ensemble_intensity_in_param_names() { + // Verify ensemble_intensity is listed in param_names at position 12 + let names = DQNParams::param_names(); + assert_eq!(names.len(), 14, "param_names should have 14 entries"); + assert_eq!(names[12], "ensemble_intensity", "Index 12 should be ensemble_intensity"); + assert_eq!(names[13], "causal_intensity", "Index 13 should be causal_intensity"); } diff --git a/crates/ml/tests/hyperopt_kelly_params_test.rs b/crates/ml/tests/hyperopt_kelly_params_test.rs index 83bad6693..b7f0a4c07 100644 --- a/crates/ml/tests/hyperopt_kelly_params_test.rs +++ b/crates/ml/tests/hyperopt_kelly_params_test.rs @@ -72,79 +72,81 @@ clippy::unnecessary_to_owned, clippy::single_component_path_imports, )] +//! Hyperopt search space dimension tests (updated for 14D family-intensity structure). +//! +//! Kelly params (kelly_fractional, kelly_max_fraction, etc.) are now fixed at TOML +//! defaults and absorbed into the risk_intensity family. The search space is 14D. + #[cfg(test)] mod hyperopt_kelly_params_tests { use ml::hyperopt::adapters::dqn::DQNParams; use ml::hyperopt::traits::ParameterSpace; - + #[test] - fn test_search_space_has_22_dimensions() { + fn test_search_space_has_14_dimensions() { let bounds = DQNParams::continuous_bounds(); - assert_eq!(bounds.len(), 22, "Search space should be 22D after adding Kelly params"); + assert_eq!(bounds.len(), 14, "Search space should be 14D (3 breakouts + 5 core + 6 gen families)"); } - + #[test] - fn test_kelly_fractional_bounds() { + fn test_risk_intensity_bounds() { let bounds = DQNParams::continuous_bounds(); - let (min, max) = bounds[18]; // Kelly fractional at index 18 - assert_eq!(min, 0.25, "Kelly fractional min should be 0.25 (quarter-Kelly)"); - assert_eq!(max, 1.0, "Kelly fractional max should be 1.0 (full-Kelly)"); + let names = DQNParams::param_names(); + let risk_idx = names.iter().position(|&n| n == "risk_intensity") + .expect("risk_intensity should be in param_names"); + let (min, max) = bounds[risk_idx]; + assert_eq!(min, 0.0, "risk_intensity min should be 0.0"); + assert_eq!(max, 2.0, "risk_intensity max should be 2.0"); } - + #[test] - fn test_kelly_max_fraction_bounds() { + fn test_from_continuous_accepts_14d() { let bounds = DQNParams::continuous_bounds(); - let (min, max) = bounds[19]; - assert_eq!(min, 0.1, "Kelly max_fraction min should be 0.1 (10%)"); - assert_eq!(max, 0.5, "Kelly max_fraction max should be 0.5 (50%)"); - } - - #[test] - fn test_kelly_min_trades_bounds() { - let bounds = DQNParams::continuous_bounds(); - let (min, max) = bounds[20]; - assert_eq!(min, 10.0, "Kelly min_trades min should be 10"); - assert_eq!(max, 50.0, "Kelly min_trades max should be 50"); - } - - #[test] - fn test_volatility_window_bounds() { - let bounds = DQNParams::continuous_bounds(); - let (min, max) = bounds[21]; - assert_eq!(min, 10.0, "Volatility window min should be 10"); - assert_eq!(max, 30.0, "Volatility window max should be 30"); - } - - #[test] - fn test_from_continuous_accepts_22d() { - let x = vec![ - // Existing 18 params (use mid-range values) - (-4.5_f64).ln(), 112.0, 0.97, (75000.0_f64).ln(), - 1.5, 6.0, (25.0_f64).ln(), 0.05, 1.25, 0.6, 0.4, - -2.0, 2.0, (0.5_f64).ln(), 320.0, 3.0, 126.0, 1.55, - // New Kelly params - 0.5, 0.25, 30.0, 20.0 - ]; - + let x: Vec = bounds.iter().map(|(lo, hi)| (lo + hi) / 2.0).collect(); + assert_eq!(x.len(), 14); + let result = DQNParams::from_continuous(&x); - assert!(result.is_ok(), "Should accept 22D parameter vector"); - + assert!(result.is_ok(), "Should accept 14D parameter vector"); + let params = result.unwrap(); - assert!((params.kelly_fractional - 0.5).abs() < 0.01); - assert!((params.kelly_max_fraction - 0.25).abs() < 0.01); - assert_eq!(params.kelly_min_trades, 30); - assert_eq!(params.volatility_window, 20); + assert!((params.gamma - 0.9745).abs() < 0.001, "gamma should be midpoint of [0.95, 0.999]"); + assert!((params.risk_intensity - 1.0).abs() < 1e-6, "risk_intensity should be 1.0 at midpoint"); } - + #[test] - fn test_from_continuous_rejects_18d() { - let x = vec![ - (-4.5_f64).ln(), 112.0, 0.97, (75000.0_f64).ln(), - 1.5, 6.0, (25.0_f64).ln(), 0.05, 1.25, 0.6, 0.4, - -2.0, 2.0, (0.5_f64).ln(), 320.0, 3.0, 126.0, 1.55, - ]; - - let result = DQNParams::from_continuous(&x); - assert!(result.is_err(), "Should reject old 18D parameter vector"); + fn test_from_continuous_rejects_wrong_dimensions() { + // Too few dimensions + let x_short = vec![0.97; 13]; + let result_short = DQNParams::from_continuous(&x_short); + assert!(result_short.is_err(), "Should reject 13D parameter vector"); + + // Too many dimensions + let x_long = vec![0.97; 15]; + let result_long = DQNParams::from_continuous(&x_long); + assert!(result_long.is_err(), "Should reject 15D parameter vector"); + } + + #[test] + fn test_all_intensity_bounds_are_0_to_2() { + let bounds = DQNParams::continuous_bounds(); + let names = DQNParams::param_names(); + for (i, name) in names.iter().enumerate() { + if name.ends_with("_intensity") { + let (lo, hi) = bounds[i]; + assert_eq!(lo, 0.0, "{name} lower bound should be 0.0"); + assert_eq!(hi, 2.0, "{name} upper bound should be 2.0"); + } + } + } + + #[test] + fn test_breakout_params_have_specific_bounds() { + let bounds = DQNParams::continuous_bounds(); + // gamma at index 0 + assert_eq!(bounds[0], (0.95, 0.999), "gamma bounds should be (0.95, 0.999)"); + // iqn_lambda at index 1 + assert_eq!(bounds[1], (0.0, 1.0), "iqn_lambda bounds should be (0.0, 1.0)"); + // c51_warmup_epochs at index 2 + assert_eq!(bounds[2], (3.0, 15.0), "c51_warmup_epochs bounds should be (3.0, 15.0)"); } } diff --git a/crates/ml/tests/ofi_features_test.rs b/crates/ml/tests/ofi_features_test.rs index 4f516d5bf..fa893c5de 100644 --- a/crates/ml/tests/ofi_features_test.rs +++ b/crates/ml/tests/ofi_features_test.rs @@ -550,3 +550,111 @@ fn test_performance_benchmark() { avg_time ); } + +/// Verify that OFI features integrate correctly with the state vector layout. +/// +/// State layout: [42 market + 8 portfolio + 16 multi-tf + 8 OFI] = 74 raw, 80 aligned. +/// Without OFI: [42 market + 8 portfolio + 16 multi-tf] = 66 raw, 72 aligned. +#[test] +fn test_ofi_features_state_integration() { + // Simulate the state-building path from crates/ml/src/trainers/dqn/trainer/state.rs + // OFI features are appended as `regime_features` in TradingState::from_normalized(). + + let price_features: Vec = vec![0.01, 0.02, -0.01, 0.005]; // 4 OHLC log returns + let technical_indicators: Vec = vec![]; // empty in 42-feature architecture + let market_features: Vec = vec![0.5; 38]; // indices 4..42 + let portfolio_features: Vec = vec![0.0, 0.0, 0.0]; // 3 portfolio features + + // Compute OFI features from test snapshots + let mut calc = OFICalculator::new(); + let snap1 = create_snapshot(1000, 150_000_000_000_000, 150_010_000_000_000, 100, 120); + let snap2 = create_snapshot(2000, 150_005_000_000_000, 150_010_000_000_000, 110, 115); + calc.calculate(&snap1).unwrap(); + let ofi = calc.calculate(&snap2).unwrap(); + let regime_features: Vec = ofi.to_array().iter().map(|&v| v as f32).collect(); + + assert_eq!(regime_features.len(), 8, "OFI must produce 8 features"); + + // Build state vector (same as TradingState::to_vector()) + let mut state_vec = Vec::new(); + state_vec.extend_from_slice(&price_features); // 4 + state_vec.extend_from_slice(&technical_indicators); // 0 + state_vec.extend_from_slice(&market_features); // 38 + state_vec.extend_from_slice(&portfolio_features); // 3 + state_vec.extend_from_slice(®ime_features); // 8 + + // 4 + 38 + 3 + 8 = 53 (raw state before multi-tf expansion; multi-tf is added in GPU pipeline) + // The full pipeline adds 8 portfolio features (not 3) + 16 multi-tf: + // 42 market + 8 portfolio + 16 MTF + 8 OFI = 74 raw, 80 aligned. + // Here we verify the raw OFI portion is correctly positioned. + assert_eq!(state_vec.len(), 4 + 38 + 3 + 8); // = 53 + assert_eq!(state_vec.len() - 8, 45, "Without OFI the state would be 45-dim"); + + // Verify OFI features are non-zero (calculated from actual snapshots) + let ofi_start = state_vec.len() - 8; + let ofi_slice = &state_vec[ofi_start..]; + assert!( + ofi_slice.iter().any(|&v| v != 0.0), + "OFI features should be non-zero when computed from real snapshots" + ); + + // Verify all features are finite + for (i, &v) in state_vec.iter().enumerate() { + assert!(v.is_finite(), "Feature at index {} is not finite: {}", i, v); + } +} + +/// Verify zero-fill behavior when no MBP-10 data is available. +#[test] +fn test_ofi_features_graceful_degradation() { + let regime_features: Vec = OFIFeatures::zeros().to_array().iter().map(|&v| v as f32).collect(); + + assert_eq!(regime_features.len(), 8); + assert!( + regime_features.iter().all(|&v| v == 0.0), + "Zero-filled OFI features should all be 0.0" + ); + + // State dim should still be valid — no NaN or Inf + let mut state_vec = vec![0.0_f32; 42]; // market features + state_vec.extend_from_slice(&[0.0; 3]); // portfolio + state_vec.extend_from_slice(®ime_features); // 8 OFI zeros + + assert_eq!(state_vec.len(), 53); + assert!(state_vec.iter().all(|v| v.is_finite())); +} + +/// Verify OFI feature names at indices 42-49 in the unified feature space. +#[test] +fn test_ofi_feature_names_ordering() { + // The 8 OFI features occupy indices [42..50) in the full 50-dim feature space + // when appended to the 42-dim base market features. + let ofi_names = [ + "ofi_level1", + "ofi_level5", + "depth_imbalance", + "vpin", + "kyles_lambda", + "bid_slope", + "ask_slope", + "trade_imbalance", + ]; + + assert_eq!(ofi_names.len(), 8, "Must have exactly 8 OFI feature names"); + + // Verify the to_array() order matches the name ordering + let features = OFIFeatures { + ofi_level1: 1.0, + ofi_level5: 2.0, + depth_imbalance: 3.0, + vpin: 4.0, + kyle_lambda: 5.0, + bid_slope: 6.0, + ask_slope: 7.0, + trade_imbalance: 8.0, + }; + let arr = features.to_array(); + for (i, &val) in arr.iter().enumerate() { + assert_eq!(val, (i + 1) as f64, "Feature '{}' at index {} has wrong value", ofi_names[i], i); + } +} diff --git a/crates/ml/tests/production_training_smoke_test.rs b/crates/ml/tests/production_training_smoke_test.rs index e56d6f10a..bbcfb5463 100644 --- a/crates/ml/tests/production_training_smoke_test.rs +++ b/crates/ml/tests/production_training_smoke_test.rs @@ -94,44 +94,45 @@ use ml::hyperopt::early_stopping::{ use ml::hyperopt::traits::ParameterSpace; use tracing::{info, warn}; -/// Test 1: Verify DQNParams::default() has IQN disabled and CQL alpha=0.1 -/// (IQN disabled to fix train/inference network mismatch, CQL reduced from 1.0) +/// Test 1: Verify DQNParams::default() has correct 14D defaults +/// iqn_lambda > 0 means IQN is enabled; num_quantiles and cql_alpha are now +/// fixed in DQNHyperparameters (not in DQNParams). #[test] fn test_qr_dqn_defaults() -> Result<()> { let params = DQNParams::default(); - // IQN is always enabled by default - assert!(params.iqn_lambda > 0.0, "IQN should be enabled by default"); - assert_eq!( - params.num_quantiles, 32, - "DQNParams::default() should have num_quantiles: 32, got {}", - params.num_quantiles - ); - let cql_diff = (params.cql_alpha - 0.1).abs(); + // IQN is always enabled by default (iqn_lambda > 0) + assert!(params.iqn_lambda > 0.0, "IQN should be enabled by default (iqn_lambda > 0)"); assert!( - cql_diff < 1e-10, - "DQNParams::default() should have cql_alpha: 0.1, got {}", - params.cql_alpha + (params.gamma - 0.99).abs() < 1e-6, + "DQNParams::default() should have gamma: 0.99, got {}", + params.gamma + ); + assert!( + (params.learning_intensity - 1.0).abs() < 1e-6, + "DQNParams::default() should have learning_intensity: 1.0, got {}", + params.learning_intensity ); info!( - cql_alpha = params.cql_alpha, - num_quantiles = params.num_quantiles, - "Test 1 PASSED: DQN defaults verified" + gamma = params.gamma, + iqn_lambda = params.iqn_lambda, + learning_intensity = params.learning_intensity, + "Test 1 PASSED: DQN 14D defaults verified" ); Ok(()) } -/// Test 2: Verify 28D parameter space and round-trip preservation of CQL alpha +/// Test 2: Verify 14D parameter space and round-trip preservation #[test] -fn test_28d_parameter_space_round_trip() -> Result<()> { - // Verify dimensionality (28D search space including sharpe_weight) +fn test_14d_parameter_space_round_trip() -> Result<()> { + // Verify dimensionality (14D search space: 3 breakouts + 5 core + 6 gen families) let bounds = DQNParams::continuous_bounds(); assert_eq!( bounds.len(), - 28, - "DQNParams::continuous_bounds() should return 28 dimensions, got {}", + 14, + "DQNParams::continuous_bounds() should return 14 dimensions, got {}", bounds.len() ); @@ -149,34 +150,34 @@ fn test_28d_parameter_space_round_trip() -> Result<()> { let continuous = original.to_continuous(); assert_eq!( continuous.len(), - 28, - "to_continuous() should return 28 values, got {}", + 14, + "to_continuous() should return 14 values, got {}", continuous.len() ); let reconstructed = DQNParams::from_continuous(&continuous) .map_err(|e| anyhow::anyhow!("from_continuous failed: {}", e))?; - // Verify CQL alpha survives round trip - let cql_diff = (reconstructed.cql_alpha - original.cql_alpha).abs(); + // Verify gamma survives round trip + let gamma_diff = (reconstructed.gamma - original.gamma).abs(); assert!( - cql_diff < 0.01, - "Round-trip should preserve cql_alpha: expected {}, got {} (diff={})", - original.cql_alpha, reconstructed.cql_alpha, cql_diff + gamma_diff < 0.01, + "Round-trip should preserve gamma: expected {}, got {} (diff={})", + original.gamma, reconstructed.gamma, gamma_diff ); - // Verify other key params survive the round trip - let lr_diff = (reconstructed.learning_rate - original.learning_rate).abs(); + // Verify learning_intensity survives the round trip + let li_diff = (reconstructed.learning_intensity - original.learning_intensity).abs(); assert!( - lr_diff < 1e-8, - "Round-trip should preserve learning_rate: expected {}, got {}", - original.learning_rate, reconstructed.learning_rate + li_diff < 1e-8, + "Round-trip should preserve learning_intensity: expected {}, got {}", + original.learning_intensity, reconstructed.learning_intensity ); info!( dimensions = bounds.len(), - cql_alpha = reconstructed.cql_alpha, - "Test 2 PASSED: 26D parameter space round-trip verified" + gamma = reconstructed.gamma, + "Test 2 PASSED: 14D parameter space round-trip verified" ); Ok(())