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)
183 lines
7.1 KiB
Rust
183 lines
7.1 KiB
Rust
// Wave 16S-V8 Bug #2 Fix: Cash Insufficiency Check Tests
|
|
// Tests to verify that cash sufficiency check runs for BUYING (positive delta), not SELLING (negative delta)
|
|
//
|
|
// Bug: Line 268 checks `if position_delta < 0.0` which catches SELLING, bypassing cash check for BUYING
|
|
// Fix: Change to `if position_delta > 0.0` to properly check cash when buying
|
|
|
|
use ml::dqn::portfolio_tracker::PortfolioTracker;
|
|
use ml::dqn::{FactoredAction, ExposureLevel, OrderType, Urgency};
|
|
|
|
#[cfg(test)]
|
|
mod cash_insufficiency_tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_buying_long_with_insufficient_cash_is_rejected() {
|
|
// Start with only $1,000 cash (insufficient for 1 ES contract at $5,600)
|
|
let mut tracker = PortfolioTracker::new(1_000.0, 0.0001, 1.0);
|
|
|
|
let initial_cash = tracker.cash_balance();
|
|
let initial_position = tracker.current_position();
|
|
|
|
// Try to buy 1 contract at $5,600 (needs $5,608.40 with fees)
|
|
let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal);
|
|
tracker.execute_action(action, 5600.0, 1.0);
|
|
|
|
let final_cash = tracker.cash_balance();
|
|
let final_position = tracker.current_position();
|
|
|
|
// Position should NOT change from 0.0 (insufficient cash)
|
|
assert!(
|
|
(final_position - initial_position).abs() < 0.01,
|
|
"Position should not change with insufficient cash. Initial: {:.2}, Final: {:.2}",
|
|
initial_position, final_position
|
|
);
|
|
|
|
// Cash should remain mostly unchanged (maybe small fee deducted)
|
|
assert!(
|
|
(final_cash - initial_cash).abs() < 100.0,
|
|
"Cash should remain mostly unchanged. Initial: ${:.2}, Final: ${:.2}",
|
|
initial_cash, final_cash
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_buying_long_with_sufficient_cash_succeeds() {
|
|
// Start with $100,000 cash (sufficient for 1 ES contract at $5,600)
|
|
let mut tracker = PortfolioTracker::new(100_000.0, 0.0001, 1.0);
|
|
|
|
let initial_cash = tracker.cash_balance();
|
|
|
|
// Buy 1 contract at $5,600
|
|
let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal);
|
|
tracker.execute_action(action, 5600.0, 1.0);
|
|
|
|
let final_cash = tracker.cash_balance();
|
|
let final_position = tracker.current_position();
|
|
|
|
// Position should change to 1.0 (we bought 1 contract)
|
|
assert!(
|
|
(final_position - 1.0).abs() < 0.01,
|
|
"Position should be 1.0 after buying with sufficient cash. Final: {:.2}",
|
|
final_position
|
|
);
|
|
|
|
// Cash should decrease by ~$5,608.40 (price + 0.15% market fee)
|
|
let expected_decrease = 5600.0 + (5600.0 * 0.0015);
|
|
let actual_decrease = initial_cash - final_cash;
|
|
assert!(
|
|
(actual_decrease - expected_decrease).abs() < 10.0,
|
|
"Cash decrease should be ~${:.2}. Actual: ${:.2}",
|
|
expected_decrease, actual_decrease
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_selling_short_with_zero_cash_succeeds() {
|
|
// Start with $0 cash (selling SHORT should NOT require cash check)
|
|
let mut tracker = PortfolioTracker::new(0.0, 0.0001, 1.0);
|
|
|
|
let initial_position = tracker.current_position();
|
|
|
|
// Sell 1 contract short at $5,600 (go from 0 to -1)
|
|
// This should SUCCEED even with $0 cash, because selling adds cash
|
|
let action = FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal);
|
|
tracker.execute_action(action, 5600.0, 1.0);
|
|
|
|
let final_cash = tracker.cash_balance();
|
|
let final_position = tracker.current_position();
|
|
|
|
// Position should change to -1.0 (we sold short)
|
|
assert!(
|
|
(final_position - (-1.0)).abs() < 0.01,
|
|
"Position should be -1.0 after selling short. Final: {:.2}",
|
|
final_position
|
|
);
|
|
|
|
// Cash should INCREASE even starting from $0 (selling adds cash)
|
|
assert!(
|
|
final_cash > 0.0,
|
|
"Cash should increase when selling short, even from $0. Final: ${:.2}",
|
|
final_cash
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_partial_position_when_cash_insufficient() {
|
|
// Start with exactly enough cash for 0.5 contracts ($2,804.20 for half position at $5,600)
|
|
let mut tracker = PortfolioTracker::new(2_900.0, 0.0001, 1.0);
|
|
|
|
// Try to buy 1.0 contract (will be reduced to ~0.5 due to cash limit)
|
|
let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal);
|
|
tracker.execute_action(action, 5600.0, 1.0);
|
|
|
|
let final_position = tracker.current_position();
|
|
let final_cash = tracker.cash_balance();
|
|
|
|
// Position should be between 0.4 and 0.6 (partial fill)
|
|
assert!(
|
|
final_position > 0.3 && final_position < 0.7,
|
|
"Position should be partially filled (~0.5 contracts). Final: {:.2}",
|
|
final_position
|
|
);
|
|
|
|
// Cash should be nearly depleted (less than $100 remaining)
|
|
assert!(
|
|
final_cash < 100.0,
|
|
"Cash should be nearly depleted after buying max affordable. Final: ${:.2}",
|
|
final_cash
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_cash_check_does_not_prevent_selling_from_long_position() {
|
|
// Start with $100K and buy 1 contract
|
|
let mut tracker = PortfolioTracker::new(100_000.0, 0.0001, 1.0);
|
|
let buy = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal);
|
|
tracker.execute_action(buy, 5600.0, 1.0);
|
|
|
|
let cash_after_buy = tracker.cash_balance();
|
|
|
|
// Now sell the position (go from +1 to 0), even if cash is low
|
|
// Selling should NOT be blocked by cash check
|
|
let sell = FactoredAction::new(ExposureLevel::Flat, OrderType::Market, Urgency::Normal);
|
|
tracker.execute_action(sell, 5650.0, 1.0); // Price increased
|
|
|
|
let final_position = tracker.current_position();
|
|
let final_cash = tracker.cash_balance();
|
|
|
|
// Position should be back to 0.0 (sold the contract)
|
|
assert!(
|
|
final_position.abs() < 0.01,
|
|
"Position should be 0.0 after selling. Final: {:.2}",
|
|
final_position
|
|
);
|
|
|
|
// Cash should increase (we sold at higher price)
|
|
assert!(
|
|
final_cash > cash_after_buy,
|
|
"Cash should increase after selling at profit. After buy: ${:.2}, Final: ${:.2}",
|
|
cash_after_buy, final_cash
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_zero_cash_prevents_buying() {
|
|
// Start with exactly $0 cash
|
|
let mut tracker = PortfolioTracker::new(0.0, 0.0001, 1.0);
|
|
|
|
// Try to buy 1 contract (should be rejected - no cash)
|
|
let action = FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal);
|
|
tracker.execute_action(action, 5600.0, 1.0);
|
|
|
|
let final_position = tracker.current_position();
|
|
|
|
// Position should remain 0.0 (buying rejected)
|
|
assert!(
|
|
final_position.abs() < 0.01,
|
|
"Position should remain 0.0 with no cash. Final: {:.2}",
|
|
final_position
|
|
);
|
|
}
|
|
}
|