Files
foxhunt/testing/integration/fixtures/scenarios.rs
jgrusewski 9c3d741a08 refactor: restructure repo — crates/, bin/, testing/ layout
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>
2026-02-25 11:56:00 +01:00

713 lines
25 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.
//! 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<String>) -> 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<Position> {
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<Instrument> {
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<AssetClass, Decimal> {
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<Position> {
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<Position> {
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<String>) -> 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<TestOrder> {
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<MarketTick> {
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<Position> {
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<Position> {
// 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<Utc>,
pub time_in_force: TimeInForce,
}
/// Market tick data for price feed scenarios
#[derive(Debug, Clone)]
pub struct MarketTick {
pub symbol: String,
pub timestamp: DateTime<Utc>,
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<Instrument>, Vec<Position>) {
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<Position>) {
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<TestOrder>, Vec<MarketTick>) {
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<Position>) {
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<Instrument>, Vec<Position>) {
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());
}
}