//! Test Data Utilities and Generators for Foxhunt HFT Trading System //! //! This module provides utilities for generating test data, including //! market data, time series, random data generation, and test data persistence. //! //! ## Usage //! //! ```rust //! use tests::fixtures::test_data::*; //! //! // Generate market data //! let market_data = MarketDataGenerator::new() //! .with_symbol(TEST_EQUITY_1) //! .generate_price_series(1000); //! //! // Generate time series data //! let time_series = TimeSeriesGenerator::new() //! .with_frequency(Duration::from_secs(1)) //! .generate_ohlcv(24 * 60 * 60); // 1 day of second data //! //! // Generate random portfolios //! let random_portfolio = RandomDataGenerator::new() //! .generate_random_portfolio(10); // 10 positions //! ``` use chrono::{DateTime, Utc, Duration as ChronoDuration, Timelike}; use ::rust_decimal::Decimal; use serde_json::json; use std::collections::HashMap; use uuid::Uuid; use rand::{Rng, SeedableRng, rngs::StdRng}; use rand::distributions::Distribution; use rand_distr::Normal; // Import types from risk crate use risk::risk_types::{Position, StressScenario}; use crate::fixtures::helpers::ToDecimal; use super::builders::*; use super::*; /// Legacy constant for backward compatibility pub const SAMPLE_PRICE: f64 = 100.0; // ============================================================================= // MARKET DATA GENERATOR // ============================================================================= /// Generator for realistic market data with configurable parameters #[derive(Debug, Clone)] pub struct MarketDataGenerator { pub symbol: String, pub initial_price: Decimal, pub volatility: f64, // Daily volatility (e.g., 0.02 = 2%) pub drift: f64, // Daily drift (e.g., 0.0001 = 0.01%) pub tick_size: Decimal, // Minimum price increment pub bid_ask_spread: Decimal, // Spread in price units pub seed: Option, // Random seed for reproducible data } impl Default for MarketDataGenerator { fn default() -> Self { Self::new() } } impl MarketDataGenerator { pub fn new() -> Self { Self { symbol: TEST_EQUITY_1.to_string(), initial_price: Decimal::from(100), volatility: 0.02, // 2% daily volatility drift: 0.0001, // 0.01% daily drift tick_size: Decimal::new(1, 2), // $0.01 bid_ask_spread: Decimal::new(2, 2), // $0.02 seed: Some(42), // Deterministic by default for testing } } pub fn with_symbol(mut self, symbol: impl Into) -> Self { self.symbol = symbol.into(); self } pub fn with_initial_price(mut self, price: Decimal) -> Self { self.initial_price = price; self } pub fn with_volatility(mut self, volatility: f64) -> Self { self.volatility = volatility; self } pub fn with_drift(mut self, drift: f64) -> Self { self.drift = drift; self } pub fn with_tick_size(mut self, tick_size: Decimal) -> Self { self.tick_size = tick_size; self } pub fn with_seed(mut self, seed: u64) -> Self { self.seed = Some(seed); self } pub fn random(mut self) -> Self { self.seed = None; self } /// Generate a price series using geometric Brownian motion pub fn generate_price_series(&self, count: usize) -> Vec { let mut rng = match self.seed { Some(seed) => StdRng::seed_from_u64(seed), None => StdRng::from_entropy(), }; let normal = Normal::new(0.0, 1.0).unwrap(); let mut prices = Vec::with_capacity(count); let mut current_price = self.initial_price; let start_time = Utc::now(); // Convert daily parameters to per-tick parameters let dt = 1.0 / (252.0 * 24.0 * 60.0 * 60.0); // Assume 1-second intervals let drift_per_tick = self.drift * dt; let vol_per_tick = self.volatility * dt.sqrt(); for i in 0..count { let timestamp = start_time + ChronoDuration::seconds(i as i64); // Geometric Brownian Motion: dS = μS dt + σS dW let random_shock = normal.sample(&mut rng); let price_change_pct = drift_per_tick + vol_per_tick * random_shock; let new_price = current_price * (Decimal::ONE + Decimal::try_from(price_change_pct).unwrap_or(Decimal::ZERO)); // Round to tick size let rounded_price = self.round_to_tick_size(new_price); current_price = rounded_price; prices.push(PricePoint { symbol: self.symbol.clone(), timestamp, price: current_price, bid: current_price - (self.bid_ask_spread / Decimal::from(2)), ask: current_price + (self.bid_ask_spread / Decimal::from(2)), volume: Decimal::from(rng.gen_range(100..=10000)), sequence: i as u64, }); } prices } /// Generate OHLCV bars from price series pub fn generate_ohlcv_bars(&self, count: usize, bar_duration: ChronoDuration) -> Vec { let price_points = self.generate_price_series(count * 60); // 60 ticks per bar let mut bars = Vec::new(); let mut current_bar: Option = None; for point in price_points { let bar_start = self.get_bar_start_time(point.timestamp, bar_duration); match &mut current_bar { Some(bar) if bar.timestamp == bar_start => { // Update existing bar bar.high = bar.high.max(point.price); bar.low = bar.low.min(point.price); bar.close = point.price; bar.volume += point.volume; }, _ => { // Start new bar if let Some(completed_bar) = current_bar.take() { bars.push(completed_bar); } current_bar = Some(OHLCVBar { symbol: self.symbol.clone(), timestamp: bar_start, open: point.price, high: point.price, low: point.price, close: point.price, volume: point.volume, }); } } if bars.len() >= count { break; } } if let Some(final_bar) = current_bar { bars.push(final_bar); } bars.truncate(count); bars } /// Generate market depth (order book) data pub fn generate_market_depth(&self, levels: usize) -> MarketDepth { let mut rng = match self.seed { Some(seed) => StdRng::seed_from_u64(seed), None => StdRng::from_entropy(), }; let mid_price = self.initial_price; let mut bids = Vec::with_capacity(levels); let mut asks = Vec::with_capacity(levels); for i in 0..levels { let level_offset = Decimal::from(i + 1) * self.tick_size; let bid_price = mid_price - level_offset; let ask_price = mid_price + level_offset; let bid_size = Decimal::from(rng.gen_range(100..=5000)); let ask_size = Decimal::from(rng.gen_range(100..=5000)); bids.push(DepthLevel { price: bid_price, size: bid_size, order_count: rng.gen_range(1..=10), }); asks.push(DepthLevel { price: ask_price, size: ask_size, order_count: rng.gen_range(1..=10), }); } MarketDepth { symbol: self.symbol.clone(), timestamp: Utc::now(), bids, asks, } } fn round_to_tick_size(&self, price: Decimal) -> Decimal { if self.tick_size == Decimal::ZERO { return price; } let ticks = price / self.tick_size; let rounded_ticks = ticks.round(); rounded_ticks * self.tick_size } fn get_bar_start_time(&self, timestamp: DateTime, duration: ChronoDuration) -> DateTime { let seconds_since_epoch = timestamp.timestamp(); let duration_seconds = duration.num_seconds(); let bar_start_seconds = (seconds_since_epoch / duration_seconds) * duration_seconds; DateTime::from_timestamp(bar_start_seconds, 0).unwrap_or(timestamp) } } // ============================================================================= // TIME SERIES GENERATOR // ============================================================================= /// Generator for time series data with various patterns #[derive(Debug, Clone)] pub struct TimeSeriesGenerator { pub frequency: ChronoDuration, pub start_time: DateTime, pub trend: f64, // Linear trend component pub seasonality: f64, // Seasonal amplitude pub noise_level: f64, // Random noise level pub seed: Option, } impl Default for TimeSeriesGenerator { fn default() -> Self { Self::new() } } impl TimeSeriesGenerator { pub fn new() -> Self { Self { frequency: ChronoDuration::minutes(1), start_time: Utc::now() - ChronoDuration::hours(24), trend: 0.0, seasonality: 0.1, noise_level: 0.05, seed: Some(42), } } pub fn with_frequency(mut self, frequency: ChronoDuration) -> Self { self.frequency = frequency; self } pub fn with_start_time(mut self, start_time: DateTime) -> Self { self.start_time = start_time; self } pub fn with_trend(mut self, trend: f64) -> Self { self.trend = trend; self } pub fn with_seasonality(mut self, seasonality: f64) -> Self { self.seasonality = seasonality; self } pub fn with_noise_level(mut self, noise_level: f64) -> Self { self.noise_level = noise_level; self } /// Generate time series with trend, seasonality, and noise pub fn generate_series(&self, count: usize, base_value: f64) -> Vec { let mut rng = match self.seed { Some(seed) => StdRng::seed_from_u64(seed), None => StdRng::from_entropy(), }; let normal = Normal::new(0.0, self.noise_level).unwrap(); let mut series = Vec::with_capacity(count); for i in 0..count { let timestamp = self.start_time + (self.frequency * i as i32); // Trend component let trend_value = self.trend * i as f64; // Seasonal component (daily pattern) let hour_of_day = timestamp.hour() as f64; let seasonal_value = self.seasonality * (2.0 * std::f64::consts::PI * hour_of_day / 24.0).sin(); // Noise component let noise_value = normal.sample(&mut rng); // Combine components let value = base_value + trend_value + seasonal_value + noise_value; series.push(TimeSeriesPoint { timestamp, value: Decimal::try_from(value).unwrap_or(Decimal::from(0)), metadata: json!({ "trend": trend_value, "seasonal": seasonal_value, "noise": noise_value }), }); } series } /// Generate OHLCV data from time series pub fn generate_ohlcv(&self, count: usize) -> Vec { let base_price = 100.0; let series = self.generate_series(count, base_price); let mut ohlcv = Vec::with_capacity(count); for point in series { let price = point.value; let volume = Decimal::from(1000 + (point.timestamp.timestamp() % 9000) as i64); ohlcv.push(OHLCVBar { symbol: TEST_EQUITY_1.to_string(), timestamp: point.timestamp, open: price, high: price * Decimal::new(1001, 3), // +0.1% low: price * Decimal::new(999, 3), // -0.1% close: price, volume, }); } ohlcv } } // ============================================================================= // RANDOM DATA GENERATOR // ============================================================================= /// Generator for random test data with realistic distributions #[derive(Debug)] pub struct RandomDataGenerator { rng: StdRng, } impl RandomDataGenerator { pub fn new() -> Self { Self { rng: StdRng::seed_from_u64(42), } } pub fn with_seed(seed: u64) -> Self { Self { rng: StdRng::seed_from_u64(seed), } } /// Generate a random portfolio with specified number of positions pub fn generate_random_portfolio(&mut self, position_count: usize) -> (Portfolio, Vec, Vec) { let portfolio_id = format!("RANDOM_PORTFOLIO_{}", self.rng.gen::()); let portfolio = PortfolioBuilder::new() .with_id(&portfolio_id) .with_name("Random Test Portfolio") .with_base_currency("USD") .strategy_portfolio() .build(); let mut instruments = Vec::with_capacity(position_count); let mut positions = Vec::with_capacity(position_count); let asset_classes = [ AssetClass::Equities, AssetClass::Currencies, AssetClass::FixedIncome, AssetClass::Derivatives, AssetClass::Commodities, AssetClass::Alternatives, ]; for i in 0..position_count { let asset_class = asset_classes[self.rng.gen_range(0..asset_classes.len())]; let symbol = generate_test_symbol(asset_class); // Generate random instrument let mut instrument_builder = InstrumentBuilder::new() .with_symbol(&symbol) .with_name(format!("Random Instrument {}", i + 1)); instrument_builder = match asset_class { AssetClass::Equities => instrument_builder.equity(), AssetClass::Currencies => instrument_builder.currency(), AssetClass::FixedIncome => instrument_builder.bond(), AssetClass::Derivatives => instrument_builder.future(), AssetClass::Commodities => instrument_builder.commodity(), AssetClass::Alternatives => instrument_builder.crypto(), AssetClass::Cash => instrument_builder.equity(), }; instruments.push(instrument_builder.build()); // Generate random position let quantity = Decimal::from(self.rng.gen_range(100..=10000)); let price = self.rng.gen_range(10.0..=1000.0).to_decimal(); let price_change_pct = self.rng.gen_range(-0.1..=0.1); // ±10% let market_price = price * (Decimal::ONE + Decimal::try_from(price_change_pct).unwrap_or(Decimal::ZERO)); let position = PositionBuilder::new() .with_portfolio_id(&portfolio_id) .with_symbol(&symbol) .with_quantity(quantity) .with_average_price(price) .with_market_price(market_price) .with_beta(Decimal::try_from(self.rng.gen_range(0.5..=2.0)).unwrap()) .build(); positions.push(position); } (portfolio, instruments, positions) } /// Generate random market events pub fn generate_random_events(&mut self, count: usize) -> Vec { let event_types = ["trade", "quote", "news", "halt", "resume"]; let symbols = ALL_TEST_SYMBOLS; let mut events = Vec::with_capacity(count); for i in 0..count { let event_type = event_types[self.rng.gen_range(0..event_types.len())]; let symbol = symbols[self.rng.gen_range(0..symbols.len())]; let timestamp = Utc::now() + ChronoDuration::seconds(i as i64); events.push(MarketEvent { id: Uuid::new_v4(), event_type: event_type.to_string(), symbol: symbol.to_string(), timestamp, data: json!({ "random_data": true, "sequence": i, "value": self.rng.gen_range(1.0..=1000.0) }), }); } events } /// Generate random stress test scenarios pub fn generate_random_stress_scenarios(&mut self, count: usize) -> Vec { let scenario_types = ["Historical", "Hypothetical", "Monte Carlo"]; let mut scenarios = Vec::with_capacity(count); for i in 0..count { let _scenario_type = scenario_types[self.rng.gen_range(0..scenario_types.len())]; let shock_factors = json!({ "equity_shock": self.rng.gen_range(-0.5..=0.2), "bond_shock": self.rng.gen_range(-0.2..=0.3), "fx_shock": self.rng.gen_range(-0.3..=0.3), "commodity_shock": self.rng.gen_range(-0.4..=0.4), "volatility_multiplier": self.rng.gen_range(1.0..=5.0) }); let equity_shock = shock_factors.get("equity_shock").and_then(|v| v.as_f64()).unwrap_or(0.0); let mut price_shocks = HashMap::new(); price_shocks.insert("EQUITY".to_string(), equity_shock); scenarios.push(StressScenario { id: Uuid::new_v4().to_string(), name: format!("Random Stress Scenario {}", i + 1), price_shocks: price_shocks.clone(), market_shocks: price_shocks, volatility_multiplier: shock_factors.get("volatility_multiplier").and_then(|v| v.as_f64()).unwrap_or(1.0), volatility_multipliers: HashMap::new(), correlation_changes: HashMap::new(), correlation_adjustments: HashMap::new(), liquidity_haircuts: HashMap::new(), }); } scenarios } } impl Default for RandomDataGenerator { fn default() -> Self { Self::new() } } // ============================================================================= // DATA STRUCTURES // ============================================================================= /// Price point for market data #[derive(Debug, Clone)] pub struct PricePoint { pub symbol: String, pub timestamp: DateTime, pub price: Decimal, pub bid: Decimal, pub ask: Decimal, pub volume: Decimal, pub sequence: u64, } /// OHLCV bar for candlestick data #[derive(Debug, Clone)] pub struct OHLCVBar { pub symbol: String, pub timestamp: DateTime, pub open: Decimal, pub high: Decimal, pub low: Decimal, pub close: Decimal, pub volume: Decimal, } /// Market depth level #[derive(Debug, Clone)] pub struct DepthLevel { pub price: Decimal, pub size: Decimal, pub order_count: u32, } /// Market depth (order book) #[derive(Debug, Clone)] pub struct MarketDepth { pub symbol: String, pub timestamp: DateTime, pub bids: Vec, pub asks: Vec, } /// Time series data point #[derive(Debug, Clone)] pub struct TimeSeriesPoint { pub timestamp: DateTime, pub value: Decimal, pub metadata: serde_json::Value, } /// Market event for event-driven testing #[derive(Debug, Clone)] pub struct MarketEvent { pub id: Uuid, pub event_type: String, pub symbol: String, pub timestamp: DateTime, pub data: serde_json::Value, } // ============================================================================= // UTILITY FUNCTIONS // ============================================================================= /// Create realistic test prices for different asset classes pub fn create_realistic_test_prices() -> HashMap { let mut prices = HashMap::new(); // Equity prices for symbol in ALL_TEST_EQUITIES { prices.insert(symbol.to_string(), Decimal::from(100 + (symbol.len() as i64 * 10))); } // FX prices (rates) prices.insert(TEST_FOREX_1.to_string(), Decimal::new(12345, 5)); // 1.2345 prices.insert(TEST_FOREX_2.to_string(), Decimal::new(13456, 5)); // 1.3456 prices.insert(TEST_FOREX_3.to_string(), Decimal::from(150)); // 150.00 prices.insert(TEST_FOREX_EXOTIC.to_string(), Decimal::new(2850, 2)); // 28.50 (USDTRY) // Futures prices for symbol in ALL_TEST_FUTURES { prices.insert(symbol.to_string(), Decimal::from(4000 + (symbol.len() as i64 * 100))); } // Bond prices (yield-like) for symbol in ALL_TEST_BONDS { prices.insert(symbol.to_string(), Decimal::new(250 + (symbol.len() as i64 * 10), 2)); } // Commodity prices prices.insert(TEST_COMMODITY_1.to_string(), Decimal::from(2000)); // Gold prices.insert(TEST_COMMODITY_2.to_string(), Decimal::from(25)); // Silver prices.insert(TEST_COMMODITY_OIL.to_string(), Decimal::from(80)); // Oil prices.insert(TEST_COMMODITY_GAS.to_string(), Decimal::from(4)); // Natural Gas // Crypto prices prices.insert(TEST_CRYPTO_1.to_string(), Decimal::from(50000)); // BTC-like prices.insert(TEST_CRYPTO_2.to_string(), Decimal::from(3000)); // ETH-like prices.insert(TEST_CRYPTO_ALT.to_string(), Decimal::from(1)); // Altcoin // Option prices prices.insert(TEST_OPTION_CALL.to_string(), Decimal::from(5)); // Call option premium prices.insert(TEST_OPTION_PUT.to_string(), Decimal::from(3)); // Put option premium prices } /// Create test volatility estimates for different asset classes pub fn create_test_volatilities() -> HashMap { let mut volatilities = HashMap::new(); // Equity volatilities (annualized) for symbol in ALL_TEST_EQUITIES { volatilities.insert(symbol.to_string(), 0.20); // 20% annual vol } // FX volatilities volatilities.insert(TEST_FOREX_1.to_string(), 0.10); // 10% annual vol volatilities.insert(TEST_FOREX_2.to_string(), 0.12); volatilities.insert(TEST_FOREX_3.to_string(), 0.08); volatilities.insert(TEST_FOREX_EXOTIC.to_string(), 0.18); // Higher vol for exotic pair // Futures volatilities for symbol in ALL_TEST_FUTURES { volatilities.insert(symbol.to_string(), 0.25); // 25% annual vol } // Bond volatilities for symbol in ALL_TEST_BONDS { volatilities.insert(symbol.to_string(), 0.05); // 5% annual vol } // Commodity volatilities volatilities.insert(TEST_COMMODITY_1.to_string(), 0.15); // Gold volatilities.insert(TEST_COMMODITY_2.to_string(), 0.20); // Silver volatilities.insert(TEST_COMMODITY_OIL.to_string(), 0.35); // Oil volatilities.insert(TEST_COMMODITY_GAS.to_string(), 0.50); // Natural Gas // Crypto volatilities volatilities.insert(TEST_CRYPTO_1.to_string(), 0.60); // BTC-like volatilities.insert(TEST_CRYPTO_2.to_string(), 0.70); // ETH-like volatilities.insert(TEST_CRYPTO_ALT.to_string(), 0.80); // Altcoin volatilities } #[cfg(test)] mod tests { use super::*; #[test] fn test_market_data_generator() { let generator = MarketDataGenerator::new(); let prices = generator.generate_price_series(100); assert_eq!(prices.len(), 100); assert!(prices[0].price > Decimal::ZERO); // Check timestamps are sequential for window in prices.windows(2) { assert!(window[1].timestamp >= window[0].timestamp); } } #[test] fn test_ohlcv_generation() { let generator = MarketDataGenerator::new(); let bars = generator.generate_ohlcv_bars(50, ChronoDuration::minutes(1)); assert_eq!(bars.len(), 50); for bar in &bars { assert!(bar.high >= bar.low); assert!(bar.high >= bar.open); assert!(bar.high >= bar.close); assert!(bar.low <= bar.open); assert!(bar.low <= bar.close); } } #[test] fn test_time_series_generator() { let generator = TimeSeriesGenerator::new() .with_trend(0.1) .with_seasonality(0.2); let series = generator.generate_series(100, 100.0); assert_eq!(series.len(), 100); // Check that trend is applied (last value should be higher due to positive trend) assert!(series.last().unwrap().value > series.first().unwrap().value); } #[test] fn test_random_data_generator() { let mut generator = RandomDataGenerator::new(); let (portfolio, instruments, positions) = generator.generate_random_portfolio(5); assert_eq!(instruments.len(), 5); assert_eq!(positions.len(), 5); assert_eq!(portfolio.id.len() > 0, true); // Check that positions are created (portfolio_id not stored in Position anymore) assert!(!positions.is_empty()); } #[test] fn test_market_depth_generation() { let generator = MarketDataGenerator::new(); let depth = generator.generate_market_depth(5); assert_eq!(depth.bids.len(), 5); assert_eq!(depth.asks.len(), 5); // Check that bid prices are decreasing and ask prices are increasing for window in depth.bids.windows(2) { assert!(window[0].price > window[1].price); } for window in depth.asks.windows(2) { assert!(window[0].price < window[1].price); } } #[test] fn test_realistic_test_prices() { let prices = create_realistic_test_prices(); assert!(!prices.is_empty()); // Check that all test symbols have prices for &symbol in ALL_TEST_SYMBOLS { assert!(prices.contains_key(symbol)); assert!(prices[symbol] > Decimal::ZERO); } } #[test] fn test_volatility_estimates() { let volatilities = create_test_volatilities(); assert!(!volatilities.is_empty()); // Check that volatilities are reasonable (between 0 and 100%) for (symbol, &vol) in &volatilities { assert!(vol > 0.0, "Volatility for {} should be > 0", symbol); assert!(vol < 1.0, "Volatility for {} should be < 1.0", symbol); // Less than 100% for most assets } } }