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

161 lines
5.4 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.
use ml::dqn::portfolio_tracker::PortfolioTracker;
use ml::dqn::action_space::{FactoredAction, ExposureLevel, OrderType, Urgency};
#[test]
fn test_position_size_respects_max_limit() {
let mut tracker = PortfolioTracker::new(100_000.0, 0.0001, 1.0);
// Try to build massive 10,000 contract position (Wave 16R bug)
for i in 0..100 {
let long_action = FactoredAction::new(
ExposureLevel::Long100, // Maximum long exposure
OrderType::Market,
Urgency::Aggressive
);
tracker.execute_action(
long_action,
5000.0, // ES futures price
200.0 // Max position parameter (should be enforced)
);
// Debug: Check position after each iteration
if i % 20 == 0 {
let features = tracker.get_raw_portfolio_features(5000.0);
println!("Iteration {}: position = {:.1}, portfolio = ${:.0}",
i, features[1], features[0]);
}
}
// CRITICAL: Position must be clamped to max_position limit
let features = tracker.get_raw_portfolio_features(5000.0);
let final_position = features[1]; // Position size from features
println!("FINAL: position = {:.1}, portfolio = ${:.0}", final_position, features[0]);
assert!(
final_position.abs() <= 200.0,
"Position size {} exceeds max_position limit 200! \
Bug #15 (unbounded position sizing) still exists!",
final_position
);
}
#[test]
fn test_position_prevents_portfolio_explosion() {
let mut tracker = PortfolioTracker::new(100_000.0, 0.0001, 1.0);
let initial_capital = 100_000.0;
// Build position to limit
for _ in 0..50 {
let action = FactoredAction::new(
ExposureLevel::Long100,
OrderType::Market,
Urgency::Aggressive
);
tracker.execute_action(action, 5000.0, 100.0);
}
let features = tracker.get_raw_portfolio_features(5000.0);
let portfolio_value = features[0];
let normalized_value = portfolio_value / initial_capital;
// Portfolio should stay in realistic range (not $50M)
assert!(
portfolio_value < 500_000.0,
"Portfolio value ${} is catastrophic! Expected <$500K. \
Position explosion indicates Bug #15 persists.",
portfolio_value
);
assert!(
normalized_value < 5.0,
"Normalized value {} is catastrophic! Expected <5.0. \
This will cause reward explosion and gradient collapse.",
normalized_value
);
}
#[test]
fn test_negative_position_also_clamped() {
let mut tracker = PortfolioTracker::new(100_000.0, 0.0001, 1.0);
// Try to build massive short position
for _ in 0..100 {
let short_action = FactoredAction::new(
ExposureLevel::Short100, // Maximum short exposure
OrderType::Market,
Urgency::Aggressive
);
tracker.execute_action(
short_action,
5000.0, // ES futures price
150.0 // Max position parameter
);
}
let features = tracker.get_raw_portfolio_features(5000.0);
let final_position = features[1];
assert!(
final_position >= -150.0,
"Short position {} exceeds max_position limit -150! \
Position clipping should work both ways.",
final_position
);
}
#[test]
fn test_low_price_creates_catastrophic_positions() {
let mut tracker = PortfolioTracker::new(100_000.0, 0.0001, 1.0);
// CRITICAL: Simulate low price scenario (e.g., penny stock or data bug)
// At price=$10, max_position = $100K / $10 = 10,000 contracts!
let low_price = 10.0;
let max_position_uncapped = 100_000.0 / low_price; // = 10,000 contracts
println!("Testing low price scenario: price=${:.2}, max_position={:.1}",
low_price, max_position_uncapped);
// Execute Long100 action with uncapped max_position
let long_action = FactoredAction::new(
ExposureLevel::Long100,
OrderType::Market,
Urgency::Aggressive
);
tracker.execute_action(long_action, low_price, max_position_uncapped);
let features = tracker.get_raw_portfolio_features(low_price);
let final_position = features[1];
let portfolio_value = features[0];
println!("After Long100: position={:.1} contracts, portfolio=${:.0}",
final_position, portfolio_value);
// THIS IS THE BUG: Position can be 10,000 contracts!
// At $10/contract, that's $100K exposure (OK)
// But if price moves to $5000 (ES futures), portfolio = 10,000 × $5000 = $50M (CATASTROPHIC)
// Demonstrate the explosion when price changes
let es_price = 5000.0;
let catastrophic_portfolio = tracker.get_raw_portfolio_features(es_price)[0];
println!("Price moves to ${:.2}: portfolio=${:.0} (CATASTROPHIC!)",
es_price, catastrophic_portfolio);
assert!(
final_position.abs() <= 200.0,
"Position size {} is catastrophic at low price! \
Should be clamped to reasonable limit (200 contracts), not based on price. \
Bug #15: max_position = capital/price creates unbounded positions at low prices!",
final_position
);
// Verify portfolio stays under $2M even at high prices (200 contracts × $5K = $1M position)
assert!(
catastrophic_portfolio < 2_000_000.0,
"Portfolio value ${:.0} is still catastrophic! Expected <$2M after position clamping.",
catastrophic_portfolio
);
}