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

348 lines
16 KiB
Rust

// Wave 16Q Agent Q3: P&L Regression Test Suite
// Mission: Test-driven validation of $621T P&L bug fix
use ml::trainers::dqn::DQNTrainer;
use std::path::PathBuf;
/// Helper: Create DQN trainer with minimal config for testing
fn create_test_dqn_trainer() -> anyhow::Result<DQNTrainer> {
let data_dir = PathBuf::from("test_data/real/databento/ml_training");
DQNTrainer::new(
128, // input_dim (128 features)
45, // num_actions (45-action factored space)
0.9626, // gamma
0.0001, // learning_rate
32, // batch_size
data_dir,
"time", // bar_sampling_method
0.01, // bar_sampling_threshold
104346, // buffer_size
500, // min_replay_size
None, // parquet_file
)
}
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// TEST 1: P&L IN REALISTIC RANGE
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
#[test]
#[ignore] // Run with: cargo test --package ml --test wave16q_pnl_regression_test --features cuda -- --ignored
fn test_pnl_in_realistic_range() -> anyhow::Result<()> {
println!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!("TEST 1: P&L IN REALISTIC RANGE");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
let mut trainer = create_test_dqn_trainer()?;
println!("🔄 Training for 1 epoch...");
trainer.train(1)?;
// Get P&L metrics from validation data
let val_data = trainer.get_val_data();
let pnl_metrics = trainer.compute_pnl_metrics(&val_data)?;
let total_pnl = pnl_metrics.total_pnl;
println!("📊 Total P&L: ${:.2}", total_pnl);
// CRITICAL ASSERTION: P&L must be within ±$50,000
// Wave 16O bug: $656,409,985,286,144.00 (656 TRILLION)
// Expected: ±$50,000 for realistic futures trading
assert!(
total_pnl.abs() < 50_000.0,
"\n❌ REGRESSION: P&L ${:.2} (${:.2}T) is catastrophic!\n\
Expected: ±$50,000\n\
Actual: ${:.2}\n\
This indicates the Wave 16O bug ($621T P&L) has returned.\n\
Root cause: target[2] contains z-scores instead of raw prices.\n\
Fix: Store raw prices separately before feature extraction.",
total_pnl,
total_pnl / 1_000_000_000_000.0,
total_pnl
);
println!("✅ TEST 1 PASSED: P&L ${:.2} is within realistic range (±$50K)", total_pnl);
Ok(())
}
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// TEST 2: TRANSACTION COSTS REALISTIC
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
#[test]
#[ignore]
fn test_transaction_costs_realistic() -> anyhow::Result<()> {
println!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!("TEST 2: TRANSACTION COSTS REALISTIC");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
let mut trainer = create_test_dqn_trainer()?;
println!("🔄 Training for 1 epoch...");
trainer.train(1)?;
// Get transaction cost metrics
let val_data = trainer.get_val_data();
let pnl_metrics = trainer.compute_pnl_metrics(&val_data)?;
let net_costs = pnl_metrics.total_fees - pnl_metrics.total_rebates;
println!("💰 Transaction Costs (Net): ${:.2}", net_costs);
println!(" • Total Fees: ${:.2}", pnl_metrics.total_fees);
println!(" • Total Rebates: ${:.2}", pnl_metrics.total_rebates);
// CRITICAL ASSERTION: Transaction costs must be <$5,000
// Wave 16O bug: $704,866,354,900.29 (704 BILLION)
// Expected: <$5,000 for reasonable trading activity
assert!(
net_costs.abs() < 5_000.0,
"\n❌ REGRESSION: Transaction costs ${:.2} (${:.2}B) are catastrophic!\n\
Expected: <$5,000\n\
Actual: ${:.2}\n\
This indicates max_position is inflated (~100M contracts instead of 10-50).\n\
Root cause: price_f32 contains z-scores (1.107) instead of raw prices ($5400).\n\
Fix: Use raw prices from target[2] for position sizing.",
net_costs,
net_costs / 1_000_000_000.0,
net_costs
);
println!("✅ TEST 2 PASSED: Transaction costs ${:.2} are realistic (<$5K)", net_costs);
Ok(())
}
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// TEST 3: MAX POSITION SANITY CHECK
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
#[test]
#[ignore]
fn test_max_position_sanity() -> anyhow::Result<()> {
println!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!("TEST 3: MAX POSITION SANITY CHECK");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
let mut trainer = create_test_dqn_trainer()?;
println!("🔄 Training for 1 epoch (collecting position data)...");
trainer.train(1)?;
// Get validation data to inspect position sizes
let val_data = trainer.get_val_data();
// Calculate max position for a typical ES futures price ($5000)
let typical_price = 5000.0;
let initial_capital = 100_000.0;
let expected_max_position = initial_capital / typical_price; // ~20 contracts
println!("📊 Position Sizing Analysis:");
println!(" • Initial Capital: ${:.2}", initial_capital);
println!(" • Typical ES Price: ${:.2}", typical_price);
println!(" • Expected Max Position: {:.2} contracts", expected_max_position);
// Inspect first 10 states to find actual position sizes
let mut max_position_observed = 0.0f32;
for (i, state) in val_data.states.iter().take(10).enumerate() {
// Extract price from state (feature index depends on implementation)
// For now, we'll use the target values which should contain raw prices
if let Some(target) = val_data.targets.get(i) {
let price = target[2]; // Should be raw price (not z-score)
let max_pos = if price > 100.0 {
initial_capital as f32 / price
} else {
// If price is <100, it's likely a z-score (BUG!)
100_000_000.0 // Huge value to trigger assertion failure
};
if i < 3 {
println!(" • Sample {}: price={:.2}, max_position={:.2}", i, price, max_pos);
}
max_position_observed = max_position_observed.max(max_pos);
}
}
println!("📏 Max Position Observed: {:.2} contracts", max_position_observed);
// CRITICAL ASSERTION: Max position must be between 10-100 contracts
// Wave 16O bug: 90,325 contracts (using z-score 1.107 instead of price $5400)
// Expected: 10-100 contracts for $100K capital trading ES futures
assert!(
max_position_observed > 10.0 && max_position_observed < 100.0,
"\n❌ REGRESSION: max_position={:.0} is insane!\n\
Expected: 10-100 contracts\n\
Actual: {:.0} contracts\n\
This indicates:\n\
- If >1000: Using z-scores instead of raw prices (Wave 16O bug)\n\
- If <10: Over-conservative position sizing\n\
Root cause: target[2] should contain raw price ($5400) but contains z-score (1.107)",
max_position_observed,
max_position_observed
);
println!("✅ TEST 3 PASSED: Max position {:.2} is realistic (10-100 contracts)", max_position_observed);
Ok(())
}
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// TEST 4: RETURN PERCENTAGE REALISTIC
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
#[test]
#[ignore]
fn test_return_percentage_realistic() -> anyhow::Result<()> {
println!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!("TEST 4: RETURN PERCENTAGE REALISTIC");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
let mut trainer = create_test_dqn_trainer()?;
println!("🔄 Training for 1 epoch...");
trainer.train(1)?;
// Get P&L metrics
let val_data = trainer.get_val_data();
let pnl_metrics = trainer.compute_pnl_metrics(&val_data)?;
let return_pct = pnl_metrics.total_return * 100.0;
println!("📈 Total Return: {:.2}%", return_pct);
// CRITICAL ASSERTION: Return must be within ±10%
// Wave 16O bug: ±100,000% (656T P&L on $100K capital)
// Expected: ±10% for 1-epoch training on validation data
assert!(
return_pct.abs() < 10.0,
"\n❌ REGRESSION: Return {:.2}% is absurd!\n\
Expected: ±10%\n\
Actual: {:.2}%\n\
This indicates P&L calculation is broken (likely $621T bug regression).\n\
Root cause: Inflated P&L due to 100M contract position sizes.",
return_pct,
return_pct
);
println!("✅ TEST 4 PASSED: Return {:.2}% is realistic (±10%)", return_pct);
Ok(())
}
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// TEST 5: INTEGRATION TEST (END-TO-END VALIDATION)
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
#[test]
#[ignore]
fn test_wave16q_complete_fix_validation() -> anyhow::Result<()> {
println!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!("TEST 5: WAVE 16Q COMPLETE FIX VALIDATION (INTEGRATION)");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
let mut trainer = create_test_dqn_trainer()?;
println!("🔄 Running full 1-epoch training...");
trainer.train(1)?;
// Get all metrics in one pass
let val_data = trainer.get_val_data();
let pnl_metrics = trainer.compute_pnl_metrics(&val_data)?;
let pnl = pnl_metrics.total_pnl;
let costs = pnl_metrics.total_fees - pnl_metrics.total_rebates;
let return_pct = pnl_metrics.total_return * 100.0;
println!("\n📊 COMPREHENSIVE METRICS SUMMARY:");
println!(" • Total P&L: ${:.2}", pnl);
println!(" • Transaction Costs (Net): ${:.2}", costs);
println!(" • Total Return: {:.2}%", return_pct);
// ALL METRICS MUST BE SANE
let mut all_pass = true;
// Check P&L
if pnl.abs() >= 50_000.0 {
println!(" ❌ P&L FAIL: ${:.2} (expected ±$50K)", pnl);
all_pass = false;
} else {
println!(" ✅ P&L PASS: ${:.2} within range", pnl);
}
// Check transaction costs
if costs.abs() >= 5_000.0 {
println!(" ❌ COSTS FAIL: ${:.2} (expected <$5K)", costs);
all_pass = false;
} else {
println!(" ✅ COSTS PASS: ${:.2} within range", costs);
}
// Check return percentage
if return_pct.abs() >= 10.0 {
println!(" ❌ RETURN FAIL: {:.2}% (expected ±10%)", return_pct);
all_pass = false;
} else {
println!(" ✅ RETURN PASS: {:.2}% within range", return_pct);
}
assert!(
all_pass,
"\n❌ INTEGRATION TEST FAILED: One or more metrics out of range.\n\
This indicates the Wave 16Q fix is incomplete or incorrect.\n\
Review Agent Q2 implementation and P1 root cause analysis."
);
println!("\n✅ TEST 5 PASSED: ALL METRICS WITHIN REALISTIC RANGES");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
Ok(())
}
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// BONUS TEST: RAW PRICE VERIFICATION (DIRECT DATA STRUCTURE CHECK)
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
#[test]
#[ignore]
fn test_target_contains_raw_prices_not_zscores() -> anyhow::Result<()> {
println!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!("BONUS TEST: TARGET[2] CONTAINS RAW PRICES (NOT Z-SCORES)");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
let mut trainer = create_test_dqn_trainer()?;
println!("🔄 Training for 1 epoch...");
trainer.train(1)?;
// Get validation data to inspect target structure
let val_data = trainer.get_val_data();
println!("🔍 Inspecting first 10 samples:");
let mut all_valid = true;
for (i, target) in val_data.targets.iter().take(10).enumerate() {
let preprocessed_price = target[0];
let raw_price = target[2];
println!(" Sample {}: target[0]={:.6}, target[2]={:.6}",
i, preprocessed_price, raw_price);
// Check if target[2] looks like a raw price (ES futures: $1000-$10000)
// vs z-score (typically -3 to +3)
if raw_price < 100.0 {
println!(" ⚠️ WARNING: target[2]={:.6} looks like z-score (expected >$1000)", raw_price);
all_valid = false;
}
}
// CRITICAL ASSERTION: target[2] must contain raw prices (>$1000 for ES futures)
// Wave 16O bug: target[2] = 1.107100 (z-score, not price)
assert!(
all_valid,
"\n❌ REGRESSION: target[2] contains z-scores instead of raw prices!\n\
Expected: target[2] = $5000-$6000 (ES futures)\n\
Actual: target[2] = 1.107 (z-score)\n\
This is the ROOT CAUSE of the $621T P&L bug.\n\
Fix: Store raw prices separately BEFORE feature extraction (Agent P1 recommendation)."
);
println!("✅ BONUS TEST PASSED: target[2] contains raw prices (not z-scores)");
Ok(())
}