Files
foxhunt/tests/integration/risk_enforcement.rs
jgrusewski 72f607759a 🔧 Fix import paths: Remove non-existent prelude module references
- Fixed 101+ files importing common::types::prelude which doesn't exist
- Changed all imports to use common::types directly
- Fixed BarEvent duplicate import in data/src/types.rs
- Aligned all imports with canonical type system in common crate
2025-09-26 19:53:00 +02:00

976 lines
38 KiB
Rust

//! Risk Limit Enforcement Integration Tests
//!
//! This module provides comprehensive integration tests for risk limit enforcement
//! within the Foxhunt HFT system, including position limits, exposure limits,
//! drawdown protection, and real-time risk monitoring.
use std::collections::HashMap;
use std::sync::{Arc, atomic::{AtomicU64, AtomicBool, Ordering}};
use std::time::{Duration, Instant};
use tokio::sync::{RwLock, Mutex};
use uuid::Uuid;
use serde_json::json;
use chrono::{DateTime, Utc};
use common::types::*;
use tli::prelude::*;
use crate::fixtures::{IntegrationTestConfig, TestEnvironment, TestMetricsCollector};
use crate::mocks::{MockTradingService, MockRiskService, TestDatabaseManager};
/// Risk limit enforcement integration tests
pub struct RiskEnforcementTests {
client_suite: TliClientSuite,
mock_trading_service: MockTradingService,
mock_risk_service: MockRiskService,
test_db: TestDatabaseManager,
risk_manager: Arc<RiskManager>,
position_tracker: Arc<PositionTracker>,
metrics: Arc<RiskMetrics>,
config: IntegrationTestConfig,
test_accounts: Arc<RwLock<HashMap<String, TestAccount>>>,
}
/// Risk enforcement performance metrics
#[derive(Debug, Default)]
pub struct RiskMetrics {
pub risk_check_latency: AtomicU64,
pub position_update_latency: AtomicU64,
pub limit_breach_detection_latency: AtomicU64,
pub risk_checks_performed: AtomicU64,
pub orders_rejected: AtomicU64,
pub positions_liquidated: AtomicU64,
pub limit_breaches_detected: AtomicU64,
pub emergency_stops_triggered: AtomicU64,
}
impl RiskMetrics {
pub fn new() -> Self {
Self::default()
}
pub fn record_risk_check(&self, latency_ns: u64) {
self.risk_check_latency.store(latency_ns, Ordering::Relaxed);
self.risk_checks_performed.fetch_add(1, Ordering::Relaxed);
}
pub fn record_position_update(&self, latency_ns: u64) {
self.position_update_latency.store(latency_ns, Ordering::Relaxed);
}
pub fn record_limit_breach(&self, latency_ns: u64) {
self.limit_breach_detection_latency.store(latency_ns, Ordering::Relaxed);
self.limit_breaches_detected.fetch_add(1, Ordering::Relaxed);
}
pub fn record_order_rejection(&self) {
self.orders_rejected.fetch_add(1, Ordering::Relaxed);
}
pub fn record_position_liquidation(&self) {
self.positions_liquidated.fetch_add(1, Ordering::Relaxed);
}
pub fn record_emergency_stop(&self) {
self.emergency_stops_triggered.fetch_add(1, Ordering::Relaxed);
}
pub fn get_summary(&self) -> serde_json::Value {
json!({
"risk_check_latency_ns": self.risk_check_latency.load(Ordering::Relaxed),
"position_update_latency_ns": self.position_update_latency.load(Ordering::Relaxed),
"limit_breach_detection_latency_ns": self.limit_breach_detection_latency.load(Ordering::Relaxed),
"risk_checks_performed": self.risk_checks_performed.load(Ordering::Relaxed),
"orders_rejected": self.orders_rejected.load(Ordering::Relaxed),
"positions_liquidated": self.positions_liquidated.load(Ordering::Relaxed),
"limit_breaches_detected": self.limit_breaches_detected.load(Ordering::Relaxed),
"emergency_stops_triggered": self.emergency_stops_triggered.load(Ordering::Relaxed)
})
}
}
/// Test account for risk enforcement testing
#[derive(Debug, Clone)]
pub struct TestAccount {
pub account_id: String,
pub balance: Decimal,
pub available_balance: Decimal,
pub position_limits: HashMap<String, Decimal>, // Symbol -> Max position size
pub exposure_limit: Decimal,
pub daily_loss_limit: Decimal,
pub max_drawdown_pct: Decimal,
pub leverage_limit: Decimal,
pub positions: HashMap<String, Position>,
pub daily_pnl: Decimal,
pub max_daily_drawdown: Decimal,
}
impl TestAccount {
pub fn new(account_id: &str, balance: Decimal) -> Self {
Self {
account_id: account_id.to_string(),
balance,
available_balance: balance,
position_limits: HashMap::new(),
exposure_limit: balance * Decimal::new(5, 0), // 5x leverage limit
daily_loss_limit: balance * Decimal::new(10, 2), // 10% daily loss limit
max_drawdown_pct: Decimal::new(20, 2), // 20% max drawdown
leverage_limit: Decimal::new(10, 0), // 10x max leverage
positions: HashMap::new(),
daily_pnl: Decimal::ZERO,
max_daily_drawdown: Decimal::ZERO,
}
}
pub fn set_position_limit(&mut self, symbol: &str, limit: Decimal) {
self.position_limits.insert(symbol.to_string(), limit);
}
pub fn get_position_limit(&self, symbol: &str) -> Option<Decimal> {
self.position_limits.get(symbol).copied()
}
pub fn get_current_exposure(&self) -> Decimal {
self.positions.values()
.map(|pos| pos.quantity.abs() * pos.average_price)
.sum()
}
pub fn update_position(&mut self, symbol: &str, quantity: Decimal, price: Decimal) {
let position = self.positions.entry(symbol.to_string()).or_insert_with(|| {
Position {
symbol: symbol.to_string(),
quantity: Decimal::ZERO,
average_price: Decimal::ZERO,
unrealized_pnl: Decimal::ZERO,
realized_pnl: Decimal::ZERO,
}
});
// Update position quantity and average price
if position.quantity.is_zero() {
position.quantity = quantity;
position.average_price = price;
} else if position.quantity.is_sign_positive() == quantity.is_sign_positive() {
// Adding to position
let total_cost = position.quantity * position.average_price + quantity * price;
position.quantity += quantity;
if !position.quantity.is_zero() {
position.average_price = total_cost / position.quantity;
}
} else {
// Reducing or reversing position
let reduction = quantity.abs().min(position.quantity.abs());
let realized = reduction * (price - position.average_price) *
if position.quantity.is_sign_positive() { Decimal::ONE } else { -Decimal::ONE };
position.realized_pnl += realized;
position.quantity += quantity;
if position.quantity.is_zero() {
position.average_price = Decimal::ZERO;
}
}
}
}
/// Position information
#[derive(Debug, Clone)]
pub struct Position {
pub symbol: String,
pub quantity: Decimal,
pub average_price: Decimal,
pub unrealized_pnl: Decimal,
pub realized_pnl: Decimal,
}
impl RiskEnforcementTests {
/// Create new risk enforcement tests instance
pub async fn new(config: IntegrationTestConfig) -> TliResult<Self> {
let test_env = TestEnvironment::new(config.clone()).await?;
// Initialize mock services
let mock_trading_service = MockTradingService::new().await?;
let mock_risk_service = MockRiskService::new().await?;
let test_db = TestDatabaseManager::new(&config.test_db_url).await?;
// Initialize risk management components
let risk_config = RiskManagerConfig {
max_position_check_latency_ns: config.max_risk_latency_ns,
enable_real_time_monitoring: true,
position_limit_buffer_pct: Decimal::new(5, 2), // 5% buffer
exposure_limit_buffer_pct: Decimal::new(10, 2), // 10% buffer
emergency_liquidation_threshold_pct: Decimal::new(95, 2), // 95% of limit
};
let risk_manager = Arc::new(RiskManager::new(risk_config).await?);
let position_tracker = Arc::new(PositionTracker::new().await?);
// Create TLI client suite
let client_suite = TliClientBuilder::new()
.with_service_endpoint(
"trading_service".to_string(),
format!("http://localhost:{}", mock_trading_service.port())
)
.with_service_endpoint(
"risk_service".to_string(),
format!("http://localhost:{}", mock_risk_service.port())
)
.with_trading_config(TradingClientConfig::default())
.build()
.await?;
Ok(Self {
client_suite,
mock_trading_service,
mock_risk_service,
test_db,
risk_manager,
position_tracker,
metrics: Arc::new(RiskMetrics::new()),
config,
test_accounts: Arc::new(RwLock::new(HashMap::new())),
})
}
/// Test position limit enforcement
pub async fn test_position_limit_enforcement(&mut self) -> TliResult<TestResult> {
let mut test_result = TestResult::new("position_limit_enforcement");
let start_time = Instant::now();
println!("🔄 Testing position limit enforcement...");
// Setup test account with position limits
let account_id = "POSITION_LIMIT_TEST";
let mut test_account = TestAccount::new(account_id, Decimal::new(100000, 0)); // $100,000
test_account.set_position_limit("AAPL", Decimal::new(1000, 0)); // 1,000 shares max
test_account.set_position_limit("GOOGL", Decimal::new(100, 0)); // 100 shares max
// Register account with risk manager
self.risk_manager.register_account(account_id, &test_account).await?;
self.test_accounts.write().await.insert(account_id.to_string(), test_account);
// Test 1: Order within position limit should be accepted
let within_limit_start = Instant::now();
let within_limit_order = SubmitOrderRequest {
symbol: "AAPL".to_string(),
side: OrderSide::Buy as i32,
order_type: OrderType::Market as i32,
quantity: 500.0, // Within 1,000 limit
client_order_id: "within_limit_order".to_string(),
account_id: Some(account_id.to_string()),
..Default::default()
};
let within_limit_result = if let Some(trading_client) = &self.client_suite.trading_client {
trading_client.submit_order(within_limit_order).await
} else {
return Err(TliError::InternalError("Trading client not available".to_string()));
};
let risk_check_latency = within_limit_start.elapsed().as_nanos() as u64;
self.metrics.record_risk_check(risk_check_latency);
test_result.add_assertion(
"Order within position limit accepted",
within_limit_result.is_ok()
);
test_result.add_assertion(
&format!("Risk check latency < {}µs (got {}ns)",
self.config.max_risk_latency_ns / 1000, risk_check_latency),
risk_check_latency < self.config.max_risk_latency_ns
);
// Update position after successful order
if within_limit_result.is_ok() {
let mut accounts = self.test_accounts.write().await;
if let Some(account) = accounts.get_mut(account_id) {
account.update_position("AAPL", Decimal::new(500, 0), Decimal::new(150, 0));
}
}
// Test 2: Order that would exceed position limit should be rejected
let exceed_limit_start = Instant::now();
let exceed_limit_order = SubmitOrderRequest {
symbol: "AAPL".to_string(),
side: OrderSide::Buy as i32,
order_type: OrderType::Market as i32,
quantity: 600.0, // Would exceed 1,000 limit (500 existing + 600 = 1,100)
client_order_id: "exceed_limit_order".to_string(),
account_id: Some(account_id.to_string()),
..Default::default()
};
let exceed_limit_result = if let Some(trading_client) = &self.client_suite.trading_client {
trading_client.submit_order(exceed_limit_order).await
} else {
return Err(TliError::InternalError("Trading client not available".to_string()));
};
let rejection_latency = exceed_limit_start.elapsed().as_nanos() as u64;
self.metrics.record_risk_check(rejection_latency);
let order_rejected = exceed_limit_result.is_err();
if order_rejected {
self.metrics.record_order_rejection();
}
test_result.add_assertion(
"Order exceeding position limit rejected",
order_rejected
);
test_result.add_assertion(
&format!("Risk rejection latency < {}µs (got {}ns)",
self.config.max_risk_latency_ns / 1000, rejection_latency),
rejection_latency < self.config.max_risk_latency_ns
);
// Test 3: Order that exactly reaches limit should be accepted
let exact_limit_order = SubmitOrderRequest {
symbol: "AAPL".to_string(),
side: OrderSide::Buy as i32,
order_type: OrderType::Market as i32,
quantity: 500.0, // Exactly reaches 1,000 limit
client_order_id: "exact_limit_order".to_string(),
account_id: Some(account_id.to_string()),
..Default::default()
};
let exact_limit_result = if let Some(trading_client) = &self.client_suite.trading_client {
trading_client.submit_order(exact_limit_order).await
} else {
return Err(TliError::InternalError("Trading client not available".to_string()));
};
test_result.add_assertion(
"Order exactly at position limit accepted",
exact_limit_result.is_ok()
);
test_result.set_passed(test_result.assertions.iter().all(|a| a.passed));
test_result.execution_time = start_time.elapsed();
// Store position limit test metadata
test_result.metadata.insert("within_limit_latency_ns".to_string(), json!(risk_check_latency));
test_result.metadata.insert("rejection_latency_ns".to_string(), json!(rejection_latency));
test_result.metadata.insert("orders_rejected".to_string(), json!(self.metrics.orders_rejected.load(Ordering::Relaxed)));
println!("✅ Position limit enforcement test completed");
Ok(test_result)
}
/// Test exposure limit enforcement
pub async fn test_exposure_limit_enforcement(&mut self) -> TliResult<TestResult> {
let mut test_result = TestResult::new("exposure_limit_enforcement");
let start_time = Instant::now();
println!("🔄 Testing exposure limit enforcement...");
// Setup test account with exposure limits
let account_id = "EXPOSURE_LIMIT_TEST";
let balance = Decimal::new(50000, 0); // $50,000
let mut test_account = TestAccount::new(account_id, balance);
test_account.exposure_limit = balance * Decimal::new(3, 0); // 3x leverage = $150,000 max exposure
self.risk_manager.register_account(account_id, &test_account).await?;
self.test_accounts.write().await.insert(account_id.to_string(), test_account);
// Test 1: Build position within exposure limit
let symbols_and_prices = vec![
("AAPL", 150.0, 300.0), // $45,000 exposure
("GOOGL", 2500.0, 20.0), // $50,000 exposure
];
let mut total_exposure = Decimal::ZERO;
for (symbol, price, quantity) in &symbols_and_prices {
let exposure_start = Instant::now();
let order = SubmitOrderRequest {
symbol: symbol.to_string(),
side: OrderSide::Buy as i32,
order_type: OrderType::Market as i32,
quantity: *quantity,
price: Some(*price),
client_order_id: format!("exposure_order_{}", symbol),
account_id: Some(account_id.to_string()),
..Default::default()
};
if let Some(trading_client) = &self.client_suite.trading_client {
let result = trading_client.submit_order(order).await;
let exposure_check_latency = exposure_start.elapsed().as_nanos() as u64;
self.metrics.record_risk_check(exposure_check_latency);
if result.is_ok() {
total_exposure += Decimal::new(*quantity as i64, 0) * Decimal::new((*price * 100.0) as i64, 2);
// Update account position
let mut accounts = self.test_accounts.write().await;
if let Some(account) = accounts.get_mut(account_id) {
account.update_position(symbol, Decimal::new(*quantity as i64, 0), Decimal::new((*price * 100.0) as i64, 2));
}
}
test_result.add_assertion(
&format!("Order for {} within exposure limit accepted", symbol),
result.is_ok()
);
}
}
// Test 2: Order that would exceed exposure limit should be rejected
let exceed_exposure_start = Instant::now();
let exceed_order = SubmitOrderRequest {
symbol: "TSLA".to_string(),
side: OrderSide::Buy as i32,
order_type: OrderType::Market as i32,
quantity: 300.0, // At $200/share = $60,000, would exceed remaining limit
price: Some(200.0),
client_order_id: "exceed_exposure_order".to_string(),
account_id: Some(account_id.to_string()),
..Default::default()
};
let exceed_result = if let Some(trading_client) = &self.client_suite.trading_client {
trading_client.submit_order(exceed_order).await
} else {
return Err(TliError::InternalError("Trading client not available".to_string()));
};
let exposure_rejection_latency = exceed_exposure_start.elapsed().as_nanos() as u64;
self.metrics.record_risk_check(exposure_rejection_latency);
let exposure_order_rejected = exceed_result.is_err();
if exposure_order_rejected {
self.metrics.record_order_rejection();
}
test_result.add_assertion(
"Order exceeding exposure limit rejected",
exposure_order_rejected
);
test_result.add_assertion(
&format!("Exposure check latency < {}µs (got {}ns)",
self.config.max_risk_latency_ns / 1000, exposure_rejection_latency),
exposure_rejection_latency < self.config.max_risk_latency_ns
);
// Test 3: Real-time exposure monitoring
let monitoring_start = Instant::now();
// Simulate price movements that increase exposure
let price_updates = vec![
("AAPL", 160.0), // +6.67% increase
("GOOGL", 2700.0), // +8% increase
];
for (symbol, new_price) in price_updates {
// Update position with new market price
let position_update_start = Instant::now();
let mut accounts = self.test_accounts.write().await;
if let Some(account) = accounts.get_mut(account_id) {
if let Some(position) = account.positions.get_mut(symbol) {
let old_value = position.quantity * position.average_price;
let new_value = position.quantity * Decimal::new((new_price * 100.0) as i64, 2);
position.unrealized_pnl = new_value - old_value;
}
}
let position_update_latency = position_update_start.elapsed().as_nanos() as u64;
self.metrics.record_position_update(position_update_latency);
// Check if exposure limit is breached
let current_exposure = {
let accounts = self.test_accounts.read().await;
accounts.get(account_id).map(|acc| acc.get_current_exposure()).unwrap_or(Decimal::ZERO)
};
let exposure_limit = balance * Decimal::new(3, 0);
if current_exposure > exposure_limit {
let breach_latency = monitoring_start.elapsed().as_nanos() as u64;
self.metrics.record_limit_breach(breach_latency);
}
}
test_result.add_assertion(
"Real-time exposure monitoring active",
self.metrics.risk_checks_performed.load(Ordering::Relaxed) > 0
);
test_result.set_passed(test_result.assertions.iter().all(|a| a.passed));
test_result.execution_time = start_time.elapsed();
println!("✅ Exposure limit enforcement test completed");
Ok(test_result)
}
/// Test drawdown protection mechanisms
pub async fn test_drawdown_protection(&mut self) -> TliResult<TestResult> {
let mut test_result = TestResult::new("drawdown_protection");
let start_time = Instant::now();
println!("🔄 Testing drawdown protection mechanisms...");
// Setup test account with drawdown limits
let account_id = "DRAWDOWN_TEST";
let initial_balance = Decimal::new(100000, 0); // $100,000
let mut test_account = TestAccount::new(account_id, initial_balance);
test_account.daily_loss_limit = initial_balance * Decimal::new(5, 2); // 5% daily loss limit
test_account.max_drawdown_pct = Decimal::new(10, 2); // 10% max drawdown
self.risk_manager.register_account(account_id, &test_account).await?;
self.test_accounts.write().await.insert(account_id.to_string(), test_account);
// Build initial profitable position
let initial_order = SubmitOrderRequest {
symbol: "PROFIT_STOCK".to_string(),
side: OrderSide::Buy as i32,
order_type: OrderType::Market as i32,
quantity: 1000.0,
price: Some(100.0),
client_order_id: "initial_position".to_string(),
account_id: Some(account_id.to_string()),
..Default::default()
};
if let Some(trading_client) = &self.client_suite.trading_client {
let _ = trading_client.submit_order(initial_order).await;
}
// Update position to be profitable initially
{
let mut accounts = self.test_accounts.write().await;
if let Some(account) = accounts.get_mut(account_id) {
account.update_position("PROFIT_STOCK", Decimal::new(1000, 0), Decimal::new(10000, 2));
account.daily_pnl = Decimal::new(5000, 0); // $5,000 profit
}
}
// Simulate adverse price movements causing losses
let loss_scenarios = vec![
("PROFIT_STOCK", 95.0, "2% loss"), // Position value drops to $95,000
("PROFIT_STOCK", 90.0, "5% loss"), // Position value drops to $90,000
("PROFIT_STOCK", 85.0, "8% loss"), // Position value drops to $85,000
];
for (symbol, new_price, scenario) in loss_scenarios {
let drawdown_check_start = Instant::now();
// Update position with loss
let mut current_pnl = Decimal::ZERO;
{
let mut accounts = self.test_accounts.write().await;
if let Some(account) = accounts.get_mut(account_id) {
if let Some(position) = account.positions.get_mut(symbol) {
let new_value = position.quantity * Decimal::new((new_price * 100.0) as i64, 2);
let cost_basis = position.quantity * position.average_price;
position.unrealized_pnl = new_value - cost_basis;
current_pnl = position.unrealized_pnl;
// Update daily PnL
account.daily_pnl = position.unrealized_pnl;
// Track maximum drawdown
if account.daily_pnl < account.max_daily_drawdown {
account.max_daily_drawdown = account.daily_pnl;
}
}
}
}
// Check if drawdown limits are breached
let daily_loss_pct = current_pnl.abs() / initial_balance * Decimal::new(100, 0);
let max_drawdown_pct = {
let accounts = self.test_accounts.read().await;
accounts.get(account_id)
.map(|acc| acc.max_daily_drawdown.abs() / initial_balance * Decimal::new(100, 0))
.unwrap_or(Decimal::ZERO)
};
let drawdown_check_latency = drawdown_check_start.elapsed().as_nanos() as u64;
// Test if new orders are blocked when approaching limits
if daily_loss_pct > Decimal::new(4, 2) { // Above 4% loss
let risk_order = SubmitOrderRequest {
symbol: "RISKY_STOCK".to_string(),
side: OrderSide::Buy as i32,
order_type: OrderType::Market as i32,
quantity: 100.0,
price: Some(50.0),
client_order_id: format!("risk_order_{}", scenario.replace(" ", "_")),
account_id: Some(account_id.to_string()),
..Default::default()
};
let risk_order_result = if let Some(trading_client) = &self.client_suite.trading_client {
trading_client.submit_order(risk_order).await
} else {
return Err(TliError::InternalError("Trading client not available".to_string()));
};
let order_blocked = risk_order_result.is_err();
if order_blocked {
self.metrics.record_order_rejection();
}
test_result.add_assertion(
&format!("New order blocked during {} scenario", scenario),
order_blocked
);
}
// Record breach detection if limits exceeded
if daily_loss_pct > Decimal::new(5, 2) || max_drawdown_pct > Decimal::new(10, 2) {
self.metrics.record_limit_breach(drawdown_check_latency);
test_result.add_assertion(
&format!("Drawdown limit breach detected for {}", scenario),
true
);
}
self.metrics.record_risk_check(drawdown_check_latency);
println!("📊 {} scenario: Daily PnL = {:.2}%, Max Drawdown = {:.2}%",
scenario, daily_loss_pct, max_drawdown_pct);
}
// Test emergency liquidation trigger
let emergency_start = Instant::now();
// Simulate severe loss that triggers emergency liquidation
{
let mut accounts = self.test_accounts.write().await;
if let Some(account) = accounts.get_mut(account_id) {
account.daily_pnl = initial_balance * Decimal::new(-12, 2); // -12% loss (exceeds 10% limit)
account.max_daily_drawdown = account.daily_pnl;
}
}
// Check if emergency stop is triggered
let emergency_triggered = self.risk_manager.check_emergency_conditions(account_id).await?;
if emergency_triggered {
self.metrics.record_emergency_stop();
let emergency_latency = emergency_start.elapsed().as_nanos() as u64;
self.metrics.record_limit_breach(emergency_latency);
}
test_result.add_assertion(
"Emergency liquidation triggered for severe drawdown",
emergency_triggered
);
test_result.add_assertion(
"Drawdown monitoring latency acceptable",
self.metrics.limit_breach_detection_latency.load(Ordering::Relaxed) < self.config.max_risk_latency_ns * 2
);
test_result.set_passed(test_result.assertions.iter().all(|a| a.passed));
test_result.execution_time = start_time.elapsed();
println!("✅ Drawdown protection test completed");
Ok(test_result)
}
/// Test real-time risk monitoring performance
pub async fn test_real_time_risk_monitoring(&mut self) -> TliResult<TestResult> {
let mut test_result = TestResult::new("real_time_risk_monitoring");
let start_time = Instant::now();
println!("🔄 Testing real-time risk monitoring performance...");
// Setup multiple test accounts for stress testing
let account_count = 10;
let orders_per_account = 50;
for i in 0..account_count {
let account_id = format!("MONITOR_TEST_{}", i);
let test_account = TestAccount::new(&account_id, Decimal::new(50000, 0));
self.risk_manager.register_account(&account_id, &test_account).await?;
self.test_accounts.write().await.insert(account_id, test_account);
}
// Generate concurrent order flow to stress test risk monitoring
let mut order_tasks = Vec::new();
let monitoring_start = Instant::now();
for account_idx in 0..account_count {
let account_id = format!("MONITOR_TEST_{}", account_idx);
let client_suite = self.client_suite.clone();
let metrics = Arc::clone(&self.metrics);
let task = tokio::spawn(async move {
let mut successful_orders = 0;
let mut rejected_orders = 0;
for order_idx in 0..orders_per_account {
let order_start = Instant::now();
let order = SubmitOrderRequest {
symbol: format!("STOCK_{}", order_idx % 5),
side: if order_idx % 2 == 0 { OrderSide::Buy as i32 } else { OrderSide::Sell as i32 },
order_type: OrderType::Market as i32,
quantity: 10.0 + (order_idx as f64),
price: Some(100.0 + (order_idx as f64 * 0.1)),
client_order_id: format!("monitor_order_{}_{}", account_idx, order_idx),
account_id: Some(account_id.clone()),
..Default::default()
};
if let Some(trading_client) = &client_suite.trading_client {
match trading_client.submit_order(order).await {
Ok(_) => successful_orders += 1,
Err(_) => rejected_orders += 1,
}
}
let order_latency = order_start.elapsed().as_nanos() as u64;
metrics.record_risk_check(order_latency);
// Small delay to simulate realistic order flow
tokio::time::sleep(Duration::from_millis(1)).await;
}
(successful_orders, rejected_orders)
});
order_tasks.push(task);
}
// Wait for all order tasks to complete
let task_results: Vec<_> = futures::future::join_all(order_tasks).await;
let monitoring_duration = monitoring_start.elapsed();
// Collect results
let mut total_successful = 0;
let mut total_rejected = 0;
let mut task_errors = 0;
for result in task_results {
match result {
Ok((successful, rejected)) => {
total_successful += successful;
total_rejected += rejected;
}
Err(_) => task_errors += 1,
}
}
let total_orders = account_count * orders_per_account;
let total_processed = total_successful + total_rejected;
let processing_rate = total_processed as f64 / monitoring_duration.as_secs_f64();
// Performance assertions
test_result.add_assertion(
&format!("All {} orders processed", total_orders),
total_processed == total_orders && task_errors == 0
);
test_result.add_assertion(
&format!("Processing rate > {} orders/sec (got {:.0})",
self.config.min_throughput_ops_per_sec, processing_rate),
processing_rate > self.config.min_throughput_ops_per_sec
);
let avg_risk_check_latency = self.metrics.risk_check_latency.load(Ordering::Relaxed);
test_result.add_assertion(
&format!("Average risk check latency < {}µs (got {}ns)",
self.config.max_risk_latency_ns / 1000, avg_risk_check_latency),
avg_risk_check_latency < self.config.max_risk_latency_ns
);
test_result.add_assertion(
"Risk monitoring system stable under load",
task_errors == 0
);
test_result.set_passed(test_result.assertions.iter().all(|a| a.passed));
test_result.execution_time = start_time.elapsed();
// Store performance metadata
test_result.metadata.insert("total_orders".to_string(), json!(total_orders));
test_result.metadata.insert("successful_orders".to_string(), json!(total_successful));
test_result.metadata.insert("rejected_orders".to_string(), json!(total_rejected));
test_result.metadata.insert("processing_rate_ops_per_sec".to_string(), json!(processing_rate));
test_result.metadata.insert("avg_latency_ns".to_string(), json!(avg_risk_check_latency));
println!("✅ Real-time risk monitoring test completed: {:.0} orders/sec", processing_rate);
Ok(test_result)
}
/// Run all risk enforcement integration tests
pub async fn run_all_tests(&mut self) -> TliResult<TestSuite> {
let mut test_suite = TestSuite::new("risk_enforcement_integration");
println!("🚀 Starting risk limit enforcement integration tests...");
// Run individual test methods
let tests = vec![
self.test_position_limit_enforcement().await,
self.test_exposure_limit_enforcement().await,
self.test_drawdown_protection().await,
self.test_real_time_risk_monitoring().await,
];
// Collect results
for test_result in tests {
match test_result {
Ok(result) => {
test_suite.add_test_result(result);
}
Err(e) => {
let mut error_result = TestResult::new("risk_enforcement_test_error");
error_result.add_error(format!("Test execution failed: {}", e));
test_suite.add_test_result(error_result);
}
}
}
// Calculate overall success
test_suite.set_passed(test_suite.passed_tests == test_suite.total_tests);
// Add risk metrics to test suite metadata
let metrics_summary = self.metrics.get_summary();
test_suite.metadata.insert("risk_metrics".to_string(), metrics_summary);
println!("🏁 Risk limit enforcement integration tests completed: {}/{} passed",
test_suite.passed_tests, test_suite.total_tests);
Ok(test_suite)
}
}
/// Test result structure
#[derive(Debug, Clone)]
pub struct TestResult {
pub name: String,
pub passed: bool,
pub execution_time: Duration,
pub assertions: Vec<Assertion>,
pub errors: Vec<String>,
pub metadata: HashMap<String, serde_json::Value>,
}
impl TestResult {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
passed: false,
execution_time: Duration::default(),
assertions: Vec::new(),
errors: Vec::new(),
metadata: HashMap::new(),
}
}
pub fn add_assertion(&mut self, description: &str, passed: bool) {
self.assertions.push(Assertion {
description: description.to_string(),
passed,
});
}
pub fn add_error(&mut self, error: String) {
self.errors.push(error);
}
pub fn set_passed(&mut self, passed: bool) {
self.passed = passed;
}
}
/// Individual test assertion
#[derive(Debug, Clone)]
pub struct Assertion {
pub description: String,
pub passed: bool,
}
/// Test suite containing multiple test results
#[derive(Debug, Clone)]
pub struct TestSuite {
pub name: String,
pub tests: Vec<TestResult>,
pub passed_tests: usize,
pub total_tests: usize,
pub passed: bool,
pub execution_time: Duration,
pub metadata: HashMap<String, serde_json::Value>,
}
impl TestSuite {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
tests: Vec::new(),
passed_tests: 0,
total_tests: 0,
passed: false,
execution_time: Duration::default(),
metadata: HashMap::new(),
}
}
pub fn add_test_result(&mut self, test: TestResult) {
if test.passed {
self.passed_tests += 1;
}
self.total_tests += 1;
self.tests.push(test);
}
pub fn set_passed(&mut self, passed: bool) {
self.passed = passed;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_risk_metrics() {
let metrics = RiskMetrics::new();
metrics.record_risk_check(25_000); // 25µs
metrics.record_order_rejection();
metrics.record_limit_breach(50_000); // 50µs
let summary = metrics.get_summary();
assert_eq!(summary["risk_checks_performed"].as_u64().unwrap(), 1);
assert_eq!(summary["orders_rejected"].as_u64().unwrap(), 1);
assert_eq!(summary["limit_breaches_detected"].as_u64().unwrap(), 1);
}
#[test]
fn test_account_position_limits() {
let mut account = TestAccount::new("TEST", Decimal::new(10000, 0));
account.set_position_limit("AAPL", Decimal::new(1000, 0));
assert_eq!(account.get_position_limit("AAPL"), Some(Decimal::new(1000, 0)));
assert_eq!(account.get_position_limit("GOOGL"), None);
}
#[test]
fn test_position_updates() {
let mut account = TestAccount::new("TEST", Decimal::new(10000, 0));
// Initial position
account.update_position("AAPL", Decimal::new(100, 0), Decimal::new(15000, 2));
assert_eq!(account.positions["AAPL"].quantity, Decimal::new(100, 0));
assert_eq!(account.positions["AAPL"].average_price, Decimal::new(15000, 2));
// Add to position
account.update_position("AAPL", Decimal::new(50, 0), Decimal::new(16000, 2));
assert_eq!(account.positions["AAPL"].quantity, Decimal::new(150, 0));
}
}