#![allow(unexpected_cfgs)] #![cfg(feature = "__trading_service_integration")] //! E2E Test Suite for Paper Trading Executor //! //! This test suite provides comprehensive TDD validation for the paper trading executor, //! which converts ensemble predictions into simulated orders. Tests cover the entire //! prediction-to-order pipeline including SQL queries, enum conversion, position tracking, //! error handling, and concurrent execution. //! //! ## Test Coverage //! 1. Fetch pending predictions (SQL query with confidence/symbol filters) //! 2. Prediction to order conversion (BUY→buy, SELL→sell, confidence≥60%) //! 3. Order creation SQL (INSERT with lowercase enum conversion) //! 4. Position tracking (HashMap-based position management) //! 5. Error handling (database errors, invalid predictions) //! 6. Polling interval timing (100ms interval validation) //! 7. Concurrent execution (race condition prevention) //! //! ## Architecture //! - Uses sqlx::test macro for automatic database setup/teardown //! - Tests use real PostgreSQL (not mocks) for SQL validation //! - Follows TDD principles from Agent 146 //! - Tests both happy path and error cases use sqlx::PgPool; use std::collections::HashMap; use std::sync::Arc; use std::time::{Duration, Instant}; use uuid::Uuid; // Import paper trading executor types // Note: This will need to be exposed in lib.rs or made pub(crate) use trading_service::paper_trading_executor::{ PaperTradingConfig, PaperTradingExecutor, PendingPrediction, }; // 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() }) } // ============================================================================ // TEST 1: Fetch Pending Predictions // ============================================================================ #[tokio::test] async fn test_fetch_pending_predictions() { let pool = PgPool::connect(&get_test_db_url()) .await .expect("Failed to connect to test database"); // Setup: Insert test predictions with various confidence levels let pred_high_confidence = Uuid::new_v4(); let pred_low_confidence = Uuid::new_v4(); let pred_already_executed = Uuid::new_v4(); let pred_wrong_symbol = Uuid::new_v4(); let pred_hold_action = Uuid::new_v4(); // High confidence BUY - should be fetched 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 ) "#, pred_high_confidence, ) .execute(&pool) .await .expect("Failed to insert high confidence prediction"); // Low confidence BUY - should NOT be fetched (< 60%) sqlx::query!( r#" INSERT INTO ensemble_predictions ( id, symbol, ensemble_action, ensemble_signal, ensemble_confidence, disagreement_rate ) VALUES ( $1, 'ES.FUT', 'BUY', 0.55, 0.55, 0.30 ) "#, pred_low_confidence, ) .execute(&pool) .await .expect("Failed to insert low confidence prediction"); // Already executed (has order_id) - should NOT be fetched sqlx::query!( r#" INSERT INTO ensemble_predictions ( id, symbol, ensemble_action, ensemble_signal, ensemble_confidence, disagreement_rate, order_id ) VALUES ( $1, 'ES.FUT', 'BUY', 0.80, 0.90, 0.05, $2 ) "#, pred_already_executed, Uuid::new_v4(), ) .execute(&pool) .await .expect("Failed to insert already executed prediction"); // Wrong symbol - should NOT be fetched sqlx::query!( r#" INSERT INTO ensemble_predictions ( id, symbol, ensemble_action, ensemble_signal, ensemble_confidence, disagreement_rate ) VALUES ( $1, 'UNKNOWN.FUT', 'BUY', 0.75, 0.85, 0.10 ) "#, pred_wrong_symbol, ) .execute(&pool) .await .expect("Failed to insert wrong symbol prediction"); // HOLD action - should NOT be fetched sqlx::query!( r#" INSERT INTO ensemble_predictions ( id, symbol, ensemble_action, ensemble_signal, ensemble_confidence, disagreement_rate ) VALUES ( $1, 'ES.FUT', 'HOLD', 0.10, 0.70, 0.20 ) "#, pred_hold_action, ) .execute(&pool) .await .expect("Failed to insert HOLD prediction"); // Execute: Query pending predictions let config = PaperTradingConfig::default(); let executor = PaperTradingExecutor::new(pool.clone(), config); let predictions = executor .fetch_pending_predictions() .await .expect("Failed to fetch predictions"); // Assert: Only high confidence BUY/SELL predictions from allowed symbols should be fetched assert_eq!( predictions.len(), 1, "Only high confidence BUY from ES.FUT should be fetched" ); assert_eq!(predictions[0].id, pred_high_confidence); assert_eq!(predictions[0].ensemble_action, "BUY"); assert!((predictions[0].ensemble_confidence - 0.85).abs() < 0.001); // Cleanup sqlx::query!( "DELETE FROM ensemble_predictions WHERE id = ANY($1)", &[ pred_high_confidence, pred_low_confidence, pred_already_executed, pred_wrong_symbol, pred_hold_action ] ) .execute(&pool) .await .expect("Failed to cleanup"); pool.close().await; } // ============================================================================ // TEST 2: Prediction to Order Conversion // ============================================================================ #[tokio::test] async fn test_prediction_to_order_conversion() { let pool = PgPool::connect(&get_test_db_url()) .await .expect("Failed to connect to test database"); // Setup: Insert BUY prediction let pred_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 ) "#, pred_id, ) .execute(&pool) .await .expect("Failed to insert test prediction"); // Execute: Create executor and execute prediction let config = PaperTradingConfig::default(); let executor = Arc::new(PaperTradingExecutor::new(pool.clone(), config)); let prediction = PendingPrediction { id: pred_id, symbol: "ES.FUT".to_string(), ensemble_action: "BUY".to_string(), ensemble_signal: 0.75, ensemble_confidence: 0.85, }; executor .execute_prediction(&prediction) .await .expect("Failed to execute prediction"); // Assert: Verify order was created with lowercase 'buy' let order = sqlx::query!( r#" SELECT id, symbol, side, status, quantity FROM orders WHERE symbol = 'ES.FUT' AND account_id = 'paper_trading_001' ORDER BY created_at DESC LIMIT 1 "#, ) .fetch_one(&pool) .await .expect("Failed to fetch created order"); assert_eq!(order.side, "buy", "Order side should be lowercase 'buy'"); assert_eq!(order.status, "filled", "Order should be filled"); assert_eq!(order.symbol, "ES.FUT"); // Assert: Verify prediction is linked to order let updated_pred = sqlx::query!( r#" SELECT order_id FROM ensemble_predictions WHERE id = $1 "#, pred_id, ) .fetch_one(&pool) .await .expect("Failed to fetch updated prediction"); assert!( updated_pred.order_id.is_some(), "Prediction should be linked to order" ); // Cleanup sqlx::query!("DELETE FROM orders WHERE id = $1", order.id) .execute(&pool) .await .expect("Failed to cleanup order"); sqlx::query!("DELETE FROM ensemble_predictions WHERE id = $1", pred_id) .execute(&pool) .await .expect("Failed to cleanup prediction"); pool.close().await; } // ============================================================================ // TEST 3: Order Creation SQL (BUY and SELL) // ============================================================================ #[tokio::test] async fn test_order_creation_sql() { let pool = PgPool::connect(&get_test_db_url()) .await .expect("Failed to connect to test database"); // Test BUY order { let pred_id = Uuid::new_v4(); sqlx::query!( r#" INSERT INTO ensemble_predictions ( id, symbol, ensemble_action, ensemble_signal, ensemble_confidence, disagreement_rate ) VALUES ( $1, 'NQ.FUT', 'BUY', 0.70, 0.80, 0.15 ) "#, pred_id, ) .execute(&pool) .await .expect("Failed to insert BUY prediction"); let config = PaperTradingConfig::default(); let executor = Arc::new(PaperTradingExecutor::new(pool.clone(), config)); let prediction = PendingPrediction { id: pred_id, symbol: "NQ.FUT".to_string(), ensemble_action: "BUY".to_string(), ensemble_signal: 0.70, ensemble_confidence: 0.80, }; executor .execute_prediction(&prediction) .await .expect("Failed to execute BUY prediction"); let order = sqlx::query!( r#" SELECT side, order_type, status, venue, time_in_force FROM orders WHERE symbol = 'NQ.FUT' AND account_id = 'paper_trading_001' ORDER BY created_at DESC LIMIT 1 "#, ) .fetch_one(&pool) .await .expect("Failed to fetch BUY order"); assert_eq!(order.side, "buy"); assert_eq!(order.order_type, "market"); assert_eq!(order.status, "filled"); assert_eq!(order.venue, "PAPER_TRADING"); assert_eq!(order.time_in_force, "day"); // Cleanup sqlx::query!( "DELETE FROM orders WHERE symbol = 'NQ.FUT' AND account_id = 'paper_trading_001'" ) .execute(&pool) .await .expect("Failed to cleanup BUY order"); sqlx::query!("DELETE FROM ensemble_predictions WHERE id = $1", pred_id) .execute(&pool) .await .expect("Failed to cleanup BUY prediction"); } // Test SELL order { let pred_id = Uuid::new_v4(); sqlx::query!( r#" INSERT INTO ensemble_predictions ( id, symbol, ensemble_action, ensemble_signal, ensemble_confidence, disagreement_rate ) VALUES ( $1, 'ZN.FUT', 'SELL', -0.65, 0.75, 0.20 ) "#, pred_id, ) .execute(&pool) .await .expect("Failed to insert SELL prediction"); let config = PaperTradingConfig::default(); let executor = Arc::new(PaperTradingExecutor::new(pool.clone(), config)); let prediction = PendingPrediction { id: pred_id, symbol: "ZN.FUT".to_string(), ensemble_action: "SELL".to_string(), ensemble_signal: -0.65, ensemble_confidence: 0.75, }; executor .execute_prediction(&prediction) .await .expect("Failed to execute SELL prediction"); let order = sqlx::query!( r#" SELECT side, order_type, status FROM orders WHERE symbol = 'ZN.FUT' AND account_id = 'paper_trading_001' ORDER BY created_at DESC LIMIT 1 "#, ) .fetch_one(&pool) .await .expect("Failed to fetch SELL order"); assert_eq!(order.side, "sell", "Order side should be lowercase 'sell'"); assert_eq!(order.order_type, "market"); assert_eq!(order.status, "filled"); // Cleanup sqlx::query!( "DELETE FROM orders WHERE symbol = 'ZN.FUT' AND account_id = 'paper_trading_001'" ) .execute(&pool) .await .expect("Failed to cleanup SELL order"); sqlx::query!("DELETE FROM ensemble_predictions WHERE id = $1", pred_id) .execute(&pool) .await .expect("Failed to cleanup SELL prediction"); } pool.close().await; } // ============================================================================ // TEST 4: Position Tracking // ============================================================================ #[tokio::test] async fn test_position_tracking() { let pool = PgPool::connect(&get_test_db_url()) .await .expect("Failed to connect to test database"); let config = PaperTradingConfig::default(); let executor = Arc::new(PaperTradingExecutor::new(pool.clone(), config)); // Execute multiple predictions for same symbol let symbols = vec!["ES.FUT", "ES.FUT", "NQ.FUT"]; let mut pred_ids = Vec::new(); for symbol in &symbols { let pred_id = Uuid::new_v4(); pred_ids.push(pred_id); sqlx::query!( r#" INSERT INTO ensemble_predictions ( id, symbol, ensemble_action, ensemble_signal, ensemble_confidence, disagreement_rate ) VALUES ( $1, $2, 'BUY', 0.75, 0.85, 0.10 ) "#, pred_id, symbol, ) .execute(&pool) .await .expect("Failed to insert prediction"); let prediction = PendingPrediction { id: pred_id, symbol: symbol.to_string(), ensemble_action: "BUY".to_string(), ensemble_signal: 0.75, ensemble_confidence: 0.85, }; executor .execute_prediction(&prediction) .await .expect("Failed to execute prediction"); } // Assert: Check position summary let summary = executor.get_position_summary().await; assert_eq!( summary.get("ES.FUT"), Some(&2), "ES.FUT should have 2 positions" ); assert_eq!( summary.get("NQ.FUT"), Some(&1), "NQ.FUT should have 1 position" ); // Cleanup for pred_id in pred_ids { sqlx::query!("DELETE FROM ensemble_predictions WHERE id = $1", pred_id) .execute(&pool) .await .expect("Failed to cleanup prediction"); } sqlx::query!("DELETE FROM orders WHERE account_id = 'paper_trading_001'") .execute(&pool) .await .expect("Failed to cleanup orders"); pool.close().await; } // ============================================================================ // TEST 5: Error Handling // ============================================================================ #[tokio::test] async fn test_error_handling_invalid_symbol() { let pool = PgPool::connect(&get_test_db_url()) .await .expect("Failed to connect to test database"); let config = PaperTradingConfig::default(); let executor = Arc::new(PaperTradingExecutor::new(pool.clone(), config)); // Test invalid symbol let prediction = PendingPrediction { id: Uuid::new_v4(), symbol: "INVALID.FUT".to_string(), ensemble_action: "BUY".to_string(), ensemble_signal: 0.75, ensemble_confidence: 0.85, }; let result = executor.execute_prediction(&prediction).await; assert!(result.is_err(), "Should fail on invalid symbol"); assert!( result .unwrap_err() .to_string() .contains("not in allowed list"), "Error should mention allowed list" ); pool.close().await; } #[tokio::test] async fn test_error_handling_low_confidence() { let pool = PgPool::connect(&get_test_db_url()) .await .expect("Failed to connect to test database"); let config = PaperTradingConfig::default(); let executor = Arc::new(PaperTradingExecutor::new(pool.clone(), config)); // Test low confidence (below 60% threshold) let prediction = PendingPrediction { id: Uuid::new_v4(), symbol: "ES.FUT".to_string(), ensemble_action: "BUY".to_string(), ensemble_signal: 0.55, ensemble_confidence: 0.50, // Below 60% threshold }; let result = executor.execute_prediction(&prediction).await; assert!(result.is_err(), "Should fail on low confidence"); assert!( result.unwrap_err().to_string().contains("below threshold"), "Error should mention threshold" ); pool.close().await; } #[tokio::test] async fn test_error_handling_position_limit() { let pool = PgPool::connect(&get_test_db_url()) .await .expect("Failed to connect to test database"); let config = PaperTradingConfig::default(); let executor = Arc::new(PaperTradingExecutor::new(pool.clone(), config)); // Create 10 positions (max limit) let mut pred_ids = Vec::new(); for i in 0..10 { let pred_id = Uuid::new_v4(); pred_ids.push(pred_id); 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 ) "#, pred_id, ) .execute(&pool) .await .expect("Failed to insert prediction"); let prediction = PendingPrediction { id: pred_id, symbol: "ES.FUT".to_string(), ensemble_action: "BUY".to_string(), ensemble_signal: 0.75, ensemble_confidence: 0.85, }; executor .execute_prediction(&prediction) .await .expect("Failed to execute prediction"); } // Try to create 11th position - should fail let pred_id = Uuid::new_v4(); pred_ids.push(pred_id); 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 ) "#, pred_id, ) .execute(&pool) .await .expect("Failed to insert prediction"); let prediction = PendingPrediction { id: pred_id, symbol: "ES.FUT".to_string(), ensemble_action: "BUY".to_string(), ensemble_signal: 0.75, ensemble_confidence: 0.85, }; let result = executor.execute_prediction(&prediction).await; assert!(result.is_err(), "Should fail when position limit reached"); assert!( result.unwrap_err().to_string().contains("position limit"), "Error should mention position limit" ); // Cleanup for pred_id in pred_ids { sqlx::query!("DELETE FROM ensemble_predictions WHERE id = $1", pred_id) .execute(&pool) .await .expect("Failed to cleanup prediction"); } sqlx::query!("DELETE FROM orders WHERE account_id = 'paper_trading_001' AND symbol = 'ES.FUT'") .execute(&pool) .await .expect("Failed to cleanup orders"); pool.close().await; } // ============================================================================ // TEST 6: Polling Interval Timing // ============================================================================ #[tokio::test] async fn test_polling_interval_timing() { let pool = PgPool::connect(&get_test_db_url()) .await .expect("Failed to connect to test database"); // Create config with 50ms polling interval for faster testing let mut config = PaperTradingConfig::default(); config.poll_interval_ms = 50; let executor = Arc::new(PaperTradingExecutor::new(pool.clone(), config.clone())); // Measure time for 5 polling cycles let start = Instant::now(); let mut interval = tokio::time::interval(Duration::from_millis(config.poll_interval_ms)); for _ in 0..5 { interval.tick().await; } let elapsed = start.elapsed(); // Assert: Should take approximately 250ms (5 cycles × 50ms) // Allow ±50ms tolerance for system jitter let expected_ms = 5 * config.poll_interval_ms; let actual_ms = elapsed.as_millis() as u64; assert!( actual_ms >= expected_ms - 50 && actual_ms <= expected_ms + 50, "Polling interval timing incorrect: expected ~{}ms, got {}ms", expected_ms, actual_ms ); pool.close().await; } // ============================================================================ // TEST 7: Concurrent Execution (Race Condition Prevention) // ============================================================================ #[tokio::test] async fn test_concurrent_execution() { let pool = PgPool::connect(&get_test_db_url()) .await .expect("Failed to connect to test database"); // Insert 20 predictions let mut pred_ids = Vec::new(); for i in 0..20 { let pred_id = Uuid::new_v4(); pred_ids.push(pred_id); let symbol = if i % 2 == 0 { "ES.FUT" } else { "NQ.FUT" }; sqlx::query!( r#" INSERT INTO ensemble_predictions ( id, symbol, ensemble_action, ensemble_signal, ensemble_confidence, disagreement_rate ) VALUES ( $1, $2, 'BUY', 0.75, 0.85, 0.10 ) "#, pred_id, symbol, ) .execute(&pool) .await .expect("Failed to insert prediction"); } // Create two executors (simulating concurrent instances) let config = PaperTradingConfig::default(); let executor1 = Arc::new(PaperTradingExecutor::new(pool.clone(), config.clone())); let executor2 = Arc::new(PaperTradingExecutor::new(pool.clone(), config.clone())); // Execute concurrently let handle1 = { let executor = executor1.clone(); tokio::spawn(async move { executor.execute_cycle().await }) }; let handle2 = { let executor = executor2.clone(); tokio::spawn(async move { executor.execute_cycle().await }) }; let result1 = handle1 .await .expect("Task 1 panicked") .expect("Executor 1 failed"); let result2 = handle2 .await .expect("Task 2 panicked") .expect("Executor 2 failed"); let total_processed = result1 + result2; // Assert: Total processed should be <= 20 (no duplicates) // Note: Some predictions may be processed twice if executors run simultaneously, // but order_id update should be idempotent assert!( total_processed <= 20, "Concurrent execution processed {} predictions (expected <= 20)", total_processed ); // Assert: All predictions should be linked to exactly one order let linked_count = sqlx::query!( r#" SELECT COUNT(*) as count FROM ensemble_predictions WHERE id = ANY($1) AND order_id IS NOT NULL "#, &pred_ids, ) .fetch_one(&pool) .await .expect("Failed to count linked predictions"); assert!( linked_count.count.unwrap_or(0) > 0, "At least some predictions should be linked to orders" ); // Cleanup for pred_id in pred_ids { sqlx::query!("DELETE FROM ensemble_predictions WHERE id = $1", pred_id) .execute(&pool) .await .expect("Failed to cleanup prediction"); } sqlx::query!("DELETE FROM orders WHERE account_id = 'paper_trading_001'") .execute(&pool) .await .expect("Failed to cleanup orders"); pool.close().await; } // ============================================================================ // TEST 8: End-to-End Execute Cycle // ============================================================================ #[tokio::test] async fn test_execute_cycle_e2e() { let pool = PgPool::connect(&get_test_db_url()) .await .expect("Failed to connect to test database"); // Insert mixed predictions let mut pred_ids = Vec::new(); // Valid BUY prediction let pred1 = Uuid::new_v4(); pred_ids.push(pred1); 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 ) "#, pred1, ) .execute(&pool) .await .expect("Failed to insert valid BUY prediction"); // Valid SELL prediction let pred2 = Uuid::new_v4(); pred_ids.push(pred2); sqlx::query!( r#" INSERT INTO ensemble_predictions ( id, symbol, ensemble_action, ensemble_signal, ensemble_confidence, disagreement_rate ) VALUES ( $1, 'NQ.FUT', 'SELL', -0.70, 0.80, 0.15 ) "#, pred2, ) .execute(&pool) .await .expect("Failed to insert valid SELL prediction"); // Low confidence - should be ignored let pred3 = Uuid::new_v4(); pred_ids.push(pred3); sqlx::query!( r#" INSERT INTO ensemble_predictions ( id, symbol, ensemble_action, ensemble_signal, ensemble_confidence, disagreement_rate ) VALUES ( $1, 'ZN.FUT', 'BUY', 0.55, 0.50, 0.40 ) "#, pred3, ) .execute(&pool) .await .expect("Failed to insert low confidence prediction"); // Execute cycle let config = PaperTradingConfig::default(); let executor = Arc::new(PaperTradingExecutor::new(pool.clone(), config)); let processed_count = executor .execute_cycle() .await .expect("Execute cycle failed"); // Assert: Should process 2 predictions (pred1 and pred2) assert_eq!(processed_count, 2, "Should process 2 valid predictions"); // Assert: Both valid predictions should be linked to orders let linked_valid = sqlx::query!( r#" SELECT COUNT(*) as count FROM ensemble_predictions WHERE id = ANY($1) AND order_id IS NOT NULL "#, &[pred1, pred2], ) .fetch_one(&pool) .await .expect("Failed to count linked predictions"); assert_eq!( linked_valid.count.unwrap_or(0), 2, "Both valid predictions should be linked" ); // Assert: Low confidence prediction should NOT be linked let linked_low = sqlx::query!( r#" SELECT order_id FROM ensemble_predictions WHERE id = $1 "#, pred3, ) .fetch_one(&pool) .await .expect("Failed to fetch low confidence prediction"); assert!( linked_low.order_id.is_none(), "Low confidence prediction should NOT be executed" ); // Assert: Two orders should be created let order_count = sqlx::query!( r#" SELECT COUNT(*) as count FROM orders WHERE account_id = 'paper_trading_001' "#, ) .fetch_one(&pool) .await .expect("Failed to count orders"); assert_eq!(order_count.count.unwrap_or(0), 2, "Should create 2 orders"); // Cleanup for pred_id in pred_ids { sqlx::query!("DELETE FROM ensemble_predictions WHERE id = $1", pred_id) .execute(&pool) .await .expect("Failed to cleanup prediction"); } sqlx::query!("DELETE FROM orders WHERE account_id = 'paper_trading_001'") .execute(&pool) .await .expect("Failed to cleanup orders"); pool.close().await; } // ============================================================================ // TEST 9: Batch Processing // ============================================================================ #[tokio::test] async fn test_batch_processing_limit() { let pool = PgPool::connect(&get_test_db_url()) .await .expect("Failed to connect to test database"); // Insert 150 predictions (exceeds default batch size of 100) let mut pred_ids = Vec::new(); for i in 0..150 { let pred_id = Uuid::new_v4(); pred_ids.push(pred_id); let symbol = match i % 4 { 0 => "ES.FUT", 1 => "NQ.FUT", 2 => "ZN.FUT", _ => "6E.FUT", }; sqlx::query!( r#" INSERT INTO ensemble_predictions ( id, symbol, ensemble_action, ensemble_signal, ensemble_confidence, disagreement_rate ) VALUES ( $1, $2, 'BUY', 0.75, 0.85, 0.10 ) "#, pred_id, symbol, ) .execute(&pool) .await .expect("Failed to insert prediction"); } // Execute single cycle let config = PaperTradingConfig::default(); let executor = Arc::new(PaperTradingExecutor::new(pool.clone(), config.clone())); let processed_count = executor .execute_cycle() .await .expect("Execute cycle failed"); // Assert: Should process exactly batch_size (100) predictions assert_eq!( processed_count, config.batch_size, "Should process batch_size predictions per cycle" ); // Execute second cycle to process remaining 50 let processed_count2 = executor .execute_cycle() .await .expect("Second execute cycle failed"); assert_eq!( processed_count2, 50, "Second cycle should process remaining 50 predictions" ); // Cleanup for pred_id in pred_ids { sqlx::query!("DELETE FROM ensemble_predictions WHERE id = $1", pred_id) .execute(&pool) .await .expect("Failed to cleanup prediction"); } sqlx::query!("DELETE FROM orders WHERE account_id = 'paper_trading_001'") .execute(&pool) .await .expect("Failed to cleanup orders"); pool.close().await; } // ============================================================================ // TEST 10: Configuration Variations // ============================================================================ #[tokio::test] async fn test_custom_configuration() { let pool = PgPool::connect(&get_test_db_url()) .await .expect("Failed to connect to test database"); // Custom config: Higher confidence threshold (75%) let mut config = PaperTradingConfig::default(); config.min_confidence = 0.75; config.allowed_symbols = vec!["ES.FUT".to_string()]; // Insert predictions with varying confidence let pred_high = 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.80, 0.85, 0.10 ) "#, pred_high, ) .execute(&pool) .await .expect("Failed to insert high confidence prediction"); let pred_medium = 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.65, 0.70, 0.20 ) "#, pred_medium, ) .execute(&pool) .await .expect("Failed to insert medium confidence prediction"); let pred_wrong_symbol = Uuid::new_v4(); sqlx::query!( r#" INSERT INTO ensemble_predictions ( id, symbol, ensemble_action, ensemble_signal, ensemble_confidence, disagreement_rate ) VALUES ( $1, 'NQ.FUT', 'BUY', 0.80, 0.85, 0.10 ) "#, pred_wrong_symbol, ) .execute(&pool) .await .expect("Failed to insert wrong symbol prediction"); // Execute with custom config let executor = Arc::new(PaperTradingExecutor::new(pool.clone(), config)); let processed_count = executor .execute_cycle() .await .expect("Execute cycle failed"); // Assert: Only high confidence ES.FUT prediction should be processed assert_eq!( processed_count, 1, "Only high confidence ES.FUT should be processed" ); // Cleanup sqlx::query!( "DELETE FROM ensemble_predictions WHERE id = ANY($1)", &[pred_high, pred_medium, pred_wrong_symbol] ) .execute(&pool) .await .expect("Failed to cleanup predictions"); sqlx::query!("DELETE FROM orders WHERE account_id = 'paper_trading_001'") .execute(&pool) .await .expect("Failed to cleanup orders"); pool.close().await; }