363 lines
11 KiB
Rust
363 lines
11 KiB
Rust
//! Sample data for MarketSim demo.
|
|
|
|
use marketsim_shared::{
|
|
Asset, AssetPrice, AssetType, BacktestRequest, EconomicFactors, MarketIndicators, MarketState,
|
|
PriceHistory, ScenarioDescription, ScenarioRequest, ScenarioResult, ScenarioType, Strategy,
|
|
StrategyParameters, StrategyType,
|
|
};
|
|
|
|
/// Create a sample crisis simulation request.
|
|
#[must_use]
|
|
pub fn create_crisis_simulation_request() -> ScenarioRequest {
|
|
ScenarioRequest {
|
|
assets: get_sample_assets(),
|
|
historical_data: get_sample_historical_data(),
|
|
initial_state: get_sample_market_state(),
|
|
scenario: ScenarioDescription {
|
|
name: "2008-Style Financial Crisis".to_string(),
|
|
scenario_type: ScenarioType::Crisis,
|
|
description: "Sharp market decline with elevated volatility and credit stress"
|
|
.to_string(),
|
|
severity: 0.8,
|
|
duration_days: 60,
|
|
},
|
|
num_paths: 100,
|
|
horizon_days: 60,
|
|
seed: Some(42),
|
|
}
|
|
}
|
|
|
|
/// Create a sample rally simulation request.
|
|
#[must_use]
|
|
pub fn create_rally_simulation_request() -> ScenarioRequest {
|
|
ScenarioRequest {
|
|
assets: get_sample_assets(),
|
|
historical_data: get_sample_historical_data(),
|
|
initial_state: get_sample_market_state(),
|
|
scenario: ScenarioDescription {
|
|
name: "Post-Crisis Recovery Rally".to_string(),
|
|
scenario_type: ScenarioType::Rally,
|
|
description: "Strong market recovery with risk-on sentiment".to_string(),
|
|
severity: 0.6,
|
|
duration_days: 90,
|
|
},
|
|
num_paths: 100,
|
|
horizon_days: 90,
|
|
seed: Some(123),
|
|
}
|
|
}
|
|
|
|
/// Create a backtest request for the given scenarios.
|
|
#[must_use]
|
|
pub fn create_backtest_request(scenarios: Vec<ScenarioResult>) -> BacktestRequest {
|
|
BacktestRequest {
|
|
strategy: Strategy {
|
|
name: "Momentum Strategy".to_string(),
|
|
strategy_type: StrategyType::Momentum,
|
|
parameters: StrategyParameters {
|
|
lookback: 20,
|
|
rebalance_frequency: 5,
|
|
max_position: 0.4,
|
|
stop_loss: Some(0.10),
|
|
take_profit: Some(0.20),
|
|
custom: std::collections::HashMap::new(),
|
|
},
|
|
},
|
|
scenarios,
|
|
initial_capital: 1_000_000.0,
|
|
transaction_cost_bps: 10.0,
|
|
}
|
|
}
|
|
|
|
/// Get sample assets.
|
|
#[must_use]
|
|
pub fn get_sample_assets() -> Vec<Asset> {
|
|
vec![
|
|
Asset {
|
|
symbol: "SPY".to_string(),
|
|
name: "S&P 500 ETF".to_string(),
|
|
asset_type: AssetType::Index,
|
|
current_price: 500.0,
|
|
},
|
|
Asset {
|
|
symbol: "QQQ".to_string(),
|
|
name: "NASDAQ 100 ETF".to_string(),
|
|
asset_type: AssetType::Index,
|
|
current_price: 400.0,
|
|
},
|
|
Asset {
|
|
symbol: "TLT".to_string(),
|
|
name: "20+ Year Treasury ETF".to_string(),
|
|
asset_type: AssetType::Bond,
|
|
current_price: 90.0,
|
|
},
|
|
Asset {
|
|
symbol: "GLD".to_string(),
|
|
name: "Gold ETF".to_string(),
|
|
asset_type: AssetType::Commodity,
|
|
current_price: 180.0,
|
|
},
|
|
Asset {
|
|
symbol: "VNQ".to_string(),
|
|
name: "Real Estate ETF".to_string(),
|
|
asset_type: AssetType::Equity,
|
|
current_price: 85.0,
|
|
},
|
|
]
|
|
}
|
|
|
|
/// Get sample market state.
|
|
#[must_use]
|
|
pub fn get_sample_market_state() -> MarketState {
|
|
MarketState {
|
|
timestamp: "2024-01-15T16:00:00Z".to_string(),
|
|
prices: vec![
|
|
AssetPrice {
|
|
symbol: "SPY".to_string(),
|
|
price: 500.0,
|
|
change_pct: 0.5,
|
|
},
|
|
AssetPrice {
|
|
symbol: "QQQ".to_string(),
|
|
price: 400.0,
|
|
change_pct: 0.8,
|
|
},
|
|
AssetPrice {
|
|
symbol: "TLT".to_string(),
|
|
price: 90.0,
|
|
change_pct: -0.2,
|
|
},
|
|
AssetPrice {
|
|
symbol: "GLD".to_string(),
|
|
price: 180.0,
|
|
change_pct: 0.1,
|
|
},
|
|
AssetPrice {
|
|
symbol: "VNQ".to_string(),
|
|
price: 85.0,
|
|
change_pct: 0.3,
|
|
},
|
|
],
|
|
indicators: MarketIndicators {
|
|
vix: 15.0,
|
|
sp500: 5000.0,
|
|
treasury_10y: 4.2,
|
|
credit_spread: 3.5,
|
|
put_call_ratio: 0.85,
|
|
},
|
|
economic_factors: EconomicFactors {
|
|
gdp_growth: 2.5,
|
|
inflation: 3.0,
|
|
unemployment: 3.8,
|
|
fed_funds_rate: 5.25,
|
|
consumer_sentiment: 102.0,
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Get sample historical data.
|
|
#[must_use]
|
|
pub fn get_sample_historical_data() -> Vec<PriceHistory> {
|
|
let num_days = 252;
|
|
|
|
vec![
|
|
generate_price_history("SPY", num_days, 500.0, 0.0003, 0.012),
|
|
generate_price_history("QQQ", num_days, 400.0, 0.0004, 0.015),
|
|
generate_price_history("TLT", num_days, 90.0, 0.0001, 0.010),
|
|
generate_price_history("GLD", num_days, 180.0, 0.0002, 0.008),
|
|
generate_price_history("VNQ", num_days, 85.0, 0.0002, 0.014),
|
|
]
|
|
}
|
|
|
|
/// Generate sample price history.
|
|
fn generate_price_history(
|
|
symbol: &str,
|
|
num_days: usize,
|
|
initial_price: f64,
|
|
daily_drift: f64,
|
|
daily_vol: f64,
|
|
) -> PriceHistory {
|
|
let mut seed = symbol
|
|
.bytes()
|
|
.fold(0u64, |acc, b| acc.wrapping_add(b as u64))
|
|
* 54321;
|
|
|
|
let mut dates = Vec::with_capacity(num_days);
|
|
let mut open = Vec::with_capacity(num_days);
|
|
let mut high = Vec::with_capacity(num_days);
|
|
let mut low = Vec::with_capacity(num_days);
|
|
let mut close = Vec::with_capacity(num_days);
|
|
let mut volume = Vec::with_capacity(num_days);
|
|
|
|
let mut current_price = initial_price;
|
|
|
|
for day in 0..num_days {
|
|
// Generate date
|
|
let date = format!("2023-{:02}-{:02}", (day / 30 % 12) + 1, (day % 28) + 1);
|
|
dates.push(date);
|
|
|
|
// Generate OHLC
|
|
seed = seed.wrapping_mul(1103515245).wrapping_add(12345);
|
|
let u1 = ((seed >> 16) % 32768) as f64 / 32768.0 + 0.0001;
|
|
seed = seed.wrapping_mul(1103515245).wrapping_add(12345);
|
|
let u2 = ((seed >> 16) % 32768) as f64 / 32768.0;
|
|
|
|
let z = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
|
|
let daily_return = (daily_drift + daily_vol * z).clamp(-0.05, 0.05);
|
|
|
|
let day_open = current_price;
|
|
let day_close = current_price * (1.0 + daily_return);
|
|
|
|
// High and low based on volatility
|
|
seed = seed.wrapping_mul(1103515245).wrapping_add(12345);
|
|
let range_mult = 1.0 + ((seed >> 16) % 32768) as f64 / 32768.0 * daily_vol;
|
|
|
|
let day_high = day_open.max(day_close) * range_mult;
|
|
let day_low = day_open.min(day_close) / range_mult;
|
|
|
|
open.push(day_open);
|
|
high.push(day_high);
|
|
low.push(day_low);
|
|
close.push(day_close);
|
|
|
|
// Volume
|
|
seed = seed.wrapping_mul(1103515245).wrapping_add(12345);
|
|
let vol = 1_000_000.0 * (0.5 + ((seed >> 16) % 32768) as f64 / 32768.0);
|
|
volume.push(vol);
|
|
|
|
current_price = day_close;
|
|
}
|
|
|
|
PriceHistory {
|
|
symbol: symbol.to_string(),
|
|
dates,
|
|
open,
|
|
high,
|
|
low,
|
|
close,
|
|
volume,
|
|
}
|
|
}
|
|
|
|
/// Get all predefined scenarios.
|
|
#[must_use]
|
|
pub fn get_predefined_scenarios() -> Vec<ScenarioDescription> {
|
|
vec![
|
|
ScenarioDescription {
|
|
name: "2008-Style Financial Crisis".to_string(),
|
|
scenario_type: ScenarioType::Crisis,
|
|
description: "Sharp market decline with elevated volatility and credit stress"
|
|
.to_string(),
|
|
severity: 0.8,
|
|
duration_days: 60,
|
|
},
|
|
ScenarioDescription {
|
|
name: "COVID-19 Crash".to_string(),
|
|
scenario_type: ScenarioType::Crisis,
|
|
description: "Rapid 30% decline followed by V-shaped recovery".to_string(),
|
|
severity: 0.7,
|
|
duration_days: 30,
|
|
},
|
|
ScenarioDescription {
|
|
name: "Tech Bubble Burst".to_string(),
|
|
scenario_type: ScenarioType::Crisis,
|
|
description: "Extended decline focused on tech sector".to_string(),
|
|
severity: 0.6,
|
|
duration_days: 120,
|
|
},
|
|
ScenarioDescription {
|
|
name: "Bull Market Rally".to_string(),
|
|
scenario_type: ScenarioType::Rally,
|
|
description: "Strong upward momentum with low volatility".to_string(),
|
|
severity: 0.6,
|
|
duration_days: 90,
|
|
},
|
|
ScenarioDescription {
|
|
name: "Post-Crisis Recovery".to_string(),
|
|
scenario_type: ScenarioType::Rally,
|
|
description: "Rapid recovery following market stress".to_string(),
|
|
severity: 0.7,
|
|
duration_days: 60,
|
|
},
|
|
ScenarioDescription {
|
|
name: "VIX Spike".to_string(),
|
|
scenario_type: ScenarioType::HighVolatility,
|
|
description: "Sudden increase in market volatility".to_string(),
|
|
severity: 0.5,
|
|
duration_days: 20,
|
|
},
|
|
ScenarioDescription {
|
|
name: "Fed Rate Hike".to_string(),
|
|
scenario_type: ScenarioType::RateShock,
|
|
description: "Aggressive monetary tightening".to_string(),
|
|
severity: 0.6,
|
|
duration_days: 45,
|
|
},
|
|
ScenarioDescription {
|
|
name: "Stagflation".to_string(),
|
|
scenario_type: ScenarioType::InflationSpike,
|
|
description: "Rising inflation with slowing growth".to_string(),
|
|
severity: 0.7,
|
|
duration_days: 90,
|
|
},
|
|
]
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_sample_assets() {
|
|
let assets = get_sample_assets();
|
|
assert_eq!(assets.len(), 5);
|
|
assert_eq!(assets[0].symbol, "SPY");
|
|
}
|
|
|
|
#[test]
|
|
fn test_sample_market_state() {
|
|
let state = get_sample_market_state();
|
|
assert_eq!(state.prices.len(), 5);
|
|
assert!(state.indicators.vix > 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_sample_historical_data() {
|
|
let data = get_sample_historical_data();
|
|
assert_eq!(data.len(), 5);
|
|
assert_eq!(data[0].close.len(), 252);
|
|
}
|
|
|
|
#[test]
|
|
fn test_crisis_request() {
|
|
let request = create_crisis_simulation_request();
|
|
assert_eq!(request.assets.len(), 5);
|
|
assert_eq!(request.num_paths, 100);
|
|
assert_eq!(request.scenario.scenario_type, ScenarioType::Crisis);
|
|
}
|
|
|
|
#[test]
|
|
fn test_rally_request() {
|
|
let request = create_rally_simulation_request();
|
|
assert_eq!(request.scenario.scenario_type, ScenarioType::Rally);
|
|
}
|
|
|
|
#[test]
|
|
fn test_predefined_scenarios() {
|
|
let scenarios = get_predefined_scenarios();
|
|
assert!(scenarios.len() >= 5);
|
|
}
|
|
|
|
#[test]
|
|
fn test_price_history_reasonable() {
|
|
let data = get_sample_historical_data();
|
|
|
|
for history in &data {
|
|
for &price in &history.close {
|
|
assert!(price > 0.0);
|
|
assert!(price < 10000.0);
|
|
}
|
|
}
|
|
}
|
|
}
|