#[derive(Debug, Clone, Copy)]
pub struct GbmParams {
pub mu: f64,
pub sigma: f64,
pub s0: f64,
}
#[derive(Debug, Clone)]
pub struct MonteCarloConfig {
pub simulations: usize,
pub horizon_days: usize,
pub seed: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct MonteCarloResult {
pub paths: Vec<Vec<f64>>,
pub var_95: f64,
pub cvar_95: f64,
pub median_final: f64,
pub best_case_final: f64,
pub worst_case_final: f64,
}
struct Lcg {
state: u64,
}
impl Lcg {
fn new(seed: u64) -> Self {
Self { state: seed.wrapping_add(1) }
}
fn next_u64(&mut self) -> u64 {
self.state = self
.state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
self.state
}
fn next_f64(&mut self) -> f64 {
(self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
}
}
fn box_muller(rng: &mut Lcg) -> (f64, f64) {
let mut u1 = rng.next_f64();
let u2 = rng.next_f64();
if u1 < 1e-15 {
u1 = 1e-15;
}
let mag = (-2.0 * u1.ln()).sqrt();
let theta = 2.0 * std::f64::consts::PI * u2;
(mag * theta.cos(), mag * theta.sin())
}
pub struct MonteCarloSimulator;
impl MonteCarloSimulator {
pub fn simulate_paths(params: &GbmParams, config: &MonteCarloConfig) -> Vec<Vec<f64>> {
let seed = config.seed.unwrap_or(42);
let mut rng = Lcg::new(seed);
let dt = 1.0 / 252.0; let drift = (params.mu - 0.5 * params.sigma * params.sigma) * dt;
let vol_sqrt_dt = params.sigma * dt.sqrt();
let mut paths = Vec::with_capacity(config.simulations);
let mut sim_i = 0;
while sim_i < config.simulations {
let mut path_a = Vec::with_capacity(config.horizon_days);
let mut path_b = Vec::with_capacity(config.horizon_days);
let mut price_a = params.s0;
let mut price_b = params.s0;
let mut day = 0;
while day < config.horizon_days {
let (z1, z2) = box_muller(&mut rng);
price_a *= (drift + vol_sqrt_dt * z1).exp();
path_a.push(price_a);
if day < config.horizon_days {
price_b *= (drift + vol_sqrt_dt * z2).exp();
path_b.push(price_b);
}
day += 1;
}
paths.push(path_a);
sim_i += 1;
if sim_i < config.simulations {
paths.push(path_b);
sim_i += 1;
}
}
paths.truncate(config.simulations);
paths
}
pub fn var(paths: &[Vec<f64>], confidence: f64) -> f64 {
if paths.is_empty() {
return 0.0;
}
let mut finals: Vec<f64> = paths
.iter()
.filter_map(|p| p.last().copied())
.collect();
finals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let idx = ((1.0 - confidence) * finals.len() as f64).ceil() as usize;
let idx = idx.min(finals.len() - 1);
finals[idx]
}
pub fn cvar(paths: &[Vec<f64>], confidence: f64) -> f64 {
if paths.is_empty() {
return 0.0;
}
let mut finals: Vec<f64> = paths
.iter()
.filter_map(|p| p.last().copied())
.collect();
finals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let tail_n = ((1.0 - confidence) * finals.len() as f64).ceil() as usize;
let tail_n = tail_n.max(1).min(finals.len());
let tail_sum: f64 = finals[..tail_n].iter().sum();
tail_sum / tail_n as f64
}
pub fn percentile_paths(paths: &[Vec<f64>], percentiles: &[f64]) -> Vec<Vec<f64>> {
if paths.is_empty() || percentiles.is_empty() {
return vec![];
}
let mut indexed: Vec<(usize, f64)> = paths
.iter()
.enumerate()
.filter_map(|(i, p)| p.last().map(|&v| (i, v)))
.collect();
indexed.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
let n = indexed.len();
percentiles
.iter()
.map(|&pct| {
let pct_clamped = pct.clamp(0.0, 100.0);
let idx = ((pct_clamped / 100.0) * (n as f64 - 1.0)).round() as usize;
let idx = idx.min(n - 1);
paths[indexed[idx].0].clone()
})
.collect()
}
pub fn run(params: &GbmParams, config: &MonteCarloConfig) -> MonteCarloResult {
let paths = Self::simulate_paths(params, config);
let var_95 = Self::var(&paths, 0.95);
let cvar_95 = Self::cvar(&paths, 0.95);
let mut finals: Vec<f64> =
paths.iter().filter_map(|p| p.last().copied()).collect();
finals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let median_final = if finals.is_empty() {
params.s0
} else {
finals[finals.len() / 2]
};
let best_case_final = finals.last().copied().unwrap_or(params.s0);
let worst_case_final = finals.first().copied().unwrap_or(params.s0);
MonteCarloResult { paths, var_95, cvar_95, median_final, best_case_final, worst_case_final }
}
}
#[cfg(test)]
mod tests {
use super::*;
fn default_params() -> GbmParams {
GbmParams { mu: 0.10, sigma: 0.20, s0: 100.0 }
}
fn default_config(seed: u64) -> MonteCarloConfig {
MonteCarloConfig { simulations: 1000, horizon_days: 252, seed: Some(seed) }
}
#[test]
fn test_path_count_matches_simulations() {
let params = default_params();
let config = default_config(1);
let paths = MonteCarloSimulator::simulate_paths(¶ms, &config);
assert_eq!(paths.len(), 1000);
}
#[test]
fn test_path_length_matches_horizon() {
let params = default_params();
let config = default_config(1);
let paths = MonteCarloSimulator::simulate_paths(¶ms, &config);
for path in &paths {
assert_eq!(path.len(), 252);
}
}
#[test]
fn test_reproducibility_with_same_seed() {
let params = default_params();
let config = default_config(42);
let paths1 = MonteCarloSimulator::simulate_paths(¶ms, &config);
let paths2 = MonteCarloSimulator::simulate_paths(¶ms, &config);
assert_eq!(paths1.len(), paths2.len());
for (p1, p2) in paths1.iter().zip(paths2.iter()) {
for (a, b) in p1.iter().zip(p2.iter()) {
assert!((a - b).abs() < 1e-12, "Paths differ: {} vs {}", a, b);
}
}
}
#[test]
fn test_different_seeds_produce_different_paths() {
let params = default_params();
let c1 = default_config(1);
let c2 = default_config(2);
let p1 = MonteCarloSimulator::simulate_paths(¶ms, &c1);
let p2 = MonteCarloSimulator::simulate_paths(¶ms, &c2);
let differs = p1[0].iter().zip(p2[0].iter()).any(|(a, b)| (a - b).abs() > 1e-10);
assert!(differs, "Different seeds should yield different paths");
}
#[test]
fn test_all_prices_positive() {
let params = default_params();
let config = default_config(7);
let paths = MonteCarloSimulator::simulate_paths(¶ms, &config);
for path in &paths {
for &price in path {
assert!(price > 0.0, "GBM price must be positive, got {}", price);
}
}
}
#[test]
fn test_var_less_than_or_equal_cvar() {
let params = default_params();
let config = default_config(10);
let paths = MonteCarloSimulator::simulate_paths(¶ms, &config);
let var = MonteCarloSimulator::var(&paths, 0.95);
let cvar = MonteCarloSimulator::cvar(&paths, 0.95);
assert!(cvar <= var + 1e-6, "CVaR={} should be <= VaR={}", cvar, var);
}
#[test]
fn test_var_is_positive_price() {
let params = default_params();
let config = default_config(5);
let paths = MonteCarloSimulator::simulate_paths(¶ms, &config);
let var = MonteCarloSimulator::var(&paths, 0.95);
assert!(var > 0.0, "VaR should be a positive price");
}
#[test]
fn test_cvar_is_positive_price() {
let params = default_params();
let config = default_config(5);
let paths = MonteCarloSimulator::simulate_paths(¶ms, &config);
let cvar = MonteCarloSimulator::cvar(&paths, 0.95);
assert!(cvar > 0.0, "CVaR should be a positive price");
}
#[test]
fn test_percentile_paths_count() {
let params = default_params();
let config = default_config(3);
let paths = MonteCarloSimulator::simulate_paths(¶ms, &config);
let pct_paths = MonteCarloSimulator::percentile_paths(&paths, &[5.0, 50.0, 95.0]);
assert_eq!(pct_paths.len(), 3);
}
#[test]
fn test_percentile_paths_length() {
let params = default_params();
let config = default_config(3);
let paths = MonteCarloSimulator::simulate_paths(¶ms, &config);
let pct_paths = MonteCarloSimulator::percentile_paths(&paths, &[5.0, 50.0, 95.0]);
for p in &pct_paths {
assert_eq!(p.len(), 252);
}
}
#[test]
fn test_percentile_ordering() {
let params = default_params();
let config = default_config(99);
let paths = MonteCarloSimulator::simulate_paths(¶ms, &config);
let pct_paths = MonteCarloSimulator::percentile_paths(&paths, &[5.0, 50.0, 95.0]);
let p5_final = pct_paths[0].last().copied().unwrap_or(0.0);
let p50_final = pct_paths[1].last().copied().unwrap_or(0.0);
let p95_final = pct_paths[2].last().copied().unwrap_or(0.0);
assert!(p5_final <= p50_final + 1e-6, "p5={} p50={}", p5_final, p50_final);
assert!(p50_final <= p95_final + 1e-6, "p50={} p95={}", p50_final, p95_final);
}
#[test]
fn test_gbm_positive_drift_raises_median() {
let params = GbmParams { mu: 0.50, sigma: 0.10, s0: 100.0 };
let config = MonteCarloConfig { simulations: 5000, horizon_days: 252, seed: Some(1) };
let paths = MonteCarloSimulator::simulate_paths(¶ms, &config);
let mut finals: Vec<f64> =
paths.iter().filter_map(|p| p.last().copied()).collect();
finals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let median = finals[finals.len() / 2];
assert!(median > 100.0, "Positive drift should raise median: median={}", median);
}
#[test]
fn test_gbm_zero_vol_deterministic() {
let params = GbmParams { mu: 0.10, sigma: 0.0, s0: 100.0 };
let config = MonteCarloConfig { simulations: 10, horizon_days: 10, seed: Some(1) };
let paths = MonteCarloSimulator::simulate_paths(¶ms, &config);
let first = &paths[0];
for path in &paths[1..] {
for (a, b) in first.iter().zip(path.iter()) {
assert!(
(a - b).abs() < 1e-6,
"Zero-vol paths should be identical: {} vs {}",
a,
b
);
}
}
}
#[test]
fn test_run_result_structure() {
let params = default_params();
let config = default_config(77);
let result = MonteCarloSimulator::run(¶ms, &config);
assert_eq!(result.paths.len(), 1000);
assert!(result.best_case_final >= result.median_final);
assert!(result.worst_case_final <= result.median_final);
assert!(result.var_95 > 0.0);
assert!(result.cvar_95 > 0.0);
}
#[test]
fn test_empty_paths_var_is_zero() {
let var = MonteCarloSimulator::var(&[], 0.95);
assert_eq!(var, 0.0);
}
#[test]
fn test_empty_paths_cvar_is_zero() {
let cvar = MonteCarloSimulator::cvar(&[], 0.95);
assert_eq!(cvar, 0.0);
}
#[test]
fn test_odd_simulation_count() {
let params = default_params();
let config = MonteCarloConfig { simulations: 101, horizon_days: 10, seed: Some(5) };
let paths = MonteCarloSimulator::simulate_paths(¶ms, &config);
assert_eq!(paths.len(), 101);
}
#[test]
fn test_single_simulation() {
let params = default_params();
let config = MonteCarloConfig { simulations: 1, horizon_days: 5, seed: Some(1) };
let paths = MonteCarloSimulator::simulate_paths(¶ms, &config);
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].len(), 5);
}
#[test]
fn test_percentile_empty_paths() {
let result = MonteCarloSimulator::percentile_paths(&[], &[50.0]);
assert!(result.is_empty());
}
}