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)
348 lines
16 KiB
Rust
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(())
|
|
}
|