- Reduce CI GPU test datasets 16x for walltime reduction - Reduce early-stop epochs 50→10, add --test-threads=1 - Serialize all GPU lib tests to prevent cuBLAS init race - Align state_dim to 16 for BF16 tensor core HMMA dispatch - BF16 precision tolerance in ml-dqn tests - Enable branching DQN + tracing subscriber in smoke tests - Prevent min_replay_size > buffer_size deadlock in early-stop tests - Prevent AutoReplaySizer from breaking gradient collapse warmup - Replace racy tokio::spawn checkpoint counter with AtomicUsize - Set warmup_steps=0 and max_training_steps_per_epoch=300 in early-stop tests - RealDataLoader respects TEST_DATA_DIR for CI PVC layout - Add collapse_warmup_capacity to gpu_smoketest DQNConfig - Drain CUDA context between test binaries - Detached HEAD checkout prevents local branch corruption - GPU pipeline tests: fix BF16 dtype and rank-1 squeeze assertions - OOD input handling tests use use_gpu: true Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
251 lines
8.7 KiB
Rust
251 lines
8.7 KiB
Rust
#![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,
|
||
)]
|
||
//! Bug #2: Transaction Cost Weight Fix - Regression Tests
|
||
//!
|
||
//! **BUG DESCRIPTION**:
|
||
//! Transaction costs were hardcoded to 0.05 instead of using proper market rates.
|
||
//! This caused the agent to undertrain on transaction cost awareness.
|
||
//!
|
||
//! **ROOT CAUSE**:
|
||
//! File: `/ml/src/dqn/reward.rs:153` (Default impl) and line 221 (Builder)
|
||
//! ```rust
|
||
//! cost_weight: Decimal::try_from(0.05).unwrap_or(Decimal::ZERO), // WAS: HARDCODED 0.05
|
||
//! ```
|
||
//!
|
||
//! **STATUS**: ✅ Bug #2 fix IMPLEMENTED
|
||
//!
|
||
//! **FIX APPLIED**:
|
||
//! ```rust
|
||
//! cost_weight: Decimal::ONE, // Apply transaction costs at full weight
|
||
//! ```
|
||
//!
|
||
//! **WHY transaction_cost_multiplier SHOULD BE REMOVED**:
|
||
//! - Transaction costs are FIXED market rates (0.05%-0.15%) from OrderType enum
|
||
//! - These are real exchange fees, not tunable hyperparameters
|
||
//! - Making them tunable allows the agent to "cheat" by learning with fake low costs
|
||
//! - Real trading must use actual market fees
|
||
//!
|
||
//! **CURRENT STATE**:
|
||
//! - cost_weight is hardcoded to 0.05 (line 982)
|
||
//! - transaction_cost_multiplier is still in hyperopt search space (dimension #8)
|
||
//! - These tests verify current behavior, not fixed behavior
|
||
|
||
use ml::dqn::action_space::{ExposureLevel, FactoredAction, OrderType, Urgency};
|
||
use ml::dqn::reward::RewardConfig;
|
||
use ml::hyperopt::adapters::dqn::DQNParams;
|
||
use ml::hyperopt::ParameterSpace;
|
||
use rust_decimal::Decimal;
|
||
use tracing::info;
|
||
|
||
/// Test that cost_weight is now 1.0 (full weight) - verifies Bug #2 fix
|
||
///
|
||
/// **Before Fix**: cost_weight = 0.05 (hardcoded in reward.rs)
|
||
/// **After Fix**: cost_weight = 1.0 (full weight)
|
||
/// **Status**: ✅ Bug #2 fix IMPLEMENTED
|
||
#[test]
|
||
fn test_cost_weight_is_full_weight() {
|
||
// Create reward config using Default (should now have cost_weight = 1.0)
|
||
let reward_config = RewardConfig::default();
|
||
|
||
// Verify cost_weight is now 1.0 (full weight)
|
||
assert_eq!(
|
||
reward_config.cost_weight,
|
||
Decimal::ONE,
|
||
"Bug #2 fix: cost_weight should be 1.0 (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)
|
||
///
|
||
/// **Current**: DQNParams DOES have transaction_cost_multiplier field
|
||
/// **Expected (after fix)**: Field should be removed
|
||
/// **Status**: Bug #2 fix NOT YET IMPLEMENTED
|
||
#[test]
|
||
fn test_transaction_cost_multiplier_removed() {
|
||
// Verify field still exists in current implementation
|
||
let params = DQNParams::default();
|
||
|
||
// 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"
|
||
);
|
||
|
||
info!(tx_cost, "transaction_cost_multiplier field still exists in DQNParams; Bug #2 fix will remove this field");
|
||
}
|
||
|
||
/// Test that LimitMaker orders have lowest cost (0.05%)
|
||
///
|
||
/// **Expected**: LimitMaker cost < IoC cost < Market cost
|
||
/// **Verifies**: OrderType enum transaction costs are correct
|
||
#[test]
|
||
fn test_order_type_transaction_costs() {
|
||
let market_cost = OrderType::Market.transaction_cost();
|
||
let limit_cost = OrderType::LimitMaker.transaction_cost();
|
||
let ioc_cost = OrderType::IoC.transaction_cost();
|
||
|
||
// Verify costs are in correct order
|
||
assert!(
|
||
limit_cost < ioc_cost && ioc_cost < market_cost,
|
||
"Order type costs should be: LimitMaker < IoC < Market\n\
|
||
Got: LimitMaker={:.4}, IoC={:.4}, Market={:.4}",
|
||
limit_cost,
|
||
ioc_cost,
|
||
market_cost
|
||
);
|
||
|
||
// Verify exact values (from action_space.rs)
|
||
assert_eq!(market_cost, 0.0015, "Market orders should cost 0.15%");
|
||
assert_eq!(limit_cost, 0.0005, "LimitMaker orders should cost 0.05%");
|
||
assert_eq!(ioc_cost, 0.0010, "IoC orders should cost 0.10%");
|
||
|
||
info!(market_pct = market_cost * 100.0, limit_pct = limit_cost * 100.0, ioc_pct = ioc_cost * 100.0, "Order type transaction costs are correct");
|
||
}
|
||
|
||
/// 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
|
||
#[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",
|
||
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)"
|
||
);
|
||
|
||
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");
|
||
}
|
||
|
||
/// Test that transaction costs correctly reflect factored action order types
|
||
#[test]
|
||
fn test_factored_action_transaction_costs() {
|
||
// Market order (most expensive)
|
||
let market_action = FactoredAction::new(
|
||
ExposureLevel::Long100,
|
||
OrderType::Market,
|
||
Urgency::Aggressive,
|
||
);
|
||
|
||
// Limit maker order (cheapest)
|
||
let limit_action = FactoredAction::new(
|
||
ExposureLevel::Long100,
|
||
OrderType::LimitMaker,
|
||
Urgency::Patient,
|
||
);
|
||
|
||
// IoC order (middle)
|
||
let ioc_action = FactoredAction::new(
|
||
ExposureLevel::Long100,
|
||
OrderType::IoC,
|
||
Urgency::Normal,
|
||
);
|
||
|
||
// Verify costs are correctly propagated from OrderType
|
||
assert_eq!(market_action.transaction_cost(), 0.0015);
|
||
assert_eq!(limit_action.transaction_cost(), 0.0005);
|
||
assert_eq!(ioc_action.transaction_cost(), 0.0010);
|
||
|
||
// Verify cost calculations for $10,000 trade
|
||
let trade_value = 10_000.0;
|
||
assert_eq!(market_action.calculate_transaction_cost(trade_value), 15.0); // 0.15% × $10k
|
||
assert_eq!(limit_action.calculate_transaction_cost(trade_value), 5.0); // 0.05% × $10k
|
||
assert_eq!(ioc_action.calculate_transaction_cost(trade_value), 10.0); // 0.10% × $10k
|
||
|
||
info!("Factored actions correctly use OrderType transaction costs");
|
||
}
|