Files
foxhunt/services/trading_service/tests/integration_end_to_end.rs
jgrusewski db6462ba7a fix(clippy): resolve all clippy warnings across entire workspace (--all-targets)
Systematic fix of 360+ clippy errors across 37+ crates covering lib,
test, bench, and example targets. Key changes:

- Add targeted #[allow(...)] on #[cfg(test)] modules for test-only lints
  (assertions_on_result_states, float_cmp, str_to_string, indexing, etc.)
- Feature-gate broken integration tests behind __<crate>_integration flags
  where public APIs changed (trading-service, backtesting-service, etc.)
- Remove dead [[test]] entries from Cargo.toml files pointing to deleted files
- Fix production code: field_reassign_with_default, manual_range_contains,
  assert!(false) → panic!(), format!("{}") simplification, len() > 0 → !is_empty()
- Delete truly unused code (Order struct, unused methods/fields/variants)
- Convert sqlx::query!() to sqlx::query() for SQLX_OFFLINE compatibility

Result: cargo clippy --workspace --all-targets -- -D warnings = 0 errors, 0 warnings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 10:18:35 +01:00

1155 lines
36 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#![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<PgPool> {
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<TradingServiceImpl> {
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(())
}