//! Predefined Test Scenarios for Foxhunt HFT Trading System //! //! This module provides comprehensive test scenarios for various trading, //! risk management, and market conditions that the system needs to handle. //! //! ## Usage //! //! ```rust //! use tests::fixtures::scenarios::*; //! //! // Get a basic trading scenario //! let scenario = BasicTradingScenario::new(); //! let positions = scenario.create_positions(); //! //! // Get a stress test scenario //! let stress_scenario = MarketCrashScenario::new(); //! let shocks = stress_scenario.generate_market_shocks(); //! //! // Get a high frequency scenario //! let hft_scenario = HighFrequencyScenario::new(); //! let orders = hft_scenario.generate_order_flow(1000); //! ``` use chrono::{DateTime, Duration as ChronoDuration, Utc}; use rust_decimal::prelude::ToPrimitive; use rust_decimal::Decimal; use std::collections::HashMap; use uuid::Uuid; // Import types from risk crate use risk::risk_types::Position; // Note: StressScenario imported from mod.rs (risk_data::models version) use super::builders::*; use super::*; use crate::fixtures::helpers::ToDecimal; // ============================================================================= // BASIC TRADING SCENARIOS // ============================================================================= /// Basic trading scenario with mixed positions #[derive(Debug, Clone)] pub struct BasicTradingScenario { pub portfolio_id: String, pub base_currency: String, pub total_value: Decimal, } impl Default for BasicTradingScenario { fn default() -> Self { Self::new() } } impl BasicTradingScenario { pub fn new() -> Self { Self { portfolio_id: TEST_PORTFOLIO_1.to_string(), base_currency: "USD".to_string(), total_value: Decimal::from(1000000), // $1M portfolio } } pub fn with_portfolio_id(mut self, portfolio_id: impl Into) -> Self { self.portfolio_id = portfolio_id.into(); self } pub fn with_total_value(mut self, value: Decimal) -> Self { self.total_value = value; self } /// Create a portfolio with the scenario settings pub fn create_portfolio(&self) -> Portfolio { PortfolioBuilder::new() .with_id(&self.portfolio_id) .with_name("Basic Trading Portfolio") .with_base_currency(&self.base_currency) .strategy_portfolio() .build() } /// Create diverse positions across asset classes pub fn create_positions(&self) -> Vec { let symbols_and_weights = vec![ (TEST_EQUITY_1, 0.30), // 30% large cap equity (TEST_EQUITY_2, 0.20), // 20% mid cap equity (TEST_FOREX_1, 0.15), // 15% major FX pair (TEST_FUTURE_1, 0.10), // 10% equity futures (TEST_BOND_1, 0.15), // 15% government bonds (TEST_COMMODITY_1, 0.10), // 10% gold commodity ]; symbols_and_weights .into_iter() .map(|(symbol, weight)| { let position_value = self.total_value * Decimal::try_from(weight).unwrap(); let price = get_test_price_for_symbol(symbol).to_decimal(); let quantity = position_value / price; PositionBuilder::new() .with_portfolio_id(&self.portfolio_id) .with_symbol(symbol) .with_quantity(quantity) .with_average_price(price) .with_market_price(price) .with_weight(Decimal::try_from(weight).unwrap()) .build() }) .collect() } /// Create corresponding instruments for all positions pub fn create_instruments(&self) -> Vec { vec![ InstrumentBuilder::new() .with_symbol(TEST_EQUITY_1) .equity() .build(), InstrumentBuilder::new() .with_symbol(TEST_EQUITY_2) .equity() .build(), InstrumentBuilder::new() .with_symbol(TEST_FOREX_1) .currency() .build(), InstrumentBuilder::new() .with_symbol(TEST_FUTURE_1) .future() .build(), InstrumentBuilder::new() .with_symbol(TEST_BOND_1) .bond() .build(), InstrumentBuilder::new() .with_symbol(TEST_COMMODITY_1) .commodity() .build(), ] } } // ============================================================================= // STRESS TEST SCENARIOS // ============================================================================= /// Market crash stress test scenario #[derive(Debug, Clone)] pub struct MarketCrashScenario { pub name: String, pub description: String, pub equity_shock: Decimal, // -30% pub bond_shock: Decimal, // +5% (flight to quality) pub commodity_shock: Decimal, // -20% pub fx_shock: Decimal, // +10% USD strength pub volatility_shock: Decimal, // +200% volatility increase } impl Default for MarketCrashScenario { fn default() -> Self { Self::new() } } impl MarketCrashScenario { pub fn new() -> Self { Self { name: "Market Crash 2008 Style".to_string(), description: "Severe market downturn with flight to quality".to_string(), equity_shock: Decimal::new(-30, 2), // -30% bond_shock: Decimal::new(5, 2), // +5% commodity_shock: Decimal::new(-20, 2), // -20% fx_shock: Decimal::new(10, 2), // +10% volatility_shock: Decimal::new(200, 2), // +200% } } /// Generate market shocks for all asset classes pub fn generate_market_shocks(&self) -> HashMap { let mut shocks = HashMap::new(); shocks.insert(AssetClass::Equities, self.equity_shock); shocks.insert(AssetClass::FixedIncome, self.bond_shock); shocks.insert(AssetClass::Commodities, self.commodity_shock); shocks.insert(AssetClass::Currencies, self.fx_shock); shocks.insert(AssetClass::Derivatives, self.equity_shock); // Correlate with equities shocks.insert(AssetClass::Alternatives, self.equity_shock); // Correlate with equities shocks } /// Apply shocks to a list of positions pub fn apply_shocks_to_positions(&self, positions: &[Position]) -> Vec { let shocks = self.generate_market_shocks(); positions .iter() .map(|pos| { let asset_class = self.get_asset_class_for_symbol(&pos.symbol); let shock = shocks.get(&asset_class).unwrap_or(&Decimal::ZERO); let shock_multiplier = 1.0 + shock.to_f64().unwrap_or(0.0); let new_market_price = pos.market_price * shock_multiplier; Position { market_price: new_market_price, market_value: pos.quantity * new_market_price, unrealized_pnl: (new_market_price - pos.average_cost) * pos.quantity, last_updated: Utc::now().timestamp(), ..pos.clone() } }) .collect() } /// Create a formal stress test scenario record pub fn create_stress_scenario(&self) -> risk::risk_types::StressScenario { let mut price_shocks = HashMap::new(); price_shocks.insert( "EQUITY".to_string(), self.equity_shock.to_f64().unwrap_or(0.0), ); price_shocks.insert("BOND".to_string(), self.bond_shock.to_f64().unwrap_or(0.0)); price_shocks.insert( "COMMODITY".to_string(), self.commodity_shock.to_f64().unwrap_or(0.0), ); price_shocks.insert("FX".to_string(), self.fx_shock.to_f64().unwrap_or(0.0)); risk::risk_types::StressScenario { id: Uuid::new_v4().to_string(), name: self.name.clone(), price_shocks: price_shocks.clone(), market_shocks: price_shocks, volatility_multiplier: 1.0 + self.volatility_shock.to_f64().unwrap_or(0.0), volatility_multipliers: HashMap::new(), correlation_changes: HashMap::new(), correlation_adjustments: HashMap::new(), liquidity_haircuts: HashMap::new(), } } fn get_asset_class_for_symbol(&self, symbol: &str) -> AssetClass { if symbol.starts_with("TEST_EQ_") { AssetClass::Equities } else if symbol.starts_with("TEST_FX_") { AssetClass::Currencies } else if symbol.starts_with("TEST_FUT_") { AssetClass::Derivatives } else if symbol.starts_with("TEST_BOND_") { AssetClass::FixedIncome } else if symbol.starts_with("TEST_COMM_") { AssetClass::Commodities } else if symbol.starts_with("TEST_CRYPTO_") { AssetClass::Alternatives } else { AssetClass::Equities // Default } } } /// Interest rate shock scenario #[derive(Debug, Clone)] pub struct InterestRateShockScenario { pub name: String, pub description: String, pub rate_shock: Decimal, // +200 basis points pub duration_impact: Decimal, // -10% for 10 year duration } impl Default for InterestRateShockScenario { fn default() -> Self { Self::new() } } impl InterestRateShockScenario { pub fn new() -> Self { Self { name: "Interest Rate Shock".to_string(), description: "200bp parallel shift in yield curve".to_string(), rate_shock: Decimal::new(200, 4), // 2.00% = 200 basis points duration_impact: Decimal::new(-10, 2), // -10% } } /// Apply duration-based shock to bond positions pub fn apply_duration_shock(&self, positions: &[Position]) -> Vec { positions .iter() .map(|pos| { if pos.symbol.starts_with("TEST_BOND_") { // Duration not stored in Position, use default 5 year duration for bonds let duration = 5.0; let price_impact = -duration * self.rate_shock.to_f64().unwrap_or(0.0); // Duration × rate change let shock_multiplier = 1.0 + (price_impact / 100.0); let new_market_price = pos.market_price * shock_multiplier; Position { market_price: new_market_price, market_value: pos.quantity * new_market_price, unrealized_pnl: (new_market_price - pos.average_cost) * pos.quantity, last_updated: Utc::now().timestamp(), ..pos.clone() } } else { pos.clone() } }) .collect() } } // ============================================================================= // HIGH FREQUENCY TRADING SCENARIOS // ============================================================================= /// High frequency trading scenario with rapid order flow #[derive(Debug, Clone)] pub struct HighFrequencyScenario { pub symbol: String, pub base_price: Decimal, pub tick_size: Decimal, pub order_rate_per_second: usize, pub volatility: Decimal, } impl Default for HighFrequencyScenario { fn default() -> Self { Self::new() } } impl HighFrequencyScenario { pub fn new() -> Self { Self { symbol: TEST_EQUITY_1.to_string(), base_price: Decimal::from(100), tick_size: Decimal::new(1, 2), // $0.01 order_rate_per_second: 1000, volatility: Decimal::new(2, 2), // 2% volatility } } pub fn with_symbol(mut self, symbol: impl Into) -> Self { self.symbol = symbol.into(); self } pub fn with_order_rate(mut self, rate: usize) -> Self { self.order_rate_per_second = rate; self } /// Generate rapid order flow for testing pub fn generate_order_flow(&self, duration_seconds: u64) -> Vec { let total_orders = (duration_seconds as usize) * self.order_rate_per_second; let mut orders = Vec::with_capacity(total_orders); let start_time = Utc::now(); for i in 0..total_orders { let timestamp = start_time + ChronoDuration::milliseconds( (i as i64 * 1000) / self.order_rate_per_second as i64, ); let side = if i % 2 == 0 { OrderSide::Buy } else { OrderSide::Sell }; let price_offset = (i % 10) as i64 - 5; // -5 to +5 ticks let price = self.base_price + (self.tick_size * Decimal::from(price_offset)); let quantity = Decimal::from(100 + (i % 900)); // 100 to 1000 shares orders.push(TestOrder { id: Uuid::new_v4(), symbol: self.symbol.clone(), side, quantity, price, order_type: OrderType::Limit, timestamp, time_in_force: TimeInForce::Day, }); } orders } /// Generate market data tick stream pub fn generate_market_ticks(&self, count: usize) -> Vec { let mut ticks = Vec::with_capacity(count); let mut current_price = self.base_price; let start_time = Utc::now(); for i in 0..count { let timestamp = start_time + ChronoDuration::microseconds(i as i64 * 1000); // 1ms intervals // Random walk price movement let price_change = if i % 3 == 0 { self.tick_size } else if i % 3 == 1 { -self.tick_size } else { Decimal::ZERO }; current_price += price_change; ticks.push(MarketTick { symbol: self.symbol.clone(), timestamp, bid: current_price - self.tick_size, ask: current_price + self.tick_size, last: current_price, volume: Decimal::from(100 + (i % 1000)), sequence: i as u64, }); } ticks } } // ============================================================================= // RISK MANAGEMENT SCENARIOS // ============================================================================= /// Risk limit breach scenario #[derive(Debug, Clone)] pub struct RiskLimitBreachScenario { pub portfolio_id: String, pub var_limit: Decimal, pub position_limit: Decimal, pub concentration_limit: Decimal, } impl Default for RiskLimitBreachScenario { fn default() -> Self { Self::new() } } impl RiskLimitBreachScenario { pub fn new() -> Self { Self { portfolio_id: TEST_PORTFOLIO_1.to_string(), var_limit: Decimal::from(100000), // $100k VaR limit position_limit: Decimal::from(1000000), // $1M position limit concentration_limit: Decimal::new(25, 2), // 25% concentration limit } } /// Create positions that breach concentration limits pub fn create_concentrated_positions(&self) -> Vec { let _total_portfolio_value = Decimal::from(1000000); vec![ // Concentrated position - 40% of portfolio (breaches 25% limit) PositionBuilder::new() .with_portfolio_id(&self.portfolio_id) .with_symbol(TEST_EQUITY_1) .with_quantity(Decimal::from(4000)) .with_average_price(Decimal::from(100)) .with_market_price(Decimal::from(100)) .with_weight(Decimal::new(40, 2)) .build(), // Normal positions PositionBuilder::new() .with_portfolio_id(&self.portfolio_id) .with_symbol(TEST_EQUITY_2) .with_quantity(Decimal::from(3000)) .with_average_price(Decimal::from(100)) .with_market_price(Decimal::from(100)) .with_weight(Decimal::new(30, 2)) .build(), PositionBuilder::new() .with_portfolio_id(&self.portfolio_id) .with_symbol(TEST_EQUITY_3) .with_quantity(Decimal::from(3000)) .with_average_price(Decimal::from(100)) .with_market_price(Decimal::from(100)) .with_weight(Decimal::new(30, 2)) .build(), ] } /// Create positions that would breach VaR limits under stress pub fn create_high_var_positions(&self) -> Vec { // High beta, high volatility positions vec![ PositionBuilder::new() .with_portfolio_id(&self.portfolio_id) .with_symbol(TEST_EQUITY_1) .with_quantity(Decimal::from(5000)) .with_average_price(Decimal::from(100)) .with_market_price(Decimal::from(100)) .with_beta(Decimal::new(20, 1)) // Beta of 2.0 .build(), PositionBuilder::new() .with_portfolio_id(&self.portfolio_id) .with_symbol(TEST_EQUITY_2) .with_quantity(Decimal::from(3000)) .with_average_price(Decimal::from(100)) .with_market_price(Decimal::from(100)) .with_beta(Decimal::new(18, 1)) // Beta of 1.8 .build(), ] } } // ============================================================================= // SUPPORTING DATA STRUCTURES // ============================================================================= /// Test order structure for order flow scenarios #[derive(Debug, Clone)] pub struct TestOrder { pub id: Uuid, pub symbol: String, pub side: OrderSide, pub quantity: Decimal, pub price: Decimal, pub order_type: OrderType, pub timestamp: DateTime, pub time_in_force: TimeInForce, } /// Market tick data for price feed scenarios #[derive(Debug, Clone)] pub struct MarketTick { pub symbol: String, pub timestamp: DateTime, pub bid: Decimal, pub ask: Decimal, pub last: Decimal, pub volume: Decimal, pub sequence: u64, } /// Enums for order testing #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum OrderSide { Buy, Sell, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum OrderType { Market, Limit, Stop, StopLimit, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TimeInForce { Day, GoodTillCanceled, ImmediateOrCancel, FillOrKill, } // ============================================================================= // SCENARIO FACTORY // ============================================================================= /// Factory for creating predefined test scenarios #[derive(Debug)] pub struct ScenarioFactory; impl ScenarioFactory { /// Create a basic balanced portfolio scenario pub fn basic_portfolio() -> (Portfolio, Vec, Vec) { let scenario = BasicTradingScenario::new(); let portfolio = scenario.create_portfolio(); let instruments = scenario.create_instruments(); let positions = scenario.create_positions(); (portfolio, instruments, positions) } /// Create a market crash stress test scenario pub fn market_crash() -> (risk::risk_types::StressScenario, Vec) { let crash_scenario = MarketCrashScenario::new(); let basic_scenario = BasicTradingScenario::new(); let original_positions = basic_scenario.create_positions(); let stressed_positions = crash_scenario.apply_shocks_to_positions(&original_positions); let stress_scenario = crash_scenario.create_stress_scenario(); (stress_scenario, stressed_positions) } /// Create a high frequency trading scenario pub fn high_frequency_trading(duration_seconds: u64) -> (Vec, Vec) { let hft_scenario = HighFrequencyScenario::new(); let orders = hft_scenario.generate_order_flow(duration_seconds); let ticks = hft_scenario.generate_market_ticks((duration_seconds * 1000) as usize); // 1 tick per ms (orders, ticks) } /// Create a risk limit breach scenario pub fn risk_limit_breach() -> (Portfolio, Vec) { let risk_scenario = RiskLimitBreachScenario::new(); let portfolio = PortfolioBuilder::new() .with_id(&risk_scenario.portfolio_id) .with_var_limit(risk_scenario.var_limit) .build(); let positions = risk_scenario.create_concentrated_positions(); (portfolio, positions) } /// Create a multi-asset diversified scenario pub fn multi_asset_diversified() -> (Portfolio, Vec, Vec) { let portfolio = PortfolioBuilder::new() .with_id("MULTI_ASSET_PORTFOLIO") .with_name("Multi-Asset Diversified Portfolio") .build(); let instruments = BatchBuilder::create_diverse_instruments(10); let symbols: Vec<&str> = instruments.iter().map(|i| i.symbol.as_str()).collect(); let positions = BatchBuilder::create_test_positions("MULTI_ASSET_PORTFOLIO", &symbols); (portfolio, instruments, positions) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_basic_trading_scenario() { let scenario = BasicTradingScenario::new(); let portfolio = scenario.create_portfolio(); let positions = scenario.create_positions(); let instruments = scenario.create_instruments(); assert_eq!(portfolio.id, TEST_PORTFOLIO_1); assert!(!positions.is_empty()); assert_eq!(positions.len(), instruments.len()); // Check portfolio value adds up let total_value: Decimal = positions.iter().map(|p| p.market_value.to_decimal()).sum(); assert!((total_value - scenario.total_value).abs() < Decimal::new(1, 0)); // Within $1 } #[test] fn test_market_crash_scenario() { let crash_scenario = MarketCrashScenario::new(); let basic_scenario = BasicTradingScenario::new(); let original_positions = basic_scenario.create_positions(); let stressed_positions = crash_scenario.apply_shocks_to_positions(&original_positions); assert_eq!(original_positions.len(), stressed_positions.len()); // Check that equity positions went down for (original, stressed) in original_positions .into_iter() .zip(stressed_positions.into_iter()) { if original.symbol.starts_with("TEST_EQ_") { assert!(stressed.market_price < original.market_price); assert!(stressed.unrealized_pnl < original.unrealized_pnl); } } } #[test] fn test_high_frequency_scenario() { let hft_scenario = HighFrequencyScenario::new(); let orders = hft_scenario.generate_order_flow(5); // 5 seconds let ticks = hft_scenario.generate_market_ticks(100); assert_eq!(orders.len(), 5 * hft_scenario.order_rate_per_second); assert_eq!(ticks.len(), 100); // Check order timestamps are sequential for window in orders.windows(2) { assert!(window[1].timestamp >= window[0].timestamp); } } #[test] fn test_risk_limit_breach_scenario() { let risk_scenario = RiskLimitBreachScenario::new(); let positions = risk_scenario.create_concentrated_positions(); // Check that first position breaches concentration limit // Position has market value of $400,000 (4000 shares * $100) // Total portfolio is $1,000,000, so weight is 40% = 0.40 let concentrated_position = &positions[0]; let total_portfolio_value = 1_000_000.0; let position_weight = concentrated_position.market_value / total_portfolio_value; assert!(position_weight > risk_scenario.concentration_limit.to_f64().unwrap_or(0.0)); } #[test] fn test_scenario_factory() { let (portfolio, instruments, positions) = ScenarioFactory::basic_portfolio(); assert!(!positions.is_empty()); assert_eq!(positions.len(), instruments.len()); assert_eq!(portfolio.id, TEST_PORTFOLIO_1); let (stress_scenario, stressed_positions) = ScenarioFactory::market_crash(); assert!(!stressed_positions.is_empty()); // StressScenario doesn't have scenario_type field - just verify it has a name assert!(!stress_scenario.name.is_empty()); let (orders, ticks) = ScenarioFactory::high_frequency_trading(1); assert!(!orders.is_empty()); assert!(!ticks.is_empty()); } }