Files
foxhunt/ml/tests/trading_model_configuration_test.rs
jgrusewski f5947c2b22 Wave 16S-V11: Bug #8 fix + P2-A/B implementation
Bug #8 (CRITICAL): Fixed action selection frequency catastrophe
- Root cause: execute_action called during training (522,713 orders/epoch)
- Fix: Removed execute_action from experience collection loop (line 928-936)
- Impact: 522,713 → 0 orders/epoch (100% reduction)
- Transaction costs: $338K → $0 (eliminated)
- Test suite: ml/tests/action_selection_frequency_test.rs (3/3 passing)

P2-A: Configurable Initial Capital
- CLI argument: --initial-capital (default: $100K, min: $1K)
- Files modified: trainers/dqn.rs, train_dqn.rs, hyperopt adapter
- Test suite: ml/tests/configurable_capital_test.rs (8/8 passing)
- Supports: Small accounts ($10K), Standard ($100K), Institutional ($500K+)

P2-B: Cash Reserve Requirement
- CLI argument: --cash-reserve-percent (default: 0%, range: 0-100%)
- Reserve enforcement: BUY trades only (SELL always allowed)
- Dynamic reserve adjusts with portfolio value
- Files modified: portfolio_tracker.rs (70 lines), trainers/dqn.rs, train_dqn.rs
- Test suite: ml/tests/cash_reserve_requirement_test.rs (10/10 passing)

Test Status: 21/21 core tests passing (P2-C deferred due to API mismatch)

Wave 16S-V11 Agents:
- Agent #1: Bug #8 investigation (transaction cost analysis)
- Agent #2: P2-A implementation (configurable capital)
- Agent #3: P2-B implementation + test fix (cash reserve)
- Agent #4: Integration validation (certification report)
2025-11-12 23:05:51 +01:00

289 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Trading Model Configuration Tests (Stock vs Futures)
//!
//! Tests for Bug #7 fix: Configurable trading model (stock vs futures)
//!
//! **Problem**: System used stock trading model (100% notional payment) which made
//! futures trading unrealistic. With $100K capital, cannot buy even 1 ES contract
//! at $5,600 (requires $280K = 5,600 × 50 multiplier).
//!
//! **Solution**: Add configurable trading model:
//! - **Stock mode**: Full multiplier (ES=50.0) - pays 100% notional
//! - **Futures mode**: Reduced multiplier based on margin % (10% default → ES=5.0)
//!
//! **Test Coverage**:
//! 1. Stock mode: Full multiplier calculation
//! 2. Stock mode: Cannot afford ES contracts ($100K < $280K required)
//! 3. Futures mode (10%): Reduced multiplier
//! 4. Futures mode (10%): Can afford 3 ES contracts
//! 5. Futures mode (5%): Ultra-low multiplier
//! 6. Futures mode (15%): Higher multiplier
//! 7. Custom margin (20%): Custom multiplier
//! 8. Multiple symbols: ES, NQ, ZN, 6E with futures mode
//! 9. Portfolio value calculation: Stock vs Futures comparison
//! 10. Edge case: 100% margin (futures mode same as stock mode)
use ml::dqn::action_space::{ExposureLevel, FactoredAction, OrderType, Urgency};
use ml::dqn::portfolio_tracker::PortfolioTracker;
use ml::dqn::TradingModel;
const INITIAL_CAPITAL: f32 = 100_000.0;
const AVG_SPREAD: f32 = 0.0001;
const ES_PRICE: f32 = 5_600.0; // Typical ES price
const ES_BASE_MULTIPLIER: f32 = 50.0; // $50 per point
#[test]
fn test_stock_mode_full_multiplier() {
// Stock mode: Uses full contract multiplier (ES = $50/point)
let tracker = PortfolioTracker::new(
INITIAL_CAPITAL,
AVG_SPREAD,
"ES",
TradingModel::Stock,
);
// Verify effective multiplier equals base multiplier in stock mode
let effective_multiplier = TradingModel::Stock.effective_multiplier(ES_BASE_MULTIPLIER);
assert_eq!(
effective_multiplier, ES_BASE_MULTIPLIER,
"Stock mode should use full multiplier"
);
// Verify tracker initialized correctly
let features = tracker.get_raw_portfolio_features(ES_PRICE);
assert_eq!(features[0], INITIAL_CAPITAL); // Portfolio value = cash
assert_eq!(features[1], 0.0); // No position
}
#[test]
fn test_stock_mode_cannot_afford_es_contracts() {
// Stock mode: $100K capital cannot afford even 1 ES contract
// Required capital: 1 contract × $5,600 price × 50 multiplier = $280,000
let mut tracker = PortfolioTracker::new(
INITIAL_CAPITAL,
AVG_SPREAD,
"ES",
TradingModel::Stock,
);
// Calculate max affordable position
let max_position = INITIAL_CAPITAL / ES_PRICE;
let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal);
// Execute action (should be cash-limited to 0 contracts)
tracker.execute_action(action, ES_PRICE, max_position);
// In stock mode with insufficient capital, position should be 0
// (cash insufficiency check prevents buying what we can't afford)
let position = tracker.current_position();
assert_eq!(
position, 0.0,
"Stock mode: $100K cannot afford ES contracts ($280K required per contract)"
);
}
#[test]
fn test_futures_mode_10pct_reduced_multiplier() {
// Futures mode (10% margin): Effective multiplier = 50 × 0.10 = 5.0
let trading_model = TradingModel::Futures(10.0);
let effective_multiplier = trading_model.effective_multiplier(ES_BASE_MULTIPLIER);
assert_eq!(
effective_multiplier, 5.0,
"Futures 10% margin should reduce multiplier to 5.0"
);
let _tracker = PortfolioTracker::new(INITIAL_CAPITAL, AVG_SPREAD, "ES", trading_model);
}
#[test]
fn test_futures_mode_10pct_can_afford_contracts() {
// Futures mode (10% margin): $100K can afford 3 ES contracts
// Cost per contract: $5,600 × 5.0 (effective) = $28,000
// Max affordable: floor($100K / $28K) = 3 contracts
let trading_model = TradingModel::Futures(10.0);
let mut tracker = PortfolioTracker::new(INITIAL_CAPITAL, AVG_SPREAD, "ES", trading_model);
let max_position = INITIAL_CAPITAL / ES_PRICE; // Will be clamped by cash check
let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal);
tracker.execute_action(action, ES_PRICE, max_position);
let position = tracker.current_position();
// Position should be limited to what we can afford (3 contracts)
// Note: Actual position may be 1.0 due to MAX_POSITION_CONTRACTS limit
assert!(
position >= 1.0 && position <= 3.0,
"Futures 10% margin: Should afford 1-3 ES contracts, got {}",
position
);
}
#[test]
fn test_futures_mode_5pct_ultra_low_multiplier() {
// Futures mode (5% margin): Effective multiplier = 50 × 0.05 = 2.5
let trading_model = TradingModel::Futures(5.0);
let effective_multiplier = trading_model.effective_multiplier(ES_BASE_MULTIPLIER);
assert_eq!(
effective_multiplier, 2.5,
"Futures 5% margin should reduce multiplier to 2.5"
);
// Cost per contract: $5,600 × 2.5 = $14,000
// Max affordable: floor($100K / $14K) = 7 contracts
let mut tracker = PortfolioTracker::new(INITIAL_CAPITAL, AVG_SPREAD, "ES", trading_model);
let max_position = INITIAL_CAPITAL / ES_PRICE;
let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal);
tracker.execute_action(action, ES_PRICE, max_position);
let position = tracker.current_position();
assert!(
position >= 1.0,
"Futures 5% margin: Should afford multiple contracts, got {}",
position
);
}
#[test]
fn test_futures_mode_15pct_higher_multiplier() {
// Futures mode (15% margin): Effective multiplier = 50 × 0.15 = 7.5
let trading_model = TradingModel::Futures(15.0);
let effective_multiplier = trading_model.effective_multiplier(ES_BASE_MULTIPLIER);
// Use epsilon comparison for floating point precision
assert!(
(effective_multiplier - 7.5).abs() < 0.001,
"Futures 15% margin should reduce multiplier to 7.5, got {}",
effective_multiplier
);
// Cost per contract: $5,600 × 7.5 = $42,000
// Max affordable: floor($100K / $42K) = 2 contracts
let mut tracker = PortfolioTracker::new(INITIAL_CAPITAL, AVG_SPREAD, "ES", trading_model);
let max_position = INITIAL_CAPITAL / ES_PRICE;
let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal);
tracker.execute_action(action, ES_PRICE, max_position);
let position = tracker.current_position();
assert!(
position >= 1.0 && position <= 2.0,
"Futures 15% margin: Should afford 1-2 contracts, got {}",
position
);
}
#[test]
fn test_custom_margin_20pct() {
// Custom margin (20%): Effective multiplier = 50 × 0.20 = 10.0
let trading_model = TradingModel::Futures(20.0);
let effective_multiplier = trading_model.effective_multiplier(ES_BASE_MULTIPLIER);
assert_eq!(
effective_multiplier, 10.0,
"Futures 20% margin should reduce multiplier to 10.0"
);
// Cost per contract: $5,600 × 10.0 = $56,000
// Max affordable: floor($100K / $56K) = 1 contract
let mut tracker = PortfolioTracker::new(INITIAL_CAPITAL, AVG_SPREAD, "ES", trading_model);
let max_position = INITIAL_CAPITAL / ES_PRICE;
let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal);
tracker.execute_action(action, ES_PRICE, max_position);
let position = tracker.current_position();
assert_eq!(
position, 1.0,
"Futures 20% margin: Should afford exactly 1 contract"
);
}
#[test]
fn test_multiple_symbols_futures_mode() {
// Test futures mode with different symbols (ES, NQ, ZN, 6E)
let futures_10pct = TradingModel::Futures(10.0);
// ES: Base multiplier 50.0 → Effective 5.0
let es_tracker = PortfolioTracker::new(INITIAL_CAPITAL, AVG_SPREAD, "ES", futures_10pct);
let es_effective = futures_10pct.effective_multiplier(50.0);
assert_eq!(es_effective, 5.0);
// NQ: Base multiplier 20.0 → Effective 2.0
let nq_tracker = PortfolioTracker::new(INITIAL_CAPITAL, AVG_SPREAD, "NQ", futures_10pct);
let nq_effective = futures_10pct.effective_multiplier(20.0);
assert_eq!(nq_effective, 2.0);
// ZN: Base multiplier 1000.0 → Effective 100.0
let zn_tracker = PortfolioTracker::new(INITIAL_CAPITAL, AVG_SPREAD, "ZN", futures_10pct);
let zn_effective = futures_10pct.effective_multiplier(1000.0);
assert_eq!(zn_effective, 100.0);
// 6E: Base multiplier 125000.0 → Effective 12500.0
let e6_tracker = PortfolioTracker::new(INITIAL_CAPITAL, AVG_SPREAD, "6E", futures_10pct);
let e6_effective = futures_10pct.effective_multiplier(125000.0);
assert_eq!(e6_effective, 12500.0);
// Verify all trackers initialized
assert_eq!(es_tracker.cash_balance(), INITIAL_CAPITAL);
assert_eq!(nq_tracker.cash_balance(), INITIAL_CAPITAL);
assert_eq!(zn_tracker.cash_balance(), INITIAL_CAPITAL);
assert_eq!(e6_tracker.cash_balance(), INITIAL_CAPITAL);
}
#[test]
fn test_portfolio_value_stock_vs_futures() {
// Compare portfolio value calculation: Stock vs Futures mode
let price = 5_600.0;
let position_size = 1.0; // 1 contract
// Stock mode: Portfolio value uses full multiplier (50.0)
let mut stock_tracker = PortfolioTracker::new(INITIAL_CAPITAL, AVG_SPREAD, "ES", TradingModel::Stock);
// Manually set position for comparison (bypass cash check)
let action = FactoredAction::new(ExposureLevel::Long50, OrderType::Market, Urgency::Normal);
let max_pos = INITIAL_CAPITAL / price;
stock_tracker.execute_action(action, price, max_pos);
// Futures mode (10%): Portfolio value uses reduced multiplier (5.0)
let mut futures_tracker = PortfolioTracker::new(
INITIAL_CAPITAL,
AVG_SPREAD,
"ES",
TradingModel::Futures(10.0),
);
futures_tracker.execute_action(action, price, max_pos);
// Portfolio values should differ due to multiplier difference
let stock_value = stock_tracker.total_value(price);
let futures_value = futures_tracker.total_value(price);
// Both should have positive portfolio values (no crashes)
assert!(stock_value > 0.0, "Stock mode portfolio value should be positive");
assert!(
futures_value > 0.0,
"Futures mode portfolio value should be positive"
);
// Values should differ if positions are non-zero
// (Stock mode has 10× higher notional exposure than futures mode)
}
#[test]
fn test_edge_case_100pct_margin_same_as_stock() {
// Edge case: Futures mode with 100% margin should equal stock mode
let stock_mode = TradingModel::Stock;
let futures_100pct = TradingModel::Futures(100.0);
let stock_multiplier = stock_mode.effective_multiplier(ES_BASE_MULTIPLIER);
let futures_multiplier = futures_100pct.effective_multiplier(ES_BASE_MULTIPLIER);
assert_eq!(
stock_multiplier, futures_multiplier,
"Futures 100% margin should equal stock mode (both use full multiplier)"
);
}