Case Study: Monte Carlo Financial Simulation

This case study demonstrates Aprender's Monte Carlo framework for financial modeling and risk analysis.

Overview

The monte_carlo module provides:

  • Simulation Engine: Reproducible RNG, variance reduction, convergence diagnostics
  • Financial Models: GBM, Merton jump-diffusion, empirical bootstrap
  • Risk Metrics: VaR, CVaR, drawdown analysis
  • Risk Ratios: Sharpe, Sortino, Calmar, Treynor, Information, Omega

Basic Simulation

use aprender::monte_carlo::prelude::*;

fn main() {
    // Create reproducible simulation engine
    let engine = MonteCarloEngine::reproducible(42)
        .with_n_simulations(10_000)
        .with_variance_reduction(VarianceReduction::Antithetic);

    // Define stock model: S₀=$100, μ=8%, σ=20%
    let model = GeometricBrownianMotion::new(100.0, 0.08, 0.20);

    // Simulate 1 year with monthly steps
    let horizon = TimeHorizon::years(1);
    let result = engine.simulate(&model, &horizon);

    // Analyze results
    println!("Simulated {} paths", result.n_paths());

    let stats = result.final_value_statistics();
    println!("Final Value Statistics:");
    println!("  Mean: ${:.2}", stats.mean);
    println!("  Std Dev: ${:.2}", stats.std);
    println!("  Min: ${:.2}", stats.min);
    println!("  Max: ${:.2}", stats.max);
}

Financial Models

Geometric Brownian Motion

use aprender::monte_carlo::prelude::*;

// Standard GBM model
let gbm = GeometricBrownianMotion::new(
    100.0,  // Initial price S₀
    0.08,   // Drift μ (8% annual return)
    0.20,   // Volatility σ (20% annual)
);

// Simulate
let engine = MonteCarloEngine::reproducible(42);
let result = engine.simulate(&gbm, &TimeHorizon::years(1));

Merton Jump-Diffusion

For modeling crash risk:

use aprender::monte_carlo::prelude::*;

// Jump-diffusion with crash risk
let jump_model = MertonJumpDiffusion::new(
    100.0,   // Initial price
    0.08,    // Drift
    0.15,    // Diffusion volatility (lower due to jumps)
    1.0,     // Jump intensity λ (1 jump/year on average)
    -0.05,   // Mean jump size (5% drop)
    0.10,    // Jump size volatility
);

let engine = MonteCarloEngine::reproducible(42)
    .with_n_simulations(50_000);  // More sims for jump processes

let result = engine.simulate(&jump_model, &TimeHorizon::years(1));

// Jump models show fatter tails
let stats = result.final_value_statistics();
println!("With jumps - Min: ${:.2}, Max: ${:.2}", stats.min, stats.max);

Empirical Bootstrap

Non-parametric simulation from historical data:

use aprender::monte_carlo::prelude::*;

// Historical daily returns
let historical_returns = vec![
    0.01, -0.02, 0.005, 0.015, -0.01, 0.02, -0.005,
    0.008, -0.015, 0.012, 0.003, -0.008, 0.018, -0.003,
    // ... more historical data
];

// Bootstrap model preserves empirical distribution
let bootstrap = EmpiricalBootstrap::new(100.0, &historical_returns);

let engine = MonteCarloEngine::reproducible(42);
let result = engine.simulate(&bootstrap, &TimeHorizon::days(252));

Risk Metrics

Value at Risk (VaR)

use aprender::monte_carlo::prelude::*;

// Historical VaR from return series
let returns = vec![-0.05, -0.02, 0.01, 0.03, 0.05, 0.02, -0.01, 0.04, -0.03, 0.00];

// 95% VaR: maximum loss at 95% confidence
let var_95 = VaR::historical(&returns, 0.95);
println!("95% VaR: {:.2}%", var_95 * 100.0);

// Multiple confidence levels
let var_90 = VaR::historical(&returns, 0.90);
let var_99 = VaR::historical(&returns, 0.99);

println!("VaR Ladder:");
println!("  90%: {:.2}%", var_90 * 100.0);
println!("  95%: {:.2}%", var_95 * 100.0);
println!("  99%: {:.2}%", var_99 * 100.0);

Conditional VaR (Expected Shortfall)

use aprender::monte_carlo::prelude::*;

let returns = vec![-0.05, -0.02, 0.01, 0.03, 0.05, 0.02, -0.01, 0.04, -0.03, 0.00];

// CVaR: expected loss given we exceed VaR
let cvar_95 = CVaR::from_returns(&returns, 0.95);
let var_95 = VaR::historical(&returns, 0.95);

println!("95% VaR: {:.2}%", var_95 * 100.0);
println!("95% CVaR: {:.2}%", cvar_95 * 100.0);
println!("CVaR captures tail risk beyond VaR");

// CVaR is always >= VaR (more conservative)
assert!(cvar_95 >= var_95 - 0.001);

Drawdown Analysis

use aprender::monte_carlo::prelude::*;

// Analyze drawdowns from simulation paths
let engine = MonteCarloEngine::reproducible(42)
    .with_n_simulations(1000);
let model = GeometricBrownianMotion::new(100.0, 0.08, 0.20);
let result = engine.simulate(&model, &TimeHorizon::years(5));

// Get drawdown statistics across all paths
let drawdown_stats = DrawdownAnalysis::from_paths(result.paths());

println!("Drawdown Statistics (5-year horizon):");
println!("  Mean Max Drawdown: {:.1}%", drawdown_stats.mean * 100.0);
println!("  Median Max Drawdown: {:.1}%", drawdown_stats.median * 100.0);
println!("  95th Percentile: {:.1}%", drawdown_stats.p95 * 100.0);
println!("  Worst Case: {:.1}%", drawdown_stats.max * 100.0);

Risk-Adjusted Ratios

use aprender::monte_carlo::prelude::*;

let returns = vec![0.02, 0.01, -0.01, 0.03, 0.02, -0.02, 0.01, 0.04, -0.01, 0.02];
let risk_free_rate = 0.02;  // 2% annual
let benchmark_returns = vec![0.01, 0.005, -0.005, 0.02, 0.01, -0.01, 0.005, 0.02, 0.0, 0.01];

// Sharpe Ratio: return per unit of total risk
let sharpe = sharpe_ratio(&returns, risk_free_rate);
println!("Sharpe Ratio: {:.2}", sharpe);

// Sortino Ratio: return per unit of downside risk
let sortino = sortino_ratio(&returns, risk_free_rate, 0.0);
println!("Sortino Ratio: {:.2}", sortino);

// Information Ratio: excess return vs benchmark per tracking error
let info_ratio = information_ratio(&returns, &benchmark_returns);
println!("Information Ratio: {:.2}", info_ratio);

// Treynor Ratio: return per unit of systematic risk
let beta = 1.2;
let treynor = treynor_ratio(&returns, risk_free_rate, beta);
println!("Treynor Ratio: {:.2}", treynor);

// Omega Ratio: probability-weighted gains/losses
let threshold = 0.0;
let omega = omega_ratio(&returns, threshold);
println!("Omega Ratio: {:.2}", omega);

// Jensen's Alpha: excess return over CAPM prediction
let market_return = 0.10;
let alpha = jensens_alpha(&returns, risk_free_rate, beta, market_return);
println!("Jensen's Alpha: {:.2}%", alpha * 100.0);

Comprehensive Risk Report

use aprender::monte_carlo::prelude::*;

fn generate_risk_report() {
    // Run simulation
    let engine = MonteCarloEngine::reproducible(42)
        .with_n_simulations(10_000)
        .with_variance_reduction(VarianceReduction::Antithetic);

    let model = GeometricBrownianMotion::new(100.0, 0.08, 0.20);
    let result = engine.simulate(&model, &TimeHorizon::years(1));

    // Generate comprehensive report
    let risk_free_rate = 0.02;
    let report = RiskReport::from_paths(result.paths(), risk_free_rate)
        .expect("Should generate report");

    // Print summary
    println!("{}", report.summary());

    // Or access individual metrics
    println!("\nKey Metrics:");
    println!("  95% VaR: {:.2}%", report.var_95 * 100.0);
    println!("  95% CVaR: {:.2}%", report.cvar_95 * 100.0);
    println!("  Sharpe Ratio: {:.2}", report.sharpe_ratio);
    println!("  Max Drawdown (median): {:.2}%", report.drawdown.median * 100.0);
}

Variance Reduction

Antithetic Variates

use aprender::monte_carlo::prelude::*;

// Without variance reduction
let engine_basic = MonteCarloEngine::reproducible(42)
    .with_n_simulations(10_000)
    .with_variance_reduction(VarianceReduction::None);

// With antithetic variates
let engine_antithetic = MonteCarloEngine::reproducible(42)
    .with_n_simulations(10_000)
    .with_variance_reduction(VarianceReduction::Antithetic);

let model = GeometricBrownianMotion::new(100.0, 0.08, 0.20);
let horizon = TimeHorizon::years(1);

let result_basic = engine_basic.simulate(&model, &horizon);
let result_antithetic = engine_antithetic.simulate(&model, &horizon);

let stats_basic = result_basic.final_value_statistics();
let stats_antithetic = result_antithetic.final_value_statistics();

println!("Basic - Mean: ${:.2}, Std: ${:.2}", stats_basic.mean, stats_basic.std);
println!("Antithetic - Mean: ${:.2}, Std: ${:.2}", stats_antithetic.mean, stats_antithetic.std);
// Antithetic should have lower standard error

Convergence Monitoring

use aprender::monte_carlo::prelude::*;

// Engine with convergence target
let engine = MonteCarloEngine::reproducible(42)
    .with_n_simulations(100_000)
    .with_target_precision(0.01)  // 1% relative precision
    .with_max_simulations(100_000);

let model = GeometricBrownianMotion::new(100.0, 0.08, 0.20);
let result = engine.simulate(&model, &TimeHorizon::years(1));

// Check convergence diagnostics
let diagnostics = result.diagnostics();
println!("Convergence Diagnostics:");
println!("  Paths used: {}", result.n_paths());
println!("  Converged: {}", diagnostics.is_converged(0.01));
println!("  Relative std error: {:.4}", diagnostics.relative_std_error());
println!("  Effective sample size: {:.0}", diagnostics.effective_sample_size());

Random Number Generation

use aprender::monte_carlo::prelude::*;

// Reproducible RNG
let mut rng = MonteCarloRng::new(42);

// Standard normal samples
let z1 = rng.normal(0.0, 1.0);
let z2 = rng.normal(0.0, 1.0);

// Uniform samples
let u = rng.uniform(0.0, 1.0);

// Exponential (for Poisson process)
let exp = rng.exponential(1.0);

// Same seed = same sequence
let mut rng2 = MonteCarloRng::new(42);
assert_eq!(rng2.normal(0.0, 1.0), z1);

Time Horizon Configuration

use aprender::monte_carlo::prelude::*;

// Various time horizons
let daily = TimeHorizon::days(252);      // 1 trading year
let weekly = TimeHorizon::weeks(52);     // 1 year
let monthly = TimeHorizon::months(12);   // 1 year
let yearly = TimeHorizon::years(5);      // 5 years

// Custom horizon
let custom = TimeHorizon::custom(
    0.5,    // Total time (0.5 years = 6 months)
    126,    // Number of steps
);

println!("Daily horizon: {} steps over {} years", daily.n_steps(), daily.total_time());

Portfolio Simulation

use aprender::monte_carlo::prelude::*;

fn simulate_portfolio() {
    let mut rng = MonteCarloRng::new(42);

    // Define assets
    let assets = vec![
        ("Stock A", 0.10, 0.25),  // (name, return, vol)
        ("Stock B", 0.08, 0.20),
        ("Bonds", 0.04, 0.05),
    ];

    let weights = vec![0.5, 0.3, 0.2];  // Portfolio weights
    let initial_value = 100_000.0;

    // Correlation matrix (simplified)
    let correlations = vec![
        vec![1.0, 0.6, 0.2],
        vec![0.6, 1.0, 0.3],
        vec![0.2, 0.3, 1.0],
    ];

    // Simulate 1000 portfolio paths
    let n_sims = 1000;
    let n_steps = 252;  // Daily for 1 year

    let mut portfolio_values: Vec<f64> = Vec::with_capacity(n_sims);

    for _ in 0..n_sims {
        let mut value = initial_value;

        for _ in 0..n_steps {
            // Simplified: uncorrelated returns for demo
            let mut portfolio_return = 0.0;
            for (i, &(_, mu, sigma)) in assets.iter().enumerate() {
                let daily_return = (mu / 252.0) + (sigma / 252.0_f64.sqrt()) * rng.normal(0.0, 1.0);
                portfolio_return += weights[i] * daily_return;
            }
            value *= 1.0 + portfolio_return;
        }

        portfolio_values.push(value);
    }

    // Calculate portfolio VaR
    let returns: Vec<f64> = portfolio_values.iter()
        .map(|&v| (v - initial_value) / initial_value)
        .collect();

    let var_95 = VaR::historical(&returns, 0.95);
    println!("Portfolio 95% VaR: ${:.0}", var_95 * initial_value);
}

Running Examples

# Run Monte Carlo examples
cargo run --example monte_carlo_basic
cargo run --example monte_carlo_risk
cargo run --example monte_carlo_portfolio

Feature Flags

The monte_carlo module is included by default. For the separate crate:

[dependencies]
aprender-monte-carlo = "0.1"

References

  • Glasserman (2003), "Monte Carlo Methods in Financial Engineering"
  • Jorion (2006), "Value at Risk"
  • Hull (2018), "Options, Futures, and Other Derivatives"