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)
289 lines
11 KiB
Rust
289 lines
11 KiB
Rust
//! 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)"
|
||
);
|
||
}
|