Files
foxhunt/ml/tests/cash_insufficiency_check_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

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
);
}
}