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>
713 lines
25 KiB
Rust
713 lines
25 KiB
Rust
//! 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());
|
||
}
|
||
}
|