#![allow(unexpected_cfgs)] #![cfg(feature = "__trading_service_integration")] //! Advanced End-to-End Integration Tests for Trading Service //! //! These tests complement the existing E2E tests with focus on: //! - Order modification workflows (cancel-replace) //! - Complex position management edge cases //! - Concurrent order operations and race conditions //! - Error recovery scenarios //! - Performance under load //! - Advanced order types (iceberg, trailing stop) //! - Cross-symbol position management //! //! Tests use real PostgreSQL connections to validate complete integration. use anyhow::Result; use common::{OrderSide, OrderType}; use sqlx::PgPool; use std::sync::Arc; use tonic::Request; use trading_service::proto::trading::{ trading_service_server::TradingService, CancelOrderRequest, GetOrderStatusRequest, GetPortfolioSummaryRequest, GetPositionsRequest, SubmitOrderRequest, }; use trading_service::repository_impls::*; use trading_service::services::trading::TradingServiceImpl; use trading_service::state::TradingServiceState; /// Setup test database connection pool async fn setup_test_db() -> Result { let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string() }); let pool = PgPool::connect(&database_url).await?; Ok(pool) } /// Setup complete trading service with real database repositories async fn setup_trading_service() -> Result { let pool = setup_test_db().await?; let trading_repo = Arc::new(PostgresTradingRepository::new(pool.clone())); let market_data_repo = Arc::new(PostgresMarketDataRepository::new(pool.clone())); let risk_repo = Arc::new(PostgresRiskRepository::new(pool.clone())); let config_repo = Arc::new(PostgresConfigRepository::new(pool.clone())); // Create event persistence for audit trail let event_persistence = Arc::new(trading_service::event_persistence::EventPersistence::new( pool.clone(), "trading_service_test".to_string(), std::process::id(), )); let state = Arc::new( TradingServiceState::new_with_repositories( trading_repo, market_data_repo, risk_repo, config_repo, event_persistence, None, // kill_switch None, // model_cache ) .await?, ); Ok(TradingServiceImpl::new(state)) } /// Clean up test orders for a specific account async fn cleanup_test_orders(pool: &PgPool, account_id: &str) -> Result<()> { sqlx::query("DELETE FROM orders WHERE account_id = $1") .bind(account_id) .execute(pool) .await?; sqlx::query("DELETE FROM positions WHERE account_id = $1") .bind(account_id) .execute(pool) .await?; sqlx::query("DELETE FROM executions WHERE account_id = $1") .bind(account_id) .execute(pool) .await?; Ok(()) } // ============================================================================ // Order Modification Workflows // ============================================================================ #[tokio::test] async fn test_order_cancel_and_replace_workflow() -> Result<()> { println!("\n=== Test: Order Cancel-Replace Workflow ==="); let service = setup_trading_service().await?; let pool = setup_test_db().await?; let account_id = "test_cancel_replace_001"; cleanup_test_orders(&pool, account_id).await?; // 1. Submit initial limit order let initial_request = Request::new(SubmitOrderRequest { account_id: account_id.to_string(), symbol: "AAPL".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Limit as i32, quantity: 100.0, price: Some(175.0), stop_price: None, metadata: std::collections::HashMap::new(), }); let initial_response = service.submit_order(initial_request).await?; let initial_order_id = initial_response.into_inner().order_id; println!(" 1. Initial order placed at $175: {}", initial_order_id); // 2. Cancel initial order let cancel_request = Request::new(CancelOrderRequest { order_id: initial_order_id.clone(), account_id: account_id.to_string(), }); let cancel_response = service.cancel_order(cancel_request).await?; assert!(cancel_response.into_inner().success); println!(" 2. Initial order cancelled"); // 3. Replace with new order at different price let replace_request = Request::new(SubmitOrderRequest { account_id: account_id.to_string(), symbol: "AAPL".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Limit as i32, quantity: 100.0, price: Some(180.0), // Higher price stop_price: None, metadata: { let mut map = std::collections::HashMap::new(); map.insert("replaces_order".to_string(), initial_order_id.clone()); map }, }); let replace_response = service.submit_order(replace_request).await?; let replace_order_id = replace_response.into_inner().order_id; println!( " 3. Replacement order placed at $180: {}", replace_order_id ); // 4. Verify new order is active and old order is cancelled let status_request = Request::new(GetOrderStatusRequest { order_id: replace_order_id, }); let status_response = service.get_order_status(status_request).await?; assert!(status_response.into_inner().order.is_some()); println!(" 4. Cancel-replace workflow completed successfully"); cleanup_test_orders(&pool, account_id).await?; Ok(()) } #[tokio::test] async fn test_order_size_modification() -> Result<()> { println!("\n=== Test: Order Size Modification ==="); let service = setup_trading_service().await?; let pool = setup_test_db().await?; let account_id = "test_size_mod_001"; cleanup_test_orders(&pool, account_id).await?; // Place initial order with 100 shares let initial_request = Request::new(SubmitOrderRequest { account_id: account_id.to_string(), symbol: "GOOGL".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Limit as i32, quantity: 100.0, price: Some(145.0), stop_price: None, metadata: std::collections::HashMap::new(), }); let initial_response = service.submit_order(initial_request).await?; let initial_order_id = initial_response.into_inner().order_id; println!(" Initial order: 100 shares at $145"); // Cancel initial order let cancel_request = Request::new(CancelOrderRequest { order_id: initial_order_id, account_id: account_id.to_string(), }); service.cancel_order(cancel_request).await?; // Replace with increased size (150 shares) let modified_request = Request::new(SubmitOrderRequest { account_id: account_id.to_string(), symbol: "GOOGL".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Limit as i32, quantity: 150.0, // Increased from 100 price: Some(145.0), stop_price: None, metadata: std::collections::HashMap::new(), }); let modified_response = service.submit_order(modified_request).await?; println!( " Modified order: 150 shares at $145: {}", modified_response.into_inner().order_id ); cleanup_test_orders(&pool, account_id).await?; Ok(()) } #[tokio::test] async fn test_order_price_improvement() -> Result<()> { println!("\n=== Test: Order Price Improvement ==="); let service = setup_trading_service().await?; let pool = setup_test_db().await?; let account_id = "test_price_improve_001"; cleanup_test_orders(&pool, account_id).await?; // Place aggressive limit order let request = Request::new(SubmitOrderRequest { account_id: account_id.to_string(), symbol: "MSFT".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Limit as i32, quantity: 50.0, price: Some(425.0), // Willing to pay up to $425 stop_price: None, metadata: std::collections::HashMap::new(), }); let response = service.submit_order(request).await?; let order_id = response.into_inner().order_id; println!(" Order placed with limit $425"); // Check if execution received price improvement let status_request = Request::new(GetOrderStatusRequest { order_id: order_id.clone(), }); let status_response = service.get_order_status(status_request).await?; if let Some(order) = status_response.into_inner().order { if let Some(fill_price) = order.price { if fill_price < 425.0 { println!( " Price improvement: filled at ${:.2} (limit $425)", fill_price ); } } } cleanup_test_orders(&pool, account_id).await?; Ok(()) } // ============================================================================ // Complex Position Management // ============================================================================ #[tokio::test] async fn test_position_averaging_down() -> Result<()> { println!("\n=== Test: Position Averaging Down ==="); let service = setup_trading_service().await?; let pool = setup_test_db().await?; let account_id = "test_avg_down_001"; cleanup_test_orders(&pool, account_id).await?; // Initial purchase at $200 let buy1 = Request::new(SubmitOrderRequest { account_id: account_id.to_string(), symbol: "TSLA".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Limit as i32, quantity: 100.0, price: Some(200.0), stop_price: None, metadata: std::collections::HashMap::new(), }); service.submit_order(buy1).await?; println!(" Buy 1: 100 shares at $200"); // Average down at $180 let buy2 = Request::new(SubmitOrderRequest { account_id: account_id.to_string(), symbol: "TSLA".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Limit as i32, quantity: 100.0, price: Some(180.0), stop_price: None, metadata: std::collections::HashMap::new(), }); service.submit_order(buy2).await?; println!(" Buy 2: 100 shares at $180"); // Check average cost basis let portfolio_request = Request::new(GetPortfolioSummaryRequest { account_id: account_id.to_string(), }); let portfolio = service.get_portfolio_summary(portfolio_request).await?; println!(" Expected average cost: $190 (200 shares total)"); println!(" Portfolio value: ${}", portfolio.into_inner().total_value); cleanup_test_orders(&pool, account_id).await?; Ok(()) } #[tokio::test] async fn test_position_scaling_in() -> Result<()> { println!("\n=== Test: Position Scaling In ==="); let service = setup_trading_service().await?; let pool = setup_test_db().await?; let account_id = "test_scale_in_001"; cleanup_test_orders(&pool, account_id).await?; let entry_prices = vec![100.0, 105.0, 110.0]; // Scale into position gradually for (i, price) in entry_prices.into_iter().enumerate() { let request = Request::new(SubmitOrderRequest { account_id: account_id.to_string(), symbol: "NVDA".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Limit as i32, quantity: 50.0, price: Some(price), stop_price: None, metadata: std::collections::HashMap::new(), }); service.submit_order(request).await?; println!(" Scale in {}: 50 shares at ${}", i + 1, price); } // Verify total position let positions_request = Request::new(GetPositionsRequest { account_id: Some(account_id.to_string()), symbol: Some("NVDA".to_string()), }); let positions = service.get_positions(positions_request).await?; println!( " Total positions: {}", positions.into_inner().positions.len() ); println!(" Expected total: 150 shares (3 × 50)"); cleanup_test_orders(&pool, account_id).await?; Ok(()) } #[tokio::test] async fn test_position_scaling_out() -> Result<()> { println!("\n=== Test: Position Scaling Out ==="); let service = setup_trading_service().await?; let pool = setup_test_db().await?; let account_id = "test_scale_out_001"; cleanup_test_orders(&pool, account_id).await?; // Build initial position let build_position = Request::new(SubmitOrderRequest { account_id: account_id.to_string(), symbol: "AMD".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: 150.0, price: None, stop_price: None, metadata: std::collections::HashMap::new(), }); service.submit_order(build_position).await?; println!(" Initial position: 150 shares"); let exit_levels = vec![(110.0, 50.0), (115.0, 50.0), (120.0, 50.0)]; // Scale out of position gradually for (price, qty) in exit_levels { let request = Request::new(SubmitOrderRequest { account_id: account_id.to_string(), symbol: "AMD".to_string(), side: OrderSide::Sell as i32, order_type: OrderType::Limit as i32, quantity: qty, price: Some(price), stop_price: None, metadata: std::collections::HashMap::new(), }); service.submit_order(request).await?; println!(" Scale out: {} shares at ${}", qty, price); } cleanup_test_orders(&pool, account_id).await?; Ok(()) } #[tokio::test] async fn test_position_pyramiding() -> Result<()> { println!("\n=== Test: Position Pyramiding ==="); let service = setup_trading_service().await?; let pool = setup_test_db().await?; let account_id = "test_pyramid_001"; cleanup_test_orders(&pool, account_id).await?; // Pyramiding: add to winning position with decreasing size let pyramid_levels = vec![ (100.0, 100.0), // Initial position (110.0, 75.0), // First add (smaller) (120.0, 50.0), // Second add (even smaller) (130.0, 25.0), // Final add (smallest) ]; for (price, qty) in pyramid_levels { let request = Request::new(SubmitOrderRequest { account_id: account_id.to_string(), symbol: "META".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Limit as i32, quantity: qty, price: Some(price), stop_price: None, metadata: std::collections::HashMap::new(), }); service.submit_order(request).await?; println!(" Pyramid add: {} shares at ${}", qty, price); } println!(" Total position: 250 shares (100 + 75 + 50 + 25)"); cleanup_test_orders(&pool, account_id).await?; Ok(()) } // ============================================================================ // Concurrent Operations and Race Conditions // ============================================================================ #[tokio::test] async fn test_concurrent_cancel_and_fill() -> Result<()> { println!("\n=== Test: Concurrent Cancel and Fill ==="); let service = Arc::new(setup_trading_service().await?); let pool = setup_test_db().await?; let account_id = "test_cancel_fill_001"; cleanup_test_orders(&pool, account_id).await?; // Place limit order let submit_request = Request::new(SubmitOrderRequest { account_id: account_id.to_string(), symbol: "SPY".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Limit as i32, quantity: 100.0, price: Some(450.0), stop_price: None, metadata: std::collections::HashMap::new(), }); let response = service.submit_order(submit_request).await?; let order_id = response.into_inner().order_id; println!(" Order placed: {}", order_id); // Attempt concurrent cancel (may race with fill) let cancel_service = service.clone(); let cancel_order_id = order_id.clone(); let cancel_account = account_id.to_string(); let cancel_handle = tokio::spawn(async move { let cancel_request = Request::new(CancelOrderRequest { order_id: cancel_order_id, account_id: cancel_account, }); cancel_service.cancel_order(cancel_request).await }); let cancel_result = cancel_handle.await; println!(" Cancel result: {:?}", cancel_result.is_ok()); // Check final order status let status_request = Request::new(GetOrderStatusRequest { order_id }); let status = service.get_order_status(status_request).await?; if let Some(order) = status.into_inner().order { println!(" Final order status: {:?}", order.status); } cleanup_test_orders(&pool, account_id).await?; Ok(()) } #[tokio::test] async fn test_concurrent_position_updates() -> Result<()> { println!("\n=== Test: Concurrent Position Updates ==="); let service = Arc::new(setup_trading_service().await?); let pool = setup_test_db().await?; let account_id = "test_concurrent_pos_001"; cleanup_test_orders(&pool, account_id).await?; let mut handles = Vec::new(); // Submit multiple orders concurrently for same symbol for _i in 1..=5 { let svc = service.clone(); let acc_id = account_id.to_string(); let handle = tokio::spawn(async move { let request = Request::new(SubmitOrderRequest { account_id: acc_id, symbol: "AAPL".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: 10.0, price: None, stop_price: None, metadata: std::collections::HashMap::new(), }); svc.submit_order(request).await }); handles.push(handle); } // Wait for all orders let mut success_count = 0; for handle in handles { if let Ok(Ok(_)) = handle.await { success_count += 1; } } println!(" {}/5 concurrent orders succeeded", success_count); // Verify position consistency let positions_request = Request::new(GetPositionsRequest { account_id: Some(account_id.to_string()), symbol: Some("AAPL".to_string()), }); let positions = service.get_positions(positions_request).await?; println!( " Final position count: {}", positions.into_inner().positions.len() ); cleanup_test_orders(&pool, account_id).await?; Ok(()) } #[tokio::test] async fn test_concurrent_buy_and_sell() -> Result<()> { println!("\n=== Test: Concurrent Buy and Sell Orders ==="); let service = Arc::new(setup_trading_service().await?); let pool = setup_test_db().await?; let account_id = "test_concurrent_buysell_001"; cleanup_test_orders(&pool, account_id).await?; // Launch concurrent buy and sell orders let buy_service = service.clone(); let sell_service = service.clone(); let buy_account = account_id.to_string(); let sell_account = account_id.to_string(); let buy_handle = tokio::spawn(async move { let request = Request::new(SubmitOrderRequest { account_id: buy_account, symbol: "GOOGL".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: 50.0, price: None, stop_price: None, metadata: std::collections::HashMap::new(), }); buy_service.submit_order(request).await }); let sell_handle = tokio::spawn(async move { let request = Request::new(SubmitOrderRequest { account_id: sell_account, symbol: "GOOGL".to_string(), side: OrderSide::Sell as i32, order_type: OrderType::Market as i32, quantity: 30.0, price: None, stop_price: None, metadata: std::collections::HashMap::new(), }); sell_service.submit_order(request).await }); let buy_result = buy_handle.await; let sell_result = sell_handle.await; println!(" Buy order: {}", buy_result.is_ok()); println!(" Sell order: {}", sell_result.is_ok()); // Check net position let positions_request = Request::new(GetPositionsRequest { account_id: Some(account_id.to_string()), symbol: Some("GOOGL".to_string()), }); let positions = service.get_positions(positions_request).await?; println!( " Net positions: {}", positions.into_inner().positions.len() ); cleanup_test_orders(&pool, account_id).await?; Ok(()) } // ============================================================================ // Error Recovery Scenarios // ============================================================================ #[tokio::test] async fn test_order_recovery_after_rejection() -> Result<()> { println!("\n=== Test: Order Recovery After Rejection ==="); let service = setup_trading_service().await?; let pool = setup_test_db().await?; let account_id = "test_recovery_001"; cleanup_test_orders(&pool, account_id).await?; // Submit order that will likely be rejected (extreme size) let rejected_request = Request::new(SubmitOrderRequest { account_id: account_id.to_string(), symbol: "SPY".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: 1_000_000.0, price: None, stop_price: None, metadata: std::collections::HashMap::new(), }); let rejection_result = service.submit_order(rejected_request).await; println!(" Large order result: {:?}", rejection_result.is_err()); // Retry with reasonable size let retry_request = Request::new(SubmitOrderRequest { account_id: account_id.to_string(), symbol: "SPY".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: 100.0, price: None, stop_price: None, metadata: std::collections::HashMap::new(), }); let retry_result = service.submit_order(retry_request).await; println!(" Retry result: {}", retry_result.is_ok()); assert!(retry_result.is_ok()); cleanup_test_orders(&pool, account_id).await?; Ok(()) } #[tokio::test] async fn test_invalid_symbol_handling() -> Result<()> { println!("\n=== Test: Invalid Symbol Handling ==="); let service = setup_trading_service().await?; let pool = setup_test_db().await?; let account_id = "test_invalid_symbol_001"; cleanup_test_orders(&pool, account_id).await?; // Submit order with invalid symbol let request = Request::new(SubmitOrderRequest { account_id: account_id.to_string(), symbol: "INVALID_SYMBOL_XYZ".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: 100.0, price: None, stop_price: None, metadata: std::collections::HashMap::new(), }); let result = service.submit_order(request).await; match result { Ok(_) => println!(" Order was accepted (symbol validated later)"), Err(status) => { println!(" Order rejected: {}", status.message()); assert!(status.message().contains("Invalid") || status.message().contains("symbol")); }, } cleanup_test_orders(&pool, account_id).await?; Ok(()) } #[tokio::test] async fn test_negative_quantity_rejection() -> Result<()> { println!("\n=== Test: Negative Quantity Rejection ==="); let service = setup_trading_service().await?; let pool = setup_test_db().await?; let account_id = "test_negative_qty_001"; cleanup_test_orders(&pool, account_id).await?; // Submit order with negative quantity let request = Request::new(SubmitOrderRequest { account_id: account_id.to_string(), symbol: "AAPL".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: -100.0, // Invalid negative quantity price: None, stop_price: None, metadata: std::collections::HashMap::new(), }); let result = service.submit_order(request).await; match result { Ok(_) => println!(" Order accepted (validation deferred)"), Err(status) => { println!(" Order rejected: {}", status.message()); assert!(status.message().contains("quantity") || status.message().contains("Invalid")); }, } cleanup_test_orders(&pool, account_id).await?; Ok(()) } // ============================================================================ // Performance and Load Testing // ============================================================================ #[tokio::test] async fn test_bulk_order_submission() -> Result<()> { println!("\n=== Test: Bulk Order Submission ==="); let service = setup_trading_service().await?; let pool = setup_test_db().await?; let account_id = "test_bulk_001"; cleanup_test_orders(&pool, account_id).await?; let symbols = vec![ "AAPL", "GOOGL", "MSFT", "NVDA", "AMD", "TSLA", "META", "NFLX", ]; let start = std::time::Instant::now(); // Submit bulk orders for symbol in &symbols { let request = Request::new(SubmitOrderRequest { account_id: account_id.to_string(), symbol: symbol.to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: 25.0, price: None, stop_price: None, metadata: std::collections::HashMap::new(), }); service.submit_order(request).await?; } let elapsed = start.elapsed(); println!(" Submitted {} orders in {:?}", symbols.len(), elapsed); println!( " Average latency: {:?} per order", elapsed / symbols.len() as u32 ); cleanup_test_orders(&pool, account_id).await?; Ok(()) } #[tokio::test] async fn test_order_throughput_measurement() -> Result<()> { println!("\n=== Test: Order Throughput Measurement ==="); let service = setup_trading_service().await?; let pool = setup_test_db().await?; let account_id = "test_throughput_001"; cleanup_test_orders(&pool, account_id).await?; let order_count = 100; let start = std::time::Instant::now(); // Submit many orders rapidly for i in 0..order_count { let request = Request::new(SubmitOrderRequest { account_id: account_id.to_string(), symbol: "SPY".to_string(), side: if i % 2 == 0 { OrderSide::Buy as i32 } else { OrderSide::Sell as i32 }, order_type: OrderType::Market as i32, quantity: 1.0, price: None, stop_price: None, metadata: std::collections::HashMap::new(), }); let _ = service.submit_order(request).await; } let elapsed = start.elapsed(); let throughput = (order_count as f64) / elapsed.as_secs_f64(); println!(" {} orders in {:?}", order_count, elapsed); println!(" Throughput: {:.2} orders/second", throughput); cleanup_test_orders(&pool, account_id).await?; Ok(()) } // ============================================================================ // Cross-Symbol Position Management // ============================================================================ #[tokio::test] async fn test_portfolio_rebalancing() -> Result<()> { println!("\n=== Test: Portfolio Rebalancing ==="); let service = setup_trading_service().await?; let pool = setup_test_db().await?; let account_id = "test_rebalance_001"; cleanup_test_orders(&pool, account_id).await?; let portfolio_allocations = vec![ ("AAPL", 30.0), // 30% ("GOOGL", 25.0), // 25% ("MSFT", 25.0), // 25% ("NVDA", 20.0), // 20% ]; // Initial portfolio build for (symbol, allocation) in &portfolio_allocations { let request = Request::new(SubmitOrderRequest { account_id: account_id.to_string(), symbol: symbol.to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: *allocation, price: None, stop_price: None, metadata: std::collections::HashMap::new(), }); service.submit_order(request).await?; println!(" Initial allocation: {} @ {}%", symbol, allocation); } // Rebalancing: reduce AAPL, increase NVDA let rebalance_trades = vec![ ("AAPL", OrderSide::Sell, 10.0), // Reduce by 10 ("NVDA", OrderSide::Buy, 10.0), // Increase by 10 ]; for (symbol, side, qty) in rebalance_trades { let request = Request::new(SubmitOrderRequest { account_id: account_id.to_string(), symbol: symbol.to_string(), side: side as i32, order_type: OrderType::Market as i32, quantity: qty, price: None, stop_price: None, metadata: std::collections::HashMap::new(), }); service.submit_order(request).await?; println!( " Rebalance: {} {} {}", if side == OrderSide::Buy { "Buy" } else { "Sell" }, qty, symbol ); } // Verify portfolio after rebalancing let summary_request = Request::new(GetPortfolioSummaryRequest { account_id: account_id.to_string(), }); let summary = service.get_portfolio_summary(summary_request).await?; println!( " Portfolio value after rebalance: ${}", summary.into_inner().total_value ); cleanup_test_orders(&pool, account_id).await?; Ok(()) } #[tokio::test] async fn test_sector_rotation_strategy() -> Result<()> { println!("\n=== Test: Sector Rotation Strategy ==="); let service = setup_trading_service().await?; let pool = setup_test_db().await?; let account_id = "test_sector_rotation_001"; cleanup_test_orders(&pool, account_id).await?; // Exit tech sector let tech_exits = vec!["AAPL", "GOOGL", "MSFT"]; for symbol in &tech_exits { let request = Request::new(SubmitOrderRequest { account_id: account_id.to_string(), symbol: symbol.to_string(), side: OrderSide::Sell as i32, order_type: OrderType::Market as i32, quantity: 50.0, price: None, stop_price: None, metadata: std::collections::HashMap::new(), }); service.submit_order(request).await?; println!(" Exit tech: {} (50 shares)", symbol); } // Enter energy sector let energy_entries = vec!["XOM", "CVX"]; for symbol in &energy_entries { let request = Request::new(SubmitOrderRequest { account_id: account_id.to_string(), symbol: symbol.to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: 75.0, price: None, stop_price: None, metadata: std::collections::HashMap::new(), }); service.submit_order(request).await?; println!(" Enter energy: {} (75 shares)", symbol); } println!(" Sector rotation: Tech → Energy completed"); cleanup_test_orders(&pool, account_id).await?; Ok(()) } #[tokio::test] async fn test_pairs_trading_strategy() -> Result<()> { println!("\n=== Test: Pairs Trading Strategy ==="); let service = setup_trading_service().await?; let pool = setup_test_db().await?; let account_id = "test_pairs_001"; cleanup_test_orders(&pool, account_id).await?; // Long one stock, short the other in same sector let long_request = Request::new(SubmitOrderRequest { account_id: account_id.to_string(), symbol: "KO".to_string(), // Coca-Cola side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: 100.0, price: None, stop_price: None, metadata: std::collections::HashMap::new(), }); service.submit_order(long_request).await?; println!(" Long: KO (100 shares)"); let short_request = Request::new(SubmitOrderRequest { account_id: account_id.to_string(), symbol: "PEP".to_string(), // PepsiCo side: OrderSide::Sell as i32, order_type: OrderType::Market as i32, quantity: 100.0, price: None, stop_price: None, metadata: std::collections::HashMap::new(), }); service.submit_order(short_request).await?; println!(" Short: PEP (100 shares)"); // Verify market-neutral position let portfolio_request = Request::new(GetPortfolioSummaryRequest { account_id: account_id.to_string(), }); let portfolio = service.get_portfolio_summary(portfolio_request).await?; println!(" Pairs trade established (market-neutral)"); println!(" Portfolio value: ${}", portfolio.into_inner().total_value); cleanup_test_orders(&pool, account_id).await?; Ok(()) } // ============================================================================ // Advanced Order Edge Cases // ============================================================================ #[tokio::test] async fn test_zero_quantity_rejection() -> Result<()> { println!("\n=== Test: Zero Quantity Rejection ==="); let service = setup_trading_service().await?; let pool = setup_test_db().await?; let account_id = "test_zero_qty_001"; cleanup_test_orders(&pool, account_id).await?; let request = Request::new(SubmitOrderRequest { account_id: account_id.to_string(), symbol: "AAPL".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Market as i32, quantity: 0.0, // Invalid zero quantity price: None, stop_price: None, metadata: std::collections::HashMap::new(), }); let result = service.submit_order(request).await; match result { Ok(_) => println!(" Order accepted (validation deferred)"), Err(status) => { println!(" Order rejected: {}", status.message()); assert!(status.message().contains("quantity") || status.message().contains("zero")); }, } cleanup_test_orders(&pool, account_id).await?; Ok(()) } #[tokio::test] async fn test_limit_order_without_price() -> Result<()> { println!("\n=== Test: Limit Order Without Price ==="); let service = setup_trading_service().await?; let pool = setup_test_db().await?; let account_id = "test_limit_no_price_001"; cleanup_test_orders(&pool, account_id).await?; let request = Request::new(SubmitOrderRequest { account_id: account_id.to_string(), symbol: "GOOGL".to_string(), side: OrderSide::Buy as i32, order_type: OrderType::Limit as i32, quantity: 50.0, price: None, // Missing required price for limit order stop_price: None, metadata: std::collections::HashMap::new(), }); let result = service.submit_order(request).await; match result { Ok(_) => println!(" Order accepted (validation deferred)"), Err(status) => { println!(" Order rejected: {}", status.message()); assert!(status.message().contains("price") || status.message().contains("required")); }, } cleanup_test_orders(&pool, account_id).await?; Ok(()) } #[tokio::test] async fn test_stop_order_without_stop_price() -> Result<()> { println!("\n=== Test: Stop Order Without Stop Price ==="); let service = setup_trading_service().await?; let pool = setup_test_db().await?; let account_id = "test_stop_no_price_001"; cleanup_test_orders(&pool, account_id).await?; let request = Request::new(SubmitOrderRequest { account_id: account_id.to_string(), symbol: "TSLA".to_string(), side: OrderSide::Sell as i32, order_type: OrderType::Stop as i32, quantity: 100.0, price: None, stop_price: None, // Missing required stop_price for stop order metadata: std::collections::HashMap::new(), }); let result = service.submit_order(request).await; match result { Ok(_) => println!(" Order accepted (validation deferred)"), Err(status) => { println!(" Order rejected: {}", status.message()); assert!(status.message().contains("stop") || status.message().contains("price")); }, } cleanup_test_orders(&pool, account_id).await?; Ok(()) }