#![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, )] //! Q-value Constraint Tests - Wave 14 Agent 27 //! //! Tests for the Q-value collapse detection constraint. //! //! Bug Fix: Previous implementation incorrectly rejected negative Q-values //! using `avg_q < 0.01`, which flagged valid negative Q-values as collapsed. //! //! Trading Context: Negative Q-values are VALID because they represent: //! - Transaction costs //! - Penalties (hold penalty, flip-flop penalty) //! - Trading fees //! - Negative expected returns in poor market conditions //! //! True Collapse: Q-values near zero (|q| < 0.01), not negative Q-values. #[cfg(test)] mod q_value_constraint_tests { /// Helper function to check if Q-value is collapsed /// This mirrors the logic that should be in dqn.rs fn is_q_value_collapsed(avg_q_value: f32) -> bool { // Check ABSOLUTE VALUE to detect true collapses near zero // Negative Q-values are valid in trading avg_q_value.abs() < 0.01 } #[test] fn test_negative_q_values_valid() { // GIVEN: Trading DQN with negative Q-values (costs dominate rewards) // These are real values from Wave 13 that were incorrectly rejected let test_cases = vec![ -3.37, // Valid negative Q-value (high costs) -43.32, // Valid large negative Q-value -2.1, // Valid moderate negative Q-value -0.5, // Valid small negative Q-value (|q| = 0.5 > 0.01) ]; for avg_q_value in test_cases { // WHEN: Constraint check is performed let is_collapsed = is_q_value_collapsed(avg_q_value); // THEN: Should NOT be flagged as collapsed assert!( !is_collapsed, "Negative Q-value {} should be VALID in trading (|q| = {:.4} > 0.01)", avg_q_value, avg_q_value.abs() ); } } #[test] fn test_near_zero_q_values_invalid() { // GIVEN: Q-values near zero (true collapse) // These indicate the network is not learning meaningful value estimates let test_cases = vec![ 0.009, // Positive near-zero -0.009, // Negative near-zero 0.0, // Exact zero 0.005, // Small positive -0.005, // Small negative 0.0099, // Just below threshold -0.0099, // Just below threshold (negative) ]; for avg_q_value in test_cases { // WHEN: Constraint check is performed let is_collapsed = is_q_value_collapsed(avg_q_value); // THEN: Should be flagged as collapsed assert!( is_collapsed, "Q-value {} (|q| = {:.4} < 0.01) should be flagged as collapsed", avg_q_value, avg_q_value.abs() ); } } #[test] fn test_large_magnitude_q_values_valid() { // GIVEN: Q-values with large magnitude (either sign) // These indicate the network is learning meaningful value estimates let test_cases = vec![ 10.5, // Large positive -43.32, // Large negative (Wave 13 false positive) 0.5, // Medium positive -2.1, // Medium negative 0.01, // Exactly at threshold (positive) - should be valid -0.01, // Exactly at threshold (negative) - should be valid 100.0, // Very large positive -100.0, // Very large negative ]; for avg_q_value in test_cases { // WHEN: Constraint check is performed let is_collapsed = is_q_value_collapsed(avg_q_value); // THEN: Should NOT be flagged (magnitude >= 0.01) assert!( !is_collapsed, "Q-value {} (|q| = {:.4} >= 0.01) should be VALID", avg_q_value, avg_q_value.abs() ); } } #[test] fn test_boundary_conditions() { // GIVEN: Values exactly at the 0.01 threshold let valid_cases = vec![0.01, -0.01]; // Should be valid (|q| >= 0.01) let invalid_cases = vec![0.009, -0.009]; // Should be invalid (|q| < 0.01) // WHEN/THEN: Valid cases should NOT be flagged for avg_q_value in valid_cases { assert!( !is_q_value_collapsed(avg_q_value), "Q-value {} (|q| = {:.4}) at threshold should be VALID", avg_q_value, avg_q_value.abs() ); } // WHEN/THEN: Invalid cases SHOULD be flagged for avg_q_value in invalid_cases { assert!( is_q_value_collapsed(avg_q_value), "Q-value {} (|q| = {:.4}) below threshold should be INVALID", avg_q_value, avg_q_value.abs() ); } } }