#![allow(unexpected_cfgs)] #![cfg(feature = "__trading_service_integration")] //! Wave D Paper Trading Integration Test //! //! This test validates the integration of Wave D regime detection features into //! paper trading, enabling regime-adaptive position sizing and stop-loss adjustments. //! //! ## Test Coverage //! 1. Regime-adaptive position sizing (1.0x Normal → 1.5x Trending → 0.5x Volatile → 0.2x Crisis) //! 2. Dynamic stop-loss adjustment (2.0x ATR → 2.5x ATR → 3.0x ATR → 4.0x ATR) //! 3. Regime transition logging to database //! 4. Order submission with regime metadata //! 5. Regime feature extraction from market data //! //! ## Architecture //! - Uses real PostgreSQL for integration testing //! - Simulates market data stream with regime transitions //! - Validates position sizing calculations //! - Tests database regime tracking //! //! ## TDD RED Phase //! This test is expected to FAIL until paper trading executor is updated //! with regime awareness (Agent D33 GREEN phase). use anyhow::Result; use chrono::Utc; use sqlx::PgPool; use std::collections::HashMap; use uuid::Uuid; // Import paper trading executor types use trading_service::paper_trading_executor::{ PaperTradingConfig, PaperTradingExecutor, PendingPrediction, }; // Import Wave D regime types (will be created in GREEN phase) // use common::trading::MarketRegime; // Test database URL fn get_test_db_url() -> String { std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }) } /// Helper to create test market data with regime characteristics fn create_regime_market_data(regime: &str) -> Vec<(f64, f64, f64, f64, f64)> { // (timestamp, open, high, low, close, volume) match regime { "normal" => { // Normal market: Low volatility, small range vec![ (1000.0, 4500.0, 4502.0, 4498.0, 4501.0, 100.0), (2000.0, 4501.0, 4503.0, 4499.0, 4500.0, 110.0), (3000.0, 4500.0, 4502.0, 4498.0, 4501.0, 105.0), ] }, "trending" => { // Trending market: Strong directional movement vec![ (1000.0, 4500.0, 4520.0, 4498.0, 4518.0, 150.0), (2000.0, 4518.0, 4540.0, 4515.0, 4538.0, 160.0), (3000.0, 4538.0, 4560.0, 4535.0, 4558.0, 155.0), ] }, "volatile" => { // Volatile market: Large price swings vec![ (1000.0, 4500.0, 4550.0, 4450.0, 4480.0, 200.0), (2000.0, 4480.0, 4530.0, 4420.0, 4520.0, 220.0), (3000.0, 4520.0, 4570.0, 4460.0, 4490.0, 210.0), ] }, "crisis" => { // Crisis market: Extreme volatility, gap moves vec![ (1000.0, 4500.0, 4600.0, 4350.0, 4380.0, 300.0), (2000.0, 4380.0, 4480.0, 4250.0, 4300.0, 350.0), (3000.0, 4300.0, 4400.0, 4150.0, 4200.0, 320.0), ] }, _ => vec![], } } /// Calculate ATR (Average True Range) for stop-loss calculation fn calculate_atr(market_data: &[(f64, f64, f64, f64, f64)]) -> f64 { if market_data.is_empty() { return 20.0; // Default ATR } let mut true_ranges = Vec::new(); for window in market_data.windows(2) { let (_, _, _, _, prev_close) = window[0]; let (_, _, high, low, _) = window[1]; let tr = (high - low) .max((high - prev_close).abs()) .max((low - prev_close).abs()); true_ranges.push(tr); } if true_ranges.is_empty() { return 20.0; } true_ranges.iter().sum::() / true_ranges.len() as f64 } // ============================================================================ // TEST 1: Regime-Adaptive Position Sizing // ============================================================================ #[tokio::test] async fn test_regime_adaptive_position_sizing() { let pool = PgPool::connect(&get_test_db_url()) .await .expect("Failed to connect to test database"); // Setup: Create paper trading executor with regime awareness let config = PaperTradingConfig::default(); let executor = PaperTradingExecutor::new(pool.clone(), config); // Test Case 1: Normal regime → 1.0x base position size let normal_data = create_regime_market_data("normal"); let base_position_size = 10.0; // 10 contracts base // Calculate expected position size for Normal regime let normal_multiplier = 1.0; let expected_normal_size = base_position_size * normal_multiplier; // Simulate regime detection (will be implemented in GREEN phase) let detected_regime = "Normal"; println!( "TEST 1.1: Normal regime detected → Expected position size: {:.2} (base {} x multiplier {})", expected_normal_size, base_position_size, normal_multiplier ); // Test Case 2: Trending regime → 1.5x base position size let trending_data = create_regime_market_data("trending"); let trending_multiplier = 1.5; let expected_trending_size = base_position_size * trending_multiplier; println!( "TEST 1.2: Trending regime detected → Expected position size: {:.2} (base {} x multiplier {})", expected_trending_size, base_position_size, trending_multiplier ); // Test Case 3: Volatile regime → 0.5x base position size let volatile_data = create_regime_market_data("volatile"); let volatile_multiplier = 0.5; let expected_volatile_size = base_position_size * volatile_multiplier; println!( "TEST 1.3: Volatile regime detected → Expected position size: {:.2} (base {} x multiplier {})", expected_volatile_size, base_position_size, volatile_multiplier ); // Test Case 4: Crisis regime → 0.2x base position size let crisis_data = create_regime_market_data("crisis"); let crisis_multiplier = 0.2; let expected_crisis_size = base_position_size * crisis_multiplier; println!( "TEST 1.4: Crisis regime detected → Expected position size: {:.2} (base {} x multiplier {})", expected_crisis_size, base_position_size, crisis_multiplier ); // RED PHASE: Expected to fail - executor does not yet implement regime-aware position sizing // GREEN PHASE: Will implement calculate_regime_adjusted_position_size() method println!("✗ RED: test_regime_adaptive_position_sizing - Not yet implemented"); } // ============================================================================ // TEST 2: Dynamic Stop-Loss Adjustment // ============================================================================ #[tokio::test] async fn test_dynamic_stop_loss_adjustment() { let pool = PgPool::connect(&get_test_db_url()) .await .expect("Failed to connect to test database"); let config = PaperTradingConfig::default(); let executor = PaperTradingExecutor::new(pool.clone(), config); // Test Case 1: Normal regime → 2.0x ATR stop-loss let normal_data = create_regime_market_data("normal"); let atr = calculate_atr(&normal_data); let normal_multiplier = 2.0; let expected_normal_stop = atr * normal_multiplier; println!( "TEST 2.1: Normal regime → Stop-loss: {:.2} (ATR {:.2} x multiplier {})", expected_normal_stop, atr, normal_multiplier ); // Test Case 2: Trending regime → 2.5x ATR stop-loss let trending_data = create_regime_market_data("trending"); let atr = calculate_atr(&trending_data); let trending_multiplier = 2.5; let expected_trending_stop = atr * trending_multiplier; println!( "TEST 2.2: Trending regime → Stop-loss: {:.2} (ATR {:.2} x multiplier {})", expected_trending_stop, atr, trending_multiplier ); // Test Case 3: Volatile regime → 3.0x ATR stop-loss let volatile_data = create_regime_market_data("volatile"); let atr = calculate_atr(&volatile_data); let volatile_multiplier = 3.0; let expected_volatile_stop = atr * volatile_multiplier; println!( "TEST 2.3: Volatile regime → Stop-loss: {:.2} (ATR {:.2} x multiplier {})", expected_volatile_stop, atr, volatile_multiplier ); // Test Case 4: Crisis regime → 4.0x ATR stop-loss let crisis_data = create_regime_market_data("crisis"); let atr = calculate_atr(&crisis_data); let crisis_multiplier = 4.0; let expected_crisis_stop = atr * crisis_multiplier; println!( "TEST 2.4: Crisis regime → Stop-loss: {:.2} (ATR {:.2} x multiplier {})", expected_crisis_stop, atr, crisis_multiplier ); // RED PHASE: Expected to fail - executor does not yet implement dynamic stop-loss // GREEN PHASE: Will implement calculate_regime_adjusted_stop_loss() method println!("✗ RED: test_dynamic_stop_loss_adjustment - Not yet implemented"); } // ============================================================================ // TEST 3: Regime Transition Logging // ============================================================================ #[tokio::test] async fn test_regime_transition_logging() { let pool = PgPool::connect(&get_test_db_url()) .await .expect("Failed to connect to test database"); let config = PaperTradingConfig::default(); let executor = PaperTradingExecutor::new(pool.clone(), config); // Setup: Create test prediction let prediction_id = Uuid::new_v4(); sqlx::query!( r#" INSERT INTO ensemble_predictions ( id, symbol, ensemble_action, ensemble_signal, ensemble_confidence, disagreement_rate ) VALUES ( $1, 'ES.FUT', 'BUY', 0.75, 0.85, 0.10 ) "#, prediction_id, ) .execute(&pool) .await .expect("Failed to insert test prediction"); // Simulate regime transitions: Normal → Trending → Volatile → Crisis let regime_sequence = vec!["Normal", "Trending", "Volatile", "Crisis"]; println!("TEST 3: Simulating regime transitions:"); for (i, regime) in regime_sequence.iter().enumerate() { println!(" Step {}: Transition to {} regime", i + 1, regime); // RED PHASE: Expected to fail - no regime logging implemented // GREEN PHASE: Will implement log_regime_transition() method // Expected: Insert into regime_transitions table with: // - prediction_id // - previous_regime // - new_regime // - transition_timestamp // - confidence_score } // Verify regime transitions were logged // RED PHASE: This query will fail because regime_transitions table doesn't exist yet // GREEN PHASE: Will create migration and verify insertions println!("✗ RED: test_regime_transition_logging - Not yet implemented"); } // ============================================================================ // TEST 4: Order Submission with Regime Metadata // ============================================================================ #[tokio::test] async fn test_order_submission_with_regime_metadata() { let pool = PgPool::connect(&get_test_db_url()) .await .expect("Failed to connect to test database"); let config = PaperTradingConfig::default(); let executor = PaperTradingExecutor::new(pool.clone(), config); // Setup: Create test prediction with regime metadata let prediction_id = Uuid::new_v4(); sqlx::query!( r#" INSERT INTO ensemble_predictions ( id, symbol, ensemble_action, ensemble_signal, ensemble_confidence, disagreement_rate ) VALUES ( $1, 'ES.FUT', 'BUY', 0.75, 0.85, 0.10 ) "#, prediction_id, ) .execute(&pool) .await .expect("Failed to insert test prediction"); // Simulate order submission with regime metadata let trending_data = create_regime_market_data("trending"); let base_size = 10.0; let regime_multiplier = 1.5; let adjusted_size = base_size * regime_multiplier; println!( "TEST 4: Submitting order with regime metadata: Trending regime, adjusted size: {:.2}", adjusted_size ); // RED PHASE: Expected to fail - orders table doesn't have regime columns yet // GREEN PHASE: Will add columns to orders table: // - regime_detected VARCHAR(50) // - regime_confidence DOUBLE PRECISION // - position_multiplier DOUBLE PRECISION // - stop_loss_multiplier DOUBLE PRECISION println!("✗ RED: test_order_submission_with_regime_metadata - Not yet implemented"); } // ============================================================================ // TEST 5: End-to-End Regime-Adaptive Paper Trading // ============================================================================ #[tokio::test] async fn test_e2e_regime_adaptive_paper_trading() { let pool = PgPool::connect(&get_test_db_url()) .await .expect("Failed to connect to test database"); let config = PaperTradingConfig::default(); let executor = PaperTradingExecutor::new(pool.clone(), config); println!("TEST 5: End-to-End Regime-Adaptive Paper Trading"); // Step 1: Start with Normal regime println!(" Step 1: Normal regime - Base position sizing"); let normal_data = create_regime_market_data("normal"); let pred_1 = Uuid::new_v4(); sqlx::query!( r#" INSERT INTO ensemble_predictions ( id, symbol, ensemble_action, ensemble_signal, ensemble_confidence, disagreement_rate ) VALUES ( $1, 'ES.FUT', 'BUY', 0.72, 0.80, 0.12 ) "#, pred_1, ) .execute(&pool) .await .expect("Failed to insert prediction 1"); // Step 2: Detect transition to Trending regime println!(" Step 2: Transition to Trending regime - Increase position size to 1.5x"); let trending_data = create_regime_market_data("trending"); let pred_2 = Uuid::new_v4(); sqlx::query!( r#" INSERT INTO ensemble_predictions ( id, symbol, ensemble_action, ensemble_signal, ensemble_confidence, disagreement_rate ) VALUES ( $1, 'ES.FUT', 'BUY', 0.78, 0.85, 0.08 ) "#, pred_2, ) .execute(&pool) .await .expect("Failed to insert prediction 2"); // Step 3: Detect transition to Volatile regime println!(" Step 3: Transition to Volatile regime - Reduce position size to 0.5x"); let volatile_data = create_regime_market_data("volatile"); let pred_3 = Uuid::new_v4(); sqlx::query!( r#" INSERT INTO ensemble_predictions ( id, symbol, ensemble_action, ensemble_signal, ensemble_confidence, disagreement_rate ) VALUES ( $1, 'ES.FUT', 'SELL', 0.65, 0.75, 0.20 ) "#, pred_3, ) .execute(&pool) .await .expect("Failed to insert prediction 3"); // Step 4: Detect transition to Crisis regime println!(" Step 4: Transition to Crisis regime - Reduce position size to 0.2x"); let crisis_data = create_regime_market_data("crisis"); let pred_4 = Uuid::new_v4(); sqlx::query!( r#" INSERT INTO ensemble_predictions ( id, symbol, ensemble_action, ensemble_signal, ensemble_confidence, disagreement_rate ) VALUES ( $1, 'ES.FUT', 'SELL', 0.70, 0.80, 0.15 ) "#, pred_4, ) .execute(&pool) .await .expect("Failed to insert prediction 4"); // Verify regime transitions and position adjustments // RED PHASE: Expected to fail - full pipeline not yet implemented // GREEN PHASE: Will validate: // 1. Regime detection from market data // 2. Position size adjustments // 3. Stop-loss adjustments // 4. Regime logging to database // 5. Order metadata includes regime information println!("✗ RED: test_e2e_regime_adaptive_paper_trading - Not yet implemented"); // Cleanup sqlx::query!( "DELETE FROM ensemble_predictions WHERE id IN ($1, $2, $3, $4)", pred_1, pred_2, pred_3, pred_4 ) .execute(&pool) .await .expect("Failed to cleanup test predictions"); } // ============================================================================ // Helper: Extract Regime Features (Agent D13-D16) // ============================================================================ /// Extract Wave D regime features from market data /// /// This function will integrate with Wave D feature extraction modules: /// - Agent D13: CUSUM Statistics (indices 201-210) /// - Agent D14: ADX & Directional Indicators (indices 211-215) /// - Agent D15: Regime Transition Probabilities (indices 216-220) /// - Agent D16: Adaptive Strategy Metrics (indices 221-224) fn extract_regime_features(market_data: &[(f64, f64, f64, f64, f64)]) -> HashMap { let mut features = HashMap::new(); // Placeholder for Wave D feature extraction // RED PHASE: Stub implementation // GREEN PHASE: Will integrate with ml/src/features/regime_features.rs features.insert("regime_confidence".to_string(), 0.85); features.insert("cusum_statistic".to_string(), 0.0); features.insert("adx".to_string(), 25.0); features.insert("transition_probability".to_string(), 0.10); features } // ============================================================================ // Helper: Calculate Regime-Adjusted Position Size // ============================================================================ /// Calculate position size adjusted for current market regime /// /// Position size multipliers: /// - Normal: 1.0x (base size) /// - Trending: 1.5x (increase exposure in strong trends) /// - Volatile: 0.5x (reduce exposure in choppy markets) /// - Crisis: 0.2x (minimal exposure during extreme volatility) fn calculate_regime_position_size(base_size: f64, regime: &str) -> f64 { let multiplier = match regime { "Normal" => 1.0, "Trending" => 1.5, "Volatile" => 0.5, "Crisis" => 0.2, _ => 1.0, }; base_size * multiplier } // ============================================================================ // Helper: Calculate Regime-Adjusted Stop-Loss // ============================================================================ /// Calculate stop-loss distance adjusted for current market regime /// /// Stop-loss multipliers (ATR-based): /// - Normal: 2.0x ATR (standard stop distance) /// - Trending: 2.5x ATR (wider stop to avoid whipsaws) /// - Volatile: 3.0x ATR (much wider stop for large swings) /// - Crisis: 4.0x ATR (very wide stop for extreme volatility) fn calculate_regime_stop_loss(atr: f64, regime: &str) -> f64 { let multiplier = match regime { "Normal" => 2.0, "Trending" => 2.5, "Volatile" => 3.0, "Crisis" => 4.0, _ => 2.0, }; atr * multiplier }