Move 17 library crates into crates/, CLI binary into bin/fxt, consolidate 10 test crates into testing/, split config crate from deployment config files. Root directory reduced from 38+ to ~17 directories. All Cargo.toml paths and build.rs proto refs updated. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
424 lines
14 KiB
Rust
424 lines
14 KiB
Rust
//! Failure Scenario Integration Tests for Foxhunt HFT System
|
|
//!
|
|
//! This module tests the system's resilience and recovery capabilities under various failure conditions:
|
|
//!
|
|
//! ## Failure Scenarios Covered:
|
|
//! 1. **Kill Switch Activation**: Emergency shutdown procedures
|
|
//! 2. **Trading Gate Guards**: Order processing gates under kill switch
|
|
//! 3. **Scoped Kill Switches**: Symbol/Account/Strategy level blocks
|
|
//!
|
|
//! ## Recovery Validation:
|
|
//! - System gracefully handles all failure modes
|
|
//! - Recovery procedures restore full functionality
|
|
//! - Kill switch activation is immediate
|
|
//! - Trading gates prevent order submission when active
|
|
|
|
#![warn(missing_docs)]
|
|
#![warn(clippy::all)]
|
|
#![allow(clippy::too_many_arguments)]
|
|
#![allow(clippy::type_complexity)]
|
|
#![allow(unused_crate_dependencies)]
|
|
|
|
use std::sync::Arc;
|
|
use std::time::{Duration, Instant};
|
|
use tracing::{info, warn};
|
|
|
|
// Core system imports - use full paths since types aren't re-exported from crate root
|
|
use risk::risk_types::KillSwitchScope;
|
|
use risk::safety::trading_gate::TradingGate;
|
|
use risk::safety::KillSwitchConfig;
|
|
use risk::AtomicKillSwitch;
|
|
|
|
/// Failure scenario test configuration
|
|
#[derive(Debug, Clone)]
|
|
pub struct FailureTestConfig {
|
|
/// Redis connection string for kill switch
|
|
pub redis_url: String,
|
|
/// Test timeout duration
|
|
pub failure_timeout: Duration,
|
|
/// Maximum acceptable activation time
|
|
pub max_activation_time: Duration,
|
|
}
|
|
|
|
impl Default for FailureTestConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
redis_url: std::env::var("TEST_REDIS_URL")
|
|
.unwrap_or_else(|_| "redis://localhost:6379/2".to_string()),
|
|
failure_timeout: Duration::from_secs(30),
|
|
max_activation_time: Duration::from_millis(100),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Failure test harness for managing kill switch scenarios
|
|
pub struct FailureTestHarness {
|
|
config: FailureTestConfig,
|
|
kill_switch: Arc<AtomicKillSwitch>,
|
|
trading_gate: Arc<TradingGate>,
|
|
}
|
|
|
|
impl FailureTestHarness {
|
|
/// Create a new failure test harness
|
|
pub async fn new() -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
|
|
let config = FailureTestConfig::default();
|
|
|
|
info!("Initializing failure scenario test harness");
|
|
|
|
// Initialize kill switch with Redis backend
|
|
let kill_switch_config = KillSwitchConfig::default();
|
|
let kill_switch =
|
|
Arc::new(AtomicKillSwitch::new(kill_switch_config, config.redis_url.clone()).await?);
|
|
|
|
// Initialize trading gate
|
|
let trading_gate = Arc::new(TradingGate::new(kill_switch.clone()));
|
|
|
|
info!("Failure scenario test harness initialized successfully");
|
|
|
|
Ok(Self {
|
|
config,
|
|
kill_switch,
|
|
trading_gate,
|
|
})
|
|
}
|
|
|
|
/// Test kill switch activation and emergency procedures
|
|
pub async fn test_kill_switch_activation(
|
|
&self,
|
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
info!("STARTING: Kill Switch Activation Test");
|
|
|
|
let start_time = Instant::now();
|
|
|
|
// Step 1: Verify normal operations (kill switch inactive)
|
|
info!("Step 1: Verifying normal trading operations...");
|
|
let is_active = self.kill_switch.is_active().await?;
|
|
if is_active {
|
|
return Err("Kill switch should be inactive at start".into());
|
|
}
|
|
|
|
// Verify trading gate allows orders
|
|
let gate_result = self.trading_gate.pre_order_gate("AAPL", None);
|
|
if gate_result.is_err() {
|
|
return Err("Trading gate should allow orders when kill switch inactive".into());
|
|
}
|
|
|
|
// Step 2: Activate kill switch
|
|
info!("Step 2: Activating emergency kill switch...");
|
|
let activation_start = Instant::now();
|
|
self.kill_switch
|
|
.activate(
|
|
KillSwitchScope::Global,
|
|
"Integration test emergency scenario".to_string(),
|
|
"test_user".to_string(),
|
|
false,
|
|
)
|
|
.await?;
|
|
let activation_time = activation_start.elapsed();
|
|
|
|
// Step 3: Verify kill switch is active
|
|
info!("Step 3: Verifying kill switch activation...");
|
|
let is_active = self.kill_switch.is_active().await?;
|
|
if !is_active {
|
|
return Err("Kill switch should be active after activation".into());
|
|
}
|
|
|
|
// Step 4: Verify all trading halts immediately
|
|
info!("Step 4: Verifying immediate trading halt...");
|
|
let gate_result = self.trading_gate.pre_order_gate("AAPL", None);
|
|
if gate_result.is_ok() {
|
|
return Err("Trading gate should block orders when kill switch active".into());
|
|
}
|
|
|
|
// Verify emergency check
|
|
if self.trading_gate.emergency_check() {
|
|
return Err("Emergency check should return false when kill switch active".into());
|
|
}
|
|
|
|
// Step 5: Test recovery from kill switch
|
|
info!("Step 5: Testing recovery from kill switch...");
|
|
self.kill_switch
|
|
.deactivate(KillSwitchScope::Global, "Test completed".to_string())
|
|
.await?;
|
|
|
|
let is_active = self.kill_switch.is_active().await?;
|
|
if is_active {
|
|
return Err("Kill switch should be inactive after deactivation".into());
|
|
}
|
|
|
|
// Verify trading gate allows orders again
|
|
let gate_result = self.trading_gate.pre_order_gate("AAPL", None);
|
|
if gate_result.is_err() {
|
|
return Err("Trading gate should allow orders after kill switch deactivation".into());
|
|
}
|
|
|
|
let test_duration = start_time.elapsed();
|
|
|
|
info!("KILL SWITCH ACTIVATION TEST COMPLETED");
|
|
info!(
|
|
" Activation Time: {:?} (target: <{:?})",
|
|
activation_time, self.config.max_activation_time
|
|
);
|
|
info!(" Total Duration: {:?}", test_duration);
|
|
|
|
if activation_time > self.config.max_activation_time {
|
|
warn!(
|
|
"Kill switch activation took longer than target: {:?} > {:?}",
|
|
activation_time, self.config.max_activation_time
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test scoped kill switch activation (symbol-level)
|
|
pub async fn test_scoped_kill_switch(
|
|
&self,
|
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
info!("STARTING: Scoped Kill Switch Test");
|
|
|
|
// Step 1: Activate kill switch for specific symbol
|
|
info!("Step 1: Activating kill switch for AAPL...");
|
|
self.kill_switch
|
|
.activate(
|
|
KillSwitchScope::Symbol("AAPL".to_string()),
|
|
"Test symbol-level block".to_string(),
|
|
"test_user".to_string(),
|
|
false,
|
|
)
|
|
.await?;
|
|
|
|
// Step 2: Verify AAPL is blocked
|
|
info!("Step 2: Verifying AAPL is blocked...");
|
|
let aapl_result = self.trading_gate.pre_order_gate("AAPL", None);
|
|
if aapl_result.is_ok() {
|
|
return Err("AAPL should be blocked by scoped kill switch".into());
|
|
}
|
|
|
|
// Step 3: Verify MSFT is still allowed
|
|
info!("Step 3: Verifying MSFT is still allowed...");
|
|
let msft_result = self.trading_gate.pre_order_gate("MSFT", None);
|
|
if msft_result.is_err() {
|
|
return Err("MSFT should not be affected by AAPL kill switch".into());
|
|
}
|
|
|
|
// Step 4: Deactivate scoped kill switch
|
|
info!("Step 4: Deactivating AAPL kill switch...");
|
|
self.kill_switch
|
|
.deactivate(
|
|
KillSwitchScope::Symbol("AAPL".to_string()),
|
|
"Test completed".to_string(),
|
|
)
|
|
.await?;
|
|
|
|
// Step 5: Verify AAPL is allowed again
|
|
info!("Step 5: Verifying AAPL is allowed again...");
|
|
let aapl_result = self.trading_gate.pre_order_gate("AAPL", None);
|
|
if aapl_result.is_err() {
|
|
return Err("AAPL should be allowed after deactivation".into());
|
|
}
|
|
|
|
info!("SCOPED KILL SWITCH TEST COMPLETED");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test trading gate performance under normal conditions
|
|
pub async fn test_trading_gate_performance(
|
|
&self,
|
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
info!("STARTING: Trading Gate Performance Test");
|
|
|
|
let num_checks = 10_000;
|
|
let start_time = Instant::now();
|
|
|
|
for _ in 0..num_checks {
|
|
let _ = self.trading_gate.pre_order_gate("AAPL", None);
|
|
}
|
|
|
|
let total_duration = start_time.elapsed();
|
|
let avg_latency_ns = total_duration.as_nanos() / num_checks;
|
|
|
|
info!("TRADING GATE PERFORMANCE TEST COMPLETED");
|
|
info!(" Total Checks: {}", num_checks);
|
|
info!(" Total Duration: {:?}", total_duration);
|
|
info!(" Average Latency: {}ns", avg_latency_ns);
|
|
|
|
// HFT compliance: each check should be sub-microsecond
|
|
if avg_latency_ns > 1000 {
|
|
warn!(
|
|
"Trading gate latency exceeds 1 microsecond target: {}ns",
|
|
avg_latency_ns
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test multiple concurrent gate checks
|
|
pub async fn test_concurrent_gate_checks(
|
|
&self,
|
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
info!("STARTING: Concurrent Gate Checks Test");
|
|
|
|
let num_tasks = 100;
|
|
let checks_per_task = 1000;
|
|
|
|
let mut handles = Vec::new();
|
|
|
|
for task_id in 0..num_tasks {
|
|
let gate = self.trading_gate.clone();
|
|
let handle = tokio::spawn(async move {
|
|
for _ in 0..checks_per_task {
|
|
let _ = gate.pre_order_gate("AAPL", None);
|
|
}
|
|
task_id
|
|
});
|
|
handles.push(handle);
|
|
}
|
|
|
|
// Wait for all tasks to complete
|
|
for handle in handles {
|
|
handle.await?;
|
|
}
|
|
|
|
info!("CONCURRENT GATE CHECKS TEST COMPLETED");
|
|
info!(" Tasks: {}", num_tasks);
|
|
info!(" Checks per Task: {}", checks_per_task);
|
|
info!(" Total Checks: {}", num_tasks * checks_per_task);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test batch symbol gate checking
|
|
pub async fn test_batch_symbol_gate(
|
|
&self,
|
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
info!("STARTING: Batch Symbol Gate Test");
|
|
|
|
let symbols = vec![
|
|
"AAPL".to_string(),
|
|
"GOOGL".to_string(),
|
|
"MSFT".to_string(),
|
|
"TSLA".to_string(),
|
|
];
|
|
|
|
// Step 1: Test with no kill switches
|
|
info!("Step 1: Testing batch gate with no kill switches...");
|
|
let allowed = self.trading_gate.batch_symbol_gate(&symbols)?;
|
|
if allowed.len() != symbols.len() {
|
|
return Err(format!(
|
|
"Expected {} allowed symbols, got {}",
|
|
symbols.len(),
|
|
allowed.len()
|
|
)
|
|
.into());
|
|
}
|
|
|
|
// Step 2: Activate kill switch for AAPL
|
|
info!("Step 2: Activating kill switch for AAPL...");
|
|
self.kill_switch
|
|
.activate(
|
|
KillSwitchScope::Symbol("AAPL".to_string()),
|
|
"Test batch filtering".to_string(),
|
|
"test_user".to_string(),
|
|
false,
|
|
)
|
|
.await?;
|
|
|
|
// Step 3: Test batch gate with one symbol blocked
|
|
info!("Step 3: Testing batch gate with AAPL blocked...");
|
|
let allowed = self.trading_gate.batch_symbol_gate(&symbols)?;
|
|
if allowed.len() != symbols.len() - 1 {
|
|
return Err(format!(
|
|
"Expected {} allowed symbols, got {}",
|
|
symbols.len() - 1,
|
|
allowed.len()
|
|
)
|
|
.into());
|
|
}
|
|
if allowed.contains(&"AAPL".to_string()) {
|
|
return Err("AAPL should not be in allowed symbols".into());
|
|
}
|
|
|
|
// Step 4: Cleanup
|
|
info!("Step 4: Cleaning up...");
|
|
self.kill_switch
|
|
.deactivate(
|
|
KillSwitchScope::Symbol("AAPL".to_string()),
|
|
"Test completed".to_string(),
|
|
)
|
|
.await?;
|
|
|
|
info!("BATCH SYMBOL GATE TEST COMPLETED");
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
// Integration test runner
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[tokio::test]
|
|
async fn test_kill_switch_activation() {
|
|
let harness = FailureTestHarness::new()
|
|
.await
|
|
.expect("Failed to initialize failure test harness");
|
|
|
|
harness
|
|
.test_kill_switch_activation()
|
|
.await
|
|
.expect("Kill switch activation test failed");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_scoped_kill_switch() {
|
|
let harness = FailureTestHarness::new()
|
|
.await
|
|
.expect("Failed to initialize failure test harness");
|
|
|
|
harness
|
|
.test_scoped_kill_switch()
|
|
.await
|
|
.expect("Scoped kill switch test failed");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_trading_gate_performance() {
|
|
let harness = FailureTestHarness::new()
|
|
.await
|
|
.expect("Failed to initialize failure test harness");
|
|
|
|
harness
|
|
.test_trading_gate_performance()
|
|
.await
|
|
.expect("Trading gate performance test failed");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_concurrent_gate_checks() {
|
|
let harness = FailureTestHarness::new()
|
|
.await
|
|
.expect("Failed to initialize failure test harness");
|
|
|
|
harness
|
|
.test_concurrent_gate_checks()
|
|
.await
|
|
.expect("Concurrent gate checks test failed");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_batch_symbol_gate() {
|
|
let harness = FailureTestHarness::new()
|
|
.await
|
|
.expect("Failed to initialize failure test harness");
|
|
|
|
harness
|
|
.test_batch_symbol_gate()
|
|
.await
|
|
.expect("Batch symbol gate test failed");
|
|
}
|
|
}
|