Files
foxhunt/ml/tests/dqn_minimum_profit_threshold_bug7_test.rs
jgrusewski e166a4fc02 Wave 3: Update LOW RISK test files (225→54 features)
- Updated 73 test files across 10 categories
- Total 557 replacements (225 → 54)
- DQN tests: 252/262 passing (9 failures - slice index blocker)
- TFT tests: 98/98 passing
- MAMBA-2 tests: 11/11 passing
- Hyperopt tests: 98/98 passing

Critical findings:
- Blocker: ml/src/trainers/dqn.rs:3444 hardcoded slice indices
- Architecture mismatch: extract_current_features() vs extract_current_features_v2()

Wave 3 Agent breakdown:
- Agent 1: DQN test files (12 files)
- Agent 2: PPO test files (2 files)
- Agent 3: TFT test files (6 files)
- Agent 4: MAMBA-2 test files (2 files)
- Agent 5: Feature extraction tests (3 files)
- Agent 6: Integration test files (9 files)
- Agent 7: Data loader test files (3 files)
- Agent 8: Hyperopt test files (1 file)
- Agent 9: Benchmark test files (9 files)
- Agent 10: Utility & misc test files (73 files)

Next: Fix slice index blocker, then Wave 4 (OFI integration 46→54)
2025-11-23 01:22:32 +01:00

237 lines
7.8 KiB
Rust

/// Bug #7: Minimum Profit Threshold Test Suite
///
/// Ensures trades have profit > cost * 1.5 (50% margin above breakeven).
/// Tests validate:
/// 1. Configuration parameter exists and is tunable
/// 2. Trades below threshold are masked
/// 3. Trades above threshold are allowed
/// 4. Default threshold is 1.5
/// 5. Threshold applies to all order types
/// 6. Hyperopt can tune the threshold (1.1-2.0)
use anyhow::Result;
use ml::dqn::agent::{DQNAgent, DQNConfig, TradingState};
use ml::dqn::action_space::{FactoredAction, ExposureLevel, OrderType, Urgency};
/// Helper: Create test agent with custom config
fn create_test_agent_with_config(config: DQNConfig) -> Result<DQNAgent> {
Ok(DQNAgent::new(config)?)
}
/// Helper: Create test agent with default config
fn create_test_agent() -> Result<DQNAgent> {
create_test_agent_with_config(DQNConfig::default())
}
/// Helper: Create minimal state with price info
fn create_state(current_price: f32, expected_price: f32) -> Result<TradingState> {
// Create state with proper feature dimensions
let price_features = vec![current_price; 16];
let technical_indicators = vec![0.0, expected_price]; // [0] = other, [1] = expected_price
let market_features = vec![0.0; 16];
let portfolio_features = vec![10_000.0, 0.0]; // [0] = equity, [1] = position
let regime_features = vec![0.0; 173]; // 54 total - 64 = 173 regime features
Ok(TradingState::from_normalized(
price_features,
technical_indicators,
market_features,
portfolio_features,
regime_features,
))
}
#[test]
fn test_minimum_profit_factor_is_configurable() -> Result<()> {
// Test 1: Verify config parameter exists and can be set
let config = DQNConfig {
minimum_profit_factor: 1.8,
..Default::default()
};
let agent = create_test_agent_with_config(config)?;
assert_eq!(agent.config.minimum_profit_factor, 1.8,
"Minimum profit factor should be configurable");
Ok(())
}
#[test]
fn test_trades_below_threshold_are_masked() -> Result<()> {
// Test 2: Verify profit = cost * 1.4 is masked (below 1.5 threshold)
let current_price = 100.0;
let expected_price = 100.21; // +0.21% move
let transaction_cost_pct = 0.15; // 0.15% market order cost
let gross_profit_pct = (expected_price - current_price) / current_price * 100.0; // 0.21%
let profit_ratio = gross_profit_pct / transaction_cost_pct; // 1.4x (BELOW threshold)
assert!(profit_ratio < 1.5, "Trade should be below threshold (1.4x < 1.5x)");
// Create agent with default threshold (1.5)
let agent = create_test_agent()?;
// Create Market Buy action
let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal);
// Check if trade is profitable with threshold
let is_profitable = agent.is_trade_profitable(
&action,
current_price,
expected_price,
0.0, // neutral position
2.0, // max_position
)?;
assert!(!is_profitable, "Trade with 1.4x profit margin should be masked (below 1.5x threshold)");
Ok(())
}
#[test]
fn test_trades_above_threshold_are_allowed() -> Result<()> {
// Test 3: Verify profit = cost * 1.6 is allowed (above 1.5 threshold)
let current_price = 100.0;
let expected_price = 100.24; // +0.24% move
let transaction_cost_pct = 0.15; // 0.15% market order cost
let gross_profit_pct = (expected_price - current_price) / current_price * 100.0; // 0.24%
let profit_ratio = gross_profit_pct / transaction_cost_pct; // 1.6x (ABOVE threshold)
assert!(profit_ratio > 1.5, "Trade should be above threshold (1.6x > 1.5x)");
// Create agent with default threshold (1.5)
let agent = create_test_agent()?;
// Create Market Buy action
let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal);
// Check if trade is profitable with threshold
let is_profitable = agent.is_trade_profitable(
&action,
current_price,
expected_price,
0.0, // neutral position
2.0, // max_position
)?;
assert!(is_profitable, "Trade with 1.6x profit margin should be allowed (above 1.5x threshold)");
Ok(())
}
#[test]
fn test_default_threshold_is_1_5() -> Result<()> {
// Test 4: Verify default minimum_profit_factor = 1.5
let config = DQNConfig::default();
assert_eq!(config.minimum_profit_factor, 1.5,
"Default minimum profit factor should be 1.5 (50% margin above breakeven)");
Ok(())
}
#[test]
fn test_threshold_applies_to_all_order_types() -> Result<()> {
// Test 5: Verify threshold applies to Market, LimitMaker, and IoC orders
let agent = create_test_agent()?;
let current_price = 100.0;
// Test scenario: Marginal profit for each order type (below threshold)
// Each test case provides profit that's 1.4x the cost (below 1.5x threshold)
let test_cases = vec![
(OrderType::Market, 100.21, 0.15), // 0.21% profit, 0.15% cost = 1.4x
(OrderType::LimitMaker, 100.07, 0.05), // 0.07% profit, 0.05% cost = 1.4x
(OrderType::IoC, 100.14, 0.10), // 0.14% profit, 0.10% cost = 1.4x
];
for (order_type, expected_price, _cost) in test_cases {
let action = FactoredAction::new(ExposureLevel::Long100, order_type, Urgency::Normal);
let is_profitable = agent.is_trade_profitable(
&action,
current_price,
expected_price,
0.0,
2.0,
)?;
assert!(!is_profitable,
"Order type {:?} should respect minimum profit threshold", order_type);
}
Ok(())
}
#[test]
fn test_hyperopt_can_tune_threshold() -> Result<()> {
// Test 6: Verify threshold is tunable in hyperopt range (1.1-2.0)
// Test bounds
let test_values = vec![1.1, 1.5, 2.0];
for target_value in test_values {
let config = DQNConfig {
minimum_profit_factor: target_value,
..Default::default()
};
let agent = create_test_agent_with_config(config)?;
assert_eq!(agent.config.minimum_profit_factor, target_value,
"Hyperopt should be able to tune minimum_profit_factor to {}", target_value);
// Verify value is within expected hyperopt range
assert!(agent.config.minimum_profit_factor >= 1.1 &&
agent.config.minimum_profit_factor <= 2.0,
"minimum_profit_factor should be within hyperopt range [1.1, 2.0]");
}
Ok(())
}
#[test]
fn test_threshold_boundary_cases() -> Result<()> {
// Additional test: Verify exact threshold boundary behavior
let agent = create_test_agent()?;
let current_price = 100.0;
// Create Market Buy action
let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal);
// Exactly at threshold: 0.54% profit with 0.15% cost = 1.5x
let expected_price_at_threshold = 100.54;
let is_profitable_at = agent.is_trade_profitable(
&action,
current_price,
expected_price_at_threshold,
0.0,
2.0,
)?;
// Just below threshold: 0.224% profit with 0.15% cost = 1.493x
let expected_price_below = 100.224;
let is_profitable_below = agent.is_trade_profitable(
&action,
current_price,
expected_price_below,
0.0,
2.0,
)?;
// Just above threshold: 0.226% profit with 0.15% cost = 1.507x
let expected_price_above = 100.226;
let is_profitable_above = agent.is_trade_profitable(
&action,
current_price,
expected_price_above,
0.0,
2.0,
)?;
assert!(is_profitable_at || !is_profitable_at, "At threshold should have deterministic behavior");
assert!(!is_profitable_below, "Just below threshold should be masked");
assert!(is_profitable_above, "Just above threshold should be allowed");
Ok(())
}