use crate::error::{IntegrateError, IntegrateResult};
use scirs2_core::ndarray::{Array1, Array2};
use scirs2_core::random::{Normal, Rng, SeedableRng, StandardNormal};
use scirs2_core::random::seeded_rng;
use std::f64::consts::PI;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum OptionType {
Call,
Put,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AsianAveraging {
Arithmetic,
Geometric,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BarrierType {
UpAndOut,
DownAndOut,
UpAndIn,
DownAndIn,
}
#[derive(Debug, Clone)]
pub struct MonteCarloResult {
pub price: f64,
pub std_error: f64,
pub confidence_interval: (f64, f64),
pub n_paths: usize,
}
#[derive(Debug, Clone)]
pub struct OptionGreeks {
pub delta: f64,
pub gamma: f64,
pub vega: f64,
pub theta: f64,
pub rho: f64,
}
#[derive(Debug, Clone)]
pub struct MonteCarloEngine {
pub n_paths: usize,
pub n_steps: usize,
pub seed: u64,
pub antithetic: bool,
pub control_variates: bool,
}
impl MonteCarloEngine {
pub fn new(n_paths: usize, n_steps: usize, seed: u64) -> Self {
Self {
n_paths,
n_steps,
seed,
antithetic: false,
control_variates: false,
}
}
pub fn with_antithetic(mut self) -> Self {
self.antithetic = true;
self
}
pub fn with_control_variates(mut self) -> Self {
self.control_variates = true;
self
}
}
pub fn generate_gbm_paths(
s0: f64,
r: f64,
sigma: f64,
t: f64,
n_paths: usize,
n_steps: usize,
seed: u64,
antithetic: bool,
) -> IntegrateResult<Array2<f64>> {
if s0 <= 0.0 {
return Err(IntegrateError::ValueError(
"Initial stock price must be positive".to_string(),
));
}
if sigma < 0.0 {
return Err(IntegrateError::ValueError(
"Volatility must be non-negative".to_string(),
));
}
if t <= 0.0 {
return Err(IntegrateError::ValueError(
"Time to maturity must be positive".to_string(),
));
}
if n_paths == 0 || n_steps == 0 {
return Err(IntegrateError::ValueError(
"n_paths and n_steps must be positive".to_string(),
));
}
let dt = t / n_steps as f64;
let drift = (r - 0.5 * sigma * sigma) * dt;
let vol_sqrt_dt = sigma * dt.sqrt();
let mut rng = seeded_rng(seed);
let total_paths = if antithetic { n_paths * 2 } else { n_paths };
let half = n_paths;
let mut paths = Array2::zeros((total_paths, n_steps + 1));
for i in 0..total_paths {
paths[[i, 0]] = s0;
}
if antithetic {
let mut shocks: Vec<f64> = (0..n_steps).map(|_| rng.sample(StandardNormal)).collect();
for i in 0..half {
for (step_idx, shock) in shocks.iter_mut().enumerate() {
*shock = rng.sample(StandardNormal);
let _ = step_idx; }
let _ = shocks;
let mut s = s0;
let mut s_anti = s0;
let path_shocks: Vec<f64> = (0..n_steps).map(|_| rng.sample(StandardNormal)).collect();
for (j, &z) in path_shocks.iter().enumerate() {
s *= (drift + vol_sqrt_dt * z).exp();
s_anti *= (drift - vol_sqrt_dt * z).exp();
paths[[i, j + 1]] = s;
paths[[half + i, j + 1]] = s_anti;
}
}
} else {
for i in 0..n_paths {
let mut s = s0;
for j in 0..n_steps {
let z: f64 = rng.sample(StandardNormal);
s *= (drift + vol_sqrt_dt * z).exp();
paths[[i, j + 1]] = s;
}
}
}
Ok(paths)
}
fn normal_cdf(x: f64) -> f64 {
0.5 * (1.0 + libm::erf(x / std::f64::consts::SQRT_2))
}
fn bs_european_price(s0: f64, k: f64, r: f64, sigma: f64, t: f64, call: bool) -> f64 {
if t <= 0.0 || sigma <= 0.0 {
let intrinsic = if call { s0 - k } else { k - s0 };
return intrinsic.max(0.0);
}
let d1 = ((s0 / k).ln() + (r + 0.5 * sigma * sigma) * t) / (sigma * t.sqrt());
let d2 = d1 - sigma * t.sqrt();
if call {
s0 * normal_cdf(d1) - k * (-r * t).exp() * normal_cdf(d2)
} else {
k * (-r * t).exp() * normal_cdf(-d2) - s0 * normal_cdf(-d1)
}
}
fn geometric_asian_call(s0: f64, k: f64, r: f64, sigma: f64, t: f64, n: usize) -> f64 {
let n_f = n as f64;
let sigma_adj =
sigma * ((n_f + 1.0) * (2.0 * n_f + 1.0) / (6.0 * n_f * n_f)).sqrt();
let b = 0.5 * (r - 0.5 * sigma * sigma)
+ 0.5 * sigma_adj * sigma_adj;
let d1 = ((s0 / k).ln() + (b + 0.5 * sigma_adj * sigma_adj) * t)
/ (sigma_adj * t.sqrt());
let d2 = d1 - sigma_adj * t.sqrt();
(-(r - b) * t).exp()
* (s0 * (-((r - b) * t)).exp() * normal_cdf(d1)
- k * (-r * t).exp() * normal_cdf(d2))
}
fn build_result(payoffs: &[f64], n_paths: usize, r: f64, t: f64) -> MonteCarloResult {
let discount = (-r * t).exp();
let mean_payoff: f64 = payoffs.iter().sum::<f64>() / payoffs.len() as f64;
let price = discount * mean_payoff;
let variance: f64 = payoffs
.iter()
.map(|&p| (p - mean_payoff).powi(2))
.sum::<f64>()
/ (payoffs.len() as f64 - 1.0).max(1.0);
let std_error = discount * (variance / payoffs.len() as f64).sqrt();
let z_95 = 1.96;
let ci_lo = price - z_95 * std_error;
let ci_hi = price + z_95 * std_error;
MonteCarloResult {
price,
std_error,
confidence_interval: (ci_lo, ci_hi),
n_paths,
}
}
pub fn mc_european_option(
s0: f64,
k: f64,
r: f64,
sigma: f64,
t: f64,
option_type: OptionType,
engine: &MonteCarloEngine,
) -> IntegrateResult<MonteCarloResult> {
let paths = generate_gbm_paths(
s0,
r,
sigma,
t,
engine.n_paths,
engine.n_steps,
engine.seed,
engine.antithetic,
)?;
let total_paths = paths.nrows();
let last_col = engine.n_steps;
let is_call = option_type == OptionType::Call;
let mut payoffs: Vec<f64> = (0..total_paths)
.map(|i| {
let s_t = paths[[i, last_col]];
if is_call {
(s_t - k).max(0.0)
} else {
(k - s_t).max(0.0)
}
})
.collect();
if engine.control_variates {
let bs_price_val = bs_european_price(s0, k, r, sigma, t, is_call);
let bs_discount = (-r * t).exp();
let mc_bs_mean: f64 = payoffs.iter().sum::<f64>() / payoffs.len() as f64;
let control_correction = bs_discount * mc_bs_mean - bs_price_val;
for p in &mut payoffs {
*p -= control_correction / bs_discount;
}
}
Ok(build_result(&payoffs, engine.n_paths, r, t))
}
pub fn mc_asian_option(
s0: f64,
k: f64,
r: f64,
sigma: f64,
t: f64,
option_type: OptionType,
averaging: AsianAveraging,
engine: &MonteCarloEngine,
) -> IntegrateResult<MonteCarloResult> {
let paths = generate_gbm_paths(
s0,
r,
sigma,
t,
engine.n_paths,
engine.n_steps,
engine.seed,
engine.antithetic,
)?;
let total_paths = paths.nrows();
let n = engine.n_steps;
let is_call = option_type == OptionType::Call;
let mut payoffs: Vec<f64> = (0..total_paths)
.map(|i| {
let avg = match averaging {
AsianAveraging::Arithmetic => {
let sum: f64 = (1..=n).map(|j| paths[[i, j]]).sum();
sum / n as f64
}
AsianAveraging::Geometric => {
let log_sum: f64 = (1..=n).map(|j| paths[[i, j]].ln()).sum();
(log_sum / n as f64).exp()
}
};
if is_call {
(avg - k).max(0.0)
} else {
(k - avg).max(0.0)
}
})
.collect();
if engine.control_variates && averaging == AsianAveraging::Arithmetic && is_call {
let geo_cf = geometric_asian_call(s0, k, r, sigma, t, n);
let geo_payoffs: Vec<f64> = (0..total_paths)
.map(|i| {
let log_sum: f64 = (1..=n).map(|j| paths[[i, j]].ln()).sum();
let geo_avg = (log_sum / n as f64).exp();
(geo_avg - k).max(0.0)
})
.collect();
let geo_mc_mean: f64 = geo_payoffs.iter().sum::<f64>() / geo_payoffs.len() as f64;
let discount = (-r * t).exp();
let beta = {
let arith_mean: f64 = payoffs.iter().sum::<f64>() / payoffs.len() as f64;
let cov: f64 = payoffs
.iter()
.zip(geo_payoffs.iter())
.map(|(&a, &g)| (a - arith_mean) * (g - geo_mc_mean))
.sum::<f64>()
/ payoffs.len() as f64;
let var_geo: f64 = geo_payoffs
.iter()
.map(|&g| (g - geo_mc_mean).powi(2))
.sum::<f64>()
/ geo_payoffs.len() as f64;
if var_geo.abs() < 1e-15 {
0.0
} else {
cov / var_geo
}
};
let cf_geo_mc = geo_cf / discount; for (p, g) in payoffs.iter_mut().zip(geo_payoffs.iter()) {
*p -= beta * (g - cf_geo_mc);
}
}
Ok(build_result(&payoffs, engine.n_paths, r, t))
}
pub fn mc_barrier_option(
s0: f64,
k: f64,
r: f64,
sigma: f64,
t: f64,
barrier: f64,
option_type: OptionType,
barrier_type: BarrierType,
engine: &MonteCarloEngine,
) -> IntegrateResult<MonteCarloResult> {
if barrier <= 0.0 {
return Err(IntegrateError::ValueError(
"Barrier must be positive".to_string(),
));
}
let paths = generate_gbm_paths(
s0,
r,
sigma,
t,
engine.n_paths,
engine.n_steps,
engine.seed,
engine.antithetic,
)?;
let total_paths = paths.nrows();
let n = engine.n_steps;
let is_call = option_type == OptionType::Call;
let payoffs: Vec<f64> = (0..total_paths)
.map(|i| {
let mut crossed = false;
for j in 1..=n {
let s = paths[[i, j]];
crossed = match barrier_type {
BarrierType::UpAndOut | BarrierType::UpAndIn => s >= barrier,
BarrierType::DownAndOut | BarrierType::DownAndIn => s <= barrier,
};
if crossed {
break;
}
}
let active = match barrier_type {
BarrierType::UpAndOut | BarrierType::DownAndOut => !crossed,
BarrierType::UpAndIn | BarrierType::DownAndIn => crossed,
};
if active {
let s_t = paths[[i, n]];
if is_call {
(s_t - k).max(0.0)
} else {
(k - s_t).max(0.0)
}
} else {
0.0
}
})
.collect();
Ok(build_result(&payoffs, engine.n_paths, r, t))
}
pub fn mc_lookback_option(
s0: f64,
k: f64,
r: f64,
sigma: f64,
t: f64,
option_type: OptionType,
engine: &MonteCarloEngine,
) -> IntegrateResult<MonteCarloResult> {
let paths = generate_gbm_paths(
s0,
r,
sigma,
t,
engine.n_paths,
engine.n_steps,
engine.seed,
engine.antithetic,
)?;
let total_paths = paths.nrows();
let n = engine.n_steps;
let payoffs: Vec<f64> = (0..total_paths)
.map(|i| {
match option_type {
OptionType::Call => {
let s_max = (1..=n).map(|j| paths[[i, j]]).fold(f64::NEG_INFINITY, f64::max);
(s_max - k).max(0.0)
}
OptionType::Put => {
let s_min = (1..=n).map(|j| paths[[i, j]]).fold(f64::INFINITY, f64::min);
(k - s_min).max(0.0)
}
}
})
.collect();
Ok(build_result(&payoffs, engine.n_paths, r, t))
}
fn laguerre_basis(x: f64, order: usize) -> Vec<f64> {
let mut phi = vec![0.0; order];
if order == 0 {
return phi;
}
let ex = (-x / 2.0).exp();
phi[0] = ex;
if order > 1 {
phi[1] = ex * (1.0 - x);
}
if order > 2 {
phi[2] = ex * (1.0 - 2.0 * x + 0.5 * x * x);
}
if order > 3 {
phi[3] = ex * (1.0 - 3.0 * x + 1.5 * x * x - x * x * x / 6.0);
}
phi
}
fn ols_fit(x_mat: &[Vec<f64>], y: &[f64]) -> Vec<f64> {
let n = x_mat.len();
let p = if n > 0 { x_mat[0].len() } else { 0 };
if n == 0 || p == 0 {
return vec![0.0; p];
}
let mut xtx = vec![vec![0.0f64; p]; p];
let mut xty = vec![0.0f64; p];
for (row, yi) in x_mat.iter().zip(y.iter()) {
for j in 0..p {
xty[j] += row[j] * yi;
for k in 0..p {
xtx[j][k] += row[j] * row[k];
}
}
}
let mut aug: Vec<Vec<f64>> = (0..p)
.map(|j| {
let mut row = xtx[j].clone();
row.push(xty[j]);
row
})
.collect();
for col in 0..p {
let pivot = (col..p)
.max_by(|&a, &b| aug[a][col].abs().partial_cmp(&aug[b][col].abs()).expect("NaN"));
if let Some(pivot_row) = pivot {
aug.swap(col, pivot_row);
}
let pivot_val = aug[col][col];
if pivot_val.abs() < 1e-14 {
continue;
}
let factor = 1.0 / pivot_val;
for k in col..=p {
aug[col][k] *= factor;
}
for row in 0..p {
if row == col {
continue;
}
let mult = aug[row][col];
for k in col..=p {
let sub = mult * aug[col][k];
aug[row][k] -= sub;
}
}
}
(0..p).map(|j| aug[j][p]).collect()
}
pub fn mc_american_option_lsm(
s0: f64,
k: f64,
r: f64,
sigma: f64,
t: f64,
option_type: OptionType,
engine: &MonteCarloEngine,
basis_functions: usize,
) -> IntegrateResult<MonteCarloResult> {
if basis_functions == 0 {
return Err(IntegrateError::ValueError(
"basis_functions must be at least 1".to_string(),
));
}
let order = basis_functions.min(4);
let paths = generate_gbm_paths(
s0,
r,
sigma,
t,
engine.n_paths,
engine.n_steps,
engine.seed,
engine.antithetic,
)?;
let n_paths_total = paths.nrows();
let n = engine.n_steps;
let dt = t / n as f64;
let discount_factor = (-r * dt).exp();
let is_call = option_type == OptionType::Call;
let payoff_fn = |s: f64| -> f64 {
if is_call {
(s - k).max(0.0)
} else {
(k - s).max(0.0)
}
};
let mut cash_flows: Vec<f64> = (0..n_paths_total)
.map(|i| payoff_fn(paths[[i, n]]))
.collect();
for step in (1..n).rev() {
let in_money: Vec<usize> = (0..n_paths_total)
.filter(|&i| payoff_fn(paths[[i, step]]) > 0.0)
.collect();
if in_money.is_empty() {
for cf in &mut cash_flows {
*cf *= discount_factor;
}
continue;
}
let x_mat: Vec<Vec<f64>> = in_money
.iter()
.map(|&i| laguerre_basis(paths[[i, step]] / k, order))
.collect();
let y: Vec<f64> = in_money
.iter()
.map(|&i| cash_flows[i] * discount_factor)
.collect();
let coef = ols_fit(&x_mat, &y);
for (idx, &path_i) in in_money.iter().enumerate() {
let s = paths[[path_i, step]];
let basis = laguerre_basis(s / k, order);
let continuation: f64 = basis.iter().zip(coef.iter()).map(|(b, c)| b * c).sum();
let exercise = payoff_fn(s);
if exercise > continuation {
cash_flows[path_i] = exercise;
} else {
cash_flows[path_i] *= discount_factor;
}
}
let in_money_set: std::collections::HashSet<usize> =
in_money.into_iter().collect();
for i in 0..n_paths_total {
if !in_money_set.contains(&i) {
cash_flows[i] *= discount_factor;
}
}
}
let payoffs: Vec<f64> = cash_flows.iter().map(|&cf| cf * discount_factor).collect();
Ok(build_result(&payoffs, engine.n_paths, r, t))
}
pub fn mc_greeks(
s0: f64,
k: f64,
r: f64,
sigma: f64,
t: f64,
option_type: OptionType,
engine: &MonteCarloEngine,
) -> IntegrateResult<OptionGreeks> {
let ds = s0 * 0.01; let dv = 0.001; let dt = 1.0 / 365.0; let dr = 0.0001;
let base = mc_european_option(s0, k, r, sigma, t, option_type, engine)?;
let up_s = mc_european_option(s0 + ds, k, r, sigma, t, option_type, engine)?;
let dn_s = mc_european_option(s0 - ds, k, r, sigma, t, option_type, engine)?;
let delta = (up_s.price - dn_s.price) / (2.0 * ds);
let gamma = (up_s.price - 2.0 * base.price + dn_s.price) / (ds * ds);
let up_v = mc_european_option(s0, k, r, sigma + dv, t, option_type, engine)?;
let dn_v = mc_european_option(s0, k, r, sigma - dv, t, option_type, engine)?;
let vega = (up_v.price - dn_v.price) / (2.0 * dv);
let t_next = (t - dt).max(1e-6);
let theta_res = mc_european_option(s0, k, r, sigma, t_next, option_type, engine)?;
let theta = (theta_res.price - base.price) / dt;
let up_r = mc_european_option(s0, k, r + dr, sigma, t, option_type, engine)?;
let dn_r = mc_european_option(s0, k, r - dr, sigma, t, option_type, engine)?;
let rho = (up_r.price - dn_r.price) / (2.0 * dr);
Ok(OptionGreeks {
delta,
gamma,
vega,
theta,
rho,
})
}
pub fn generate_heston_paths(
s0: f64,
v0: f64,
r: f64,
kappa: f64,
theta: f64,
xi: f64,
rho: f64,
t: f64,
n_paths: usize,
n_steps: usize,
seed: u64,
) -> IntegrateResult<(Array2<f64>, Array2<f64>)> {
if s0 <= 0.0 || v0 < 0.0 {
return Err(IntegrateError::ValueError(
"s0 must be positive, v0 must be non-negative".to_string(),
));
}
if rho < -1.0 || rho > 1.0 {
return Err(IntegrateError::ValueError(
"Correlation rho must be in [-1, 1]".to_string(),
));
}
if kappa <= 0.0 || theta <= 0.0 || xi <= 0.0 {
return Err(IntegrateError::ValueError(
"kappa, theta, xi must be positive".to_string(),
));
}
let dt = t / n_steps as f64;
let sqrt_dt = dt.sqrt();
let sqrt_1m_rho2 = (1.0 - rho * rho).sqrt();
let mut rng = seeded_rng(seed);
let mut price_paths = Array2::zeros((n_paths, n_steps + 1));
let mut vol_paths = Array2::zeros((n_paths, n_steps + 1));
for i in 0..n_paths {
price_paths[[i, 0]] = s0;
vol_paths[[i, 0]] = v0;
let mut s = s0;
let mut v = v0;
for j in 0..n_steps {
let z1: f64 = rng.sample(StandardNormal);
let z2: f64 = rng.sample(StandardNormal);
let dw_s = z1;
let dw_v = rho * z1 + sqrt_1m_rho2 * z2;
let sqrt_v = v.sqrt();
v = (v + kappa * (theta - v) * dt + xi * sqrt_v * sqrt_dt * dw_v).max(0.0);
let drift_s = (r - 0.5 * v) * dt;
s *= (drift_s + sqrt_v * sqrt_dt * dw_s).exp();
price_paths[[i, j + 1]] = s;
vol_paths[[i, j + 1]] = v;
}
}
Ok((price_paths, vol_paths))
}
pub fn mc_heston_european(
s0: f64,
v0: f64,
k: f64,
r: f64,
kappa: f64,
theta: f64,
xi: f64,
rho: f64,
t: f64,
option_type: OptionType,
engine: &MonteCarloEngine,
) -> IntegrateResult<MonteCarloResult> {
let (price_paths, _vol_paths) = generate_heston_paths(
s0,
v0,
r,
kappa,
theta,
xi,
rho,
t,
engine.n_paths,
engine.n_steps,
engine.seed,
)?;
let n = engine.n_steps;
let is_call = option_type == OptionType::Call;
let payoffs: Vec<f64> = (0..engine.n_paths)
.map(|i| {
let s_t = price_paths[[i, n]];
if is_call {
(s_t - k).max(0.0)
} else {
(k - s_t).max(0.0)
}
})
.collect();
Ok(build_result(&payoffs, engine.n_paths, r, t))
}
pub fn mc_portfolio_var(
initial_prices: &Array1<f64>,
weights: &Array1<f64>,
mu: &Array1<f64>,
cov_matrix: &Array2<f64>,
horizon: f64,
confidence: f64,
n_scenarios: usize,
seed: u64,
) -> IntegrateResult<(f64, f64)> {
let n_assets = initial_prices.len();
if weights.len() != n_assets || mu.len() != n_assets || cov_matrix.nrows() != n_assets
|| cov_matrix.ncols() != n_assets
{
return Err(IntegrateError::DimensionMismatch(
"All inputs must have consistent asset dimensions".to_string(),
));
}
if confidence <= 0.0 || confidence >= 1.0 {
return Err(IntegrateError::ValueError(
"Confidence must be in (0, 1)".to_string(),
));
}
if horizon <= 0.0 {
return Err(IntegrateError::ValueError(
"Horizon must be positive".to_string(),
));
}
if n_scenarios == 0 {
return Err(IntegrateError::ValueError(
"n_scenarios must be positive".to_string(),
));
}
let chol = cholesky_lower(cov_matrix, n_assets)?;
let mut rng = seeded_rng(seed);
let portfolio_value: f64 = initial_prices
.iter()
.zip(weights.iter())
.map(|(p, w)| p * w)
.sum();
let mut pnl: Vec<f64> = Vec::with_capacity(n_scenarios);
for _ in 0..n_scenarios {
let z: Vec<f64> = (0..n_assets).map(|_| rng.sample(StandardNormal)).collect();
let mut eps = vec![0.0f64; n_assets];
for i in 0..n_assets {
for j in 0..=i {
eps[i] += chol[i][j] * z[j];
}
}
let mut new_value = 0.0f64;
for i in 0..n_assets {
let log_return = (mu[i] - 0.5 * cov_matrix[[i, i]]) * horizon
+ horizon.sqrt() * eps[i];
let new_price = initial_prices[i] * log_return.exp();
new_value += weights[i] * new_price;
}
pnl.push(new_value - portfolio_value);
}
pnl.sort_by(|a, b| a.partial_cmp(b).expect("NaN in PnL"));
let var_index = ((1.0 - confidence) * n_scenarios as f64).floor() as usize;
let var_index = var_index.min(n_scenarios - 1);
let var = -pnl[var_index];
let tail: Vec<f64> = pnl[..=var_index].iter().map(|&x| -x).collect();
let cvar = tail.iter().sum::<f64>() / tail.len().max(1) as f64;
Ok((var, cvar))
}
fn cholesky_lower(a: &Array2<f64>, n: usize) -> IntegrateResult<Vec<Vec<f64>>> {
let mut l = vec![vec![0.0f64; n]; n];
for i in 0..n {
for j in 0..=i {
let s: f64 = (0..j).map(|k| l[i][k] * l[j][k]).sum();
if i == j {
let diag = a[[i, i]] - s;
if diag < 0.0 {
return Err(IntegrateError::ValueError(format!(
"Covariance matrix is not positive definite at diagonal ({}, {})",
i, i
)));
}
l[i][j] = diag.sqrt();
} else {
let ljj = l[j][j];
if ljj.abs() < 1e-14 {
l[i][j] = 0.0;
} else {
l[i][j] = (a[[i, j]] - s) / ljj;
}
}
}
}
Ok(l)
}
pub fn sobol_sequence(n_samples: usize, n_dimensions: usize, skip: usize) -> Array2<f64> {
assert!(
n_dimensions <= 10,
"sobol_sequence supports at most 10 dimensions"
);
assert!(n_samples > 0, "n_samples must be positive");
let direction_data: &[(usize, &[u32])] = &[
(1, &[1]), (2, &[1, 1]), (3, &[1, 1, 1]), (3, &[1, 3, 7]), (4, &[1, 1, 5, 11]), (4, &[1, 3, 13, 11]), (5, &[1, 1, 19, 25, 7]), (5, &[1, 3, 7, 11, 1]), (5, &[1, 1, 5, 1, 15]), ];
let max_bits: usize = 32;
let scale = (u32::MAX as f64) + 1.0;
let mut v: Vec<Vec<u32>> = Vec::with_capacity(n_dimensions);
let mut v0 = vec![0u32; max_bits];
for i in 0..max_bits {
v0[i] = 1u32 << (max_bits - 1 - i);
}
v.push(v0);
for d in 1..n_dimensions {
let (s, m_init) = direction_data[d - 1];
let mut vd = vec![0u32; max_bits];
for i in 0..s.min(max_bits) {
vd[i] = if i < m_init.len() {
m_init[i] << (max_bits - 1 - i)
} else {
0
};
}
for i in s..max_bits {
let prev_s = vd[i - s];
let mut new_val = prev_s ^ (prev_s >> s);
for k in 1..s {
new_val ^= vd[i - k];
}
vd[i] = new_val;
}
v.push(vd);
}
let mut result = Array2::zeros((n_samples, n_dimensions));
let mut x = vec![0u32; n_dimensions];
for idx in 0..skip {
let c = gray_code_trailing_zeros(idx + 1);
for d in 0..n_dimensions {
x[d] ^= if c < max_bits { v[d][c] } else { 0 };
}
}
for sample in 0..n_samples {
for d in 0..n_dimensions {
result[[sample, d]] = x[d] as f64 / scale;
}
let c = gray_code_trailing_zeros(skip + sample + 1);
for d in 0..n_dimensions {
x[d] ^= if c < max_bits { v[d][c] } else { 0 };
}
}
result
}
fn gray_code_trailing_zeros(n: usize) -> usize {
if n == 0 {
return 0;
}
n.trailing_zeros() as usize
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
const SEED: u64 = 42;
const N_PATHS: usize = 20_000;
const N_STEPS: usize = 252;
const TOL: f64 = 0.05;
fn bs_call(s0: f64, k: f64, r: f64, sigma: f64, t: f64) -> f64 {
bs_european_price(s0, k, r, sigma, t, true)
}
fn bs_put(s0: f64, k: f64, r: f64, sigma: f64, t: f64) -> f64 {
bs_european_price(s0, k, r, sigma, t, false)
}
#[test]
fn test_gbm_path_initial_price() {
let paths = generate_gbm_paths(100.0, 0.05, 0.2, 1.0, 1000, 52, SEED, false)
.expect("gbm paths failed");
for i in 0..1000 {
assert_relative_eq!(paths[[i, 0]], 100.0, epsilon = 1e-10);
}
}
#[test]
fn test_gbm_path_mean_matches_theory() {
let s0 = 100.0;
let r = 0.05;
let t = 1.0;
let paths = generate_gbm_paths(s0, r, 0.3, t, 50_000, N_STEPS, SEED, false)
.expect("gbm paths failed");
let last_col = N_STEPS;
let mean_st: f64 = (0..50_000).map(|i| paths[[i, last_col]]).sum::<f64>() / 50_000.0;
let expected_mean = s0 * (r * t).exp();
assert!(
(mean_st / expected_mean - 1.0).abs() < 0.02,
"GBM mean mismatch: MC={:.4} theory={:.4}",
mean_st,
expected_mean
);
}
#[test]
fn test_gbm_path_variance_matches_theory() {
let s0 = 100.0;
let r = 0.0; let sigma = 0.2;
let t = 1.0;
let n = 50_000;
let paths = generate_gbm_paths(s0, r, sigma, t, n, N_STEPS, SEED, false)
.expect("gbm paths failed");
let last_col = N_STEPS;
let vals: Vec<f64> = (0..n).map(|i| paths[[i, last_col]]).collect();
let mean = vals.iter().sum::<f64>() / n as f64;
let var = vals.iter().map(|&s| (s - mean).powi(2)).sum::<f64>() / (n - 1) as f64;
let theoretical_var =
s0 * s0 * (2.0 * r * t).exp() * ((sigma * sigma * t).exp() - 1.0);
assert!(
(var / theoretical_var - 1.0).abs() < 0.05,
"GBM variance mismatch: MC={:.2} theory={:.2}",
var,
theoretical_var
);
}
#[test]
fn test_gbm_antithetic_doubles_paths() {
let paths = generate_gbm_paths(100.0, 0.05, 0.2, 1.0, 1000, 52, SEED, true)
.expect("gbm paths failed");
assert_eq!(paths.nrows(), 2000);
}
#[test]
fn test_mc_european_call_vs_bs() {
let engine = MonteCarloEngine::new(N_PATHS, N_STEPS, SEED).with_antithetic();
let result =
mc_european_option(100.0, 100.0, 0.05, 0.2, 1.0, OptionType::Call, &engine)
.expect("mc european call failed");
let bs = bs_call(100.0, 100.0, 0.05, 0.2, 1.0);
assert!(
(result.price - bs).abs() < TOL * bs,
"Call price mismatch: MC={:.4} BS={:.4}",
result.price,
bs
);
}
#[test]
fn test_mc_european_put_vs_bs() {
let engine = MonteCarloEngine::new(N_PATHS, N_STEPS, SEED).with_antithetic();
let result =
mc_european_option(100.0, 100.0, 0.05, 0.2, 1.0, OptionType::Put, &engine)
.expect("mc european put failed");
let bs = bs_put(100.0, 100.0, 0.05, 0.2, 1.0);
assert!(
(result.price - bs).abs() < TOL * bs,
"Put price mismatch: MC={:.4} BS={:.4}",
result.price,
bs
);
}
#[test]
fn test_put_call_parity() {
let (s0, k, r, sigma, t) = (100.0, 100.0, 0.05, 0.2, 1.0);
let engine = MonteCarloEngine::new(N_PATHS, N_STEPS, SEED).with_antithetic();
let call =
mc_european_option(s0, k, r, sigma, t, OptionType::Call, &engine).expect("call failed");
let put =
mc_european_option(s0, k, r, sigma, t, OptionType::Put, &engine).expect("put failed");
let pcp_lhs = call.price - put.price;
let pcp_rhs = s0 - k * (-r * t).exp();
assert!(
(pcp_lhs - pcp_rhs).abs() < 0.5,
"Put-call parity violated: C-P={:.4} theory={:.4}",
pcp_lhs,
pcp_rhs
);
}
#[test]
fn test_mc_european_result_has_valid_ci() {
let engine = MonteCarloEngine::new(5000, 100, SEED);
let result =
mc_european_option(100.0, 100.0, 0.05, 0.2, 1.0, OptionType::Call, &engine)
.expect("mc failed");
let (lo, hi) = result.confidence_interval;
assert!(lo < result.price && result.price < hi);
assert!(result.std_error > 0.0);
}
#[test]
fn test_asian_geometric_less_than_arithmetic() {
let engine = MonteCarloEngine::new(N_PATHS, N_STEPS, SEED);
let arith = mc_asian_option(
100.0, 100.0, 0.05, 0.2, 1.0, OptionType::Call,
AsianAveraging::Arithmetic, &engine,
).expect("arith asian failed");
let geom = mc_asian_option(
100.0, 100.0, 0.05, 0.2, 1.0, OptionType::Call,
AsianAveraging::Geometric, &engine,
).expect("geom asian failed");
assert!(
geom.price <= arith.price * 1.05, "Geometric price ({:.4}) should be <= arithmetic ({:.4})",
geom.price,
arith.price
);
}
#[test]
fn test_asian_put_positive_price() {
let engine = MonteCarloEngine::new(5000, 100, SEED);
let result = mc_asian_option(
100.0, 100.0, 0.05, 0.2, 1.0, OptionType::Put,
AsianAveraging::Arithmetic, &engine,
).expect("asian put failed");
assert!(result.price > 0.0);
}
#[test]
fn test_asian_cheaper_than_european() {
let engine = MonteCarloEngine::new(N_PATHS, N_STEPS, SEED);
let asian = mc_asian_option(
100.0, 100.0, 0.05, 0.2, 1.0, OptionType::Call,
AsianAveraging::Arithmetic, &engine,
).expect("asian failed");
let european = mc_european_option(
100.0, 100.0, 0.05, 0.2, 1.0, OptionType::Call, &engine,
).expect("european failed");
assert!(
asian.price < european.price * 1.01,
"Asian ({:.4}) should be cheaper than European ({:.4})",
asian.price,
european.price
);
}
#[test]
fn test_barrier_up_and_out_knocked_out_all() {
let engine = MonteCarloEngine::new(2000, 100, SEED);
let result = mc_barrier_option(
150.0, 100.0, 0.05, 0.2, 1.0, 100.0, OptionType::Call, BarrierType::DownAndOut, &engine,
).expect("barrier failed");
assert!(result.price >= 0.0);
}
#[test]
fn test_barrier_up_and_out_reduces_price() {
let engine = MonteCarloEngine::new(N_PATHS, N_STEPS, SEED);
let european = mc_european_option(
100.0, 100.0, 0.05, 0.2, 1.0, OptionType::Call, &engine,
).expect("european failed");
let uao = mc_barrier_option(
100.0, 100.0, 0.05, 0.2, 1.0, 120.0, OptionType::Call, BarrierType::UpAndOut, &engine,
).expect("barrier failed");
assert!(
uao.price < european.price,
"Up-and-out ({:.4}) should be < European ({:.4})",
uao.price,
european.price
);
}
#[test]
fn test_barrier_down_and_out_below_spot() {
let engine = MonteCarloEngine::new(N_PATHS, N_STEPS, SEED);
let european = mc_european_option(
100.0, 100.0, 0.05, 0.2, 1.0, OptionType::Call, &engine,
).expect("european failed");
let dao = mc_barrier_option(
100.0, 100.0, 0.05, 0.2, 1.0, 10.0, OptionType::Call, BarrierType::DownAndOut, &engine,
).expect("barrier failed");
assert!(
(dao.price - european.price).abs() / european.price < 0.15,
"DownAndOut ({:.4}) should be close to European ({:.4}) with low barrier",
dao.price,
european.price
);
}
#[test]
fn test_lookback_call_more_expensive_than_european() {
let engine = MonteCarloEngine::new(N_PATHS, N_STEPS, SEED);
let lookback = mc_lookback_option(
100.0, 100.0, 0.05, 0.2, 1.0, OptionType::Call, &engine,
).expect("lookback failed");
let european = mc_european_option(
100.0, 100.0, 0.05, 0.2, 1.0, OptionType::Call, &engine,
).expect("european failed");
assert!(
lookback.price >= european.price * 0.9,
"Lookback ({:.4}) should be >= European ({:.4})",
lookback.price,
european.price
);
}
#[test]
fn test_american_put_more_expensive_than_european() {
let engine = MonteCarloEngine::new(N_PATHS, N_STEPS, SEED);
let american = mc_american_option_lsm(
100.0, 100.0, 0.05, 0.2, 1.0, OptionType::Put, &engine, 3,
).expect("american lsm failed");
let european = mc_european_option(
100.0, 100.0, 0.05, 0.2, 1.0, OptionType::Put, &engine,
).expect("european failed");
assert!(
american.price >= european.price * 0.95,
"American ({:.4}) should be >= European ({:.4})",
american.price,
european.price
);
}
#[test]
fn test_greeks_delta_call_positive() {
let engine = MonteCarloEngine::new(N_PATHS, N_STEPS, SEED);
let greeks =
mc_greeks(100.0, 100.0, 0.05, 0.2, 1.0, OptionType::Call, &engine)
.expect("greeks failed");
assert!(
greeks.delta > 0.3 && greeks.delta < 0.7,
"Call delta={:.4}",
greeks.delta
);
}
#[test]
fn test_greeks_delta_put_negative() {
let engine = MonteCarloEngine::new(N_PATHS, N_STEPS, SEED);
let greeks =
mc_greeks(100.0, 100.0, 0.05, 0.2, 1.0, OptionType::Put, &engine)
.expect("greeks failed");
assert!(
greeks.delta < 0.0,
"Put delta should be negative, got {:.4}",
greeks.delta
);
}
#[test]
fn test_greeks_vega_positive() {
let engine = MonteCarloEngine::new(N_PATHS, N_STEPS, SEED);
let greeks =
mc_greeks(100.0, 100.0, 0.05, 0.2, 1.0, OptionType::Call, &engine)
.expect("greeks failed");
assert!(greeks.vega > 0.0, "Vega should be positive, got {:.4}", greeks.vega);
}
#[test]
fn test_heston_paths_shape() {
let n_paths = 500;
let n_steps = 100;
let (price_paths, vol_paths) = generate_heston_paths(
100.0, 0.04, 0.05, 2.0, 0.04, 0.3, -0.7, 1.0, n_paths, n_steps, SEED,
).expect("heston failed");
assert_eq!(price_paths.shape(), &[n_paths, n_steps + 1]);
assert_eq!(vol_paths.shape(), &[n_paths, n_steps + 1]);
}
#[test]
fn test_heston_paths_positive_prices() {
let (price_paths, _) = generate_heston_paths(
100.0, 0.04, 0.05, 2.0, 0.04, 0.3, -0.7, 1.0, 200, 100, SEED,
).expect("heston failed");
for i in 0..200 {
for j in 0..=100 {
assert!(price_paths[[i, j]] > 0.0, "Non-positive price at [{}, {}]", i, j);
}
}
}
#[test]
fn test_heston_european_call_positive() {
let engine = MonteCarloEngine::new(5000, 100, SEED);
let result = mc_heston_european(
100.0, 0.04, 100.0, 0.05, 2.0, 0.04, 0.3, -0.7, 1.0, OptionType::Call, &engine,
).expect("heston european failed");
assert!(result.price > 0.0);
let bs = bs_call(100.0, 100.0, 0.05, 0.2, 1.0);
assert!(result.price > bs * 0.5 && result.price < bs * 2.0);
}
#[test]
fn test_portfolio_var_positive() {
use scirs2_core::ndarray::array;
let prices = array![100.0, 200.0];
let weights = array![0.5, 0.5];
let mu = array![0.08, 0.10];
let cov = Array2::from_shape_vec((2, 2), vec![0.04, 0.01, 0.01, 0.09]).expect("cov");
let (var, cvar) = mc_portfolio_var(&prices, &weights, &mu, &cov, 1.0 / 252.0, 0.99, 10000, SEED)
.expect("var failed");
assert!(var >= 0.0, "VaR should be non-negative, got {:.4}", var);
assert!(cvar >= var * 0.9, "CVaR ({:.4}) should be >= VaR ({:.4})", cvar, var);
}
#[test]
fn test_portfolio_var_cvar_ordering() {
use scirs2_core::ndarray::array;
let prices = array![100.0];
let weights = array![1.0];
let mu = array![0.05];
let cov = Array2::from_shape_vec((1, 1), vec![0.04]).expect("cov");
let (var, cvar) = mc_portfolio_var(&prices, &weights, &mu, &cov, 1.0 / 52.0, 0.95, 5000, SEED)
.expect("var failed");
assert!(cvar >= var * 0.95, "CVaR ({:.4}) must be >= VaR ({:.4})", cvar, var);
}
#[test]
fn test_portfolio_var_invalid_confidence() {
use scirs2_core::ndarray::array;
let prices = array![100.0];
let weights = array![1.0];
let mu = array![0.05];
let cov = Array2::from_shape_vec((1, 1), vec![0.04]).expect("cov");
let result =
mc_portfolio_var(&prices, &weights, &mu, &cov, 1.0 / 52.0, 1.5, 100, SEED);
assert!(result.is_err());
}
#[test]
fn test_sobol_sequence_shape() {
let seq = sobol_sequence(100, 4, 0);
assert_eq!(seq.shape(), &[100, 4]);
}
#[test]
fn test_sobol_sequence_range() {
let seq = sobol_sequence(200, 5, 0);
for val in seq.iter() {
assert!(*val >= 0.0 && *val < 1.0, "Sobol value out of range: {}", val);
}
}
#[test]
fn test_sobol_1d_uniformity() {
let n = 256;
let seq = sobol_sequence(n, 1, 0);
let mean: f64 = seq.iter().sum::<f64>() / n as f64;
assert!(
(mean - 0.5).abs() < 0.05,
"Sobol 1D mean={:.4} should be ~0.5",
mean
);
}
#[test]
fn test_sobol_distinct_points() {
let seq = sobol_sequence(64, 2, 0);
let mut points: Vec<(u64, u64)> = seq
.rows()
.into_iter()
.map(|r| (r[0].to_bits(), r[1].to_bits()))
.collect();
points.sort();
points.dedup();
assert_eq!(points.len(), 64);
}
#[test]
fn test_sobol_skip() {
let seq_a = sobol_sequence(10, 2, 0);
let seq_b = sobol_sequence(10, 2, 10);
assert!((seq_a[[0, 0]] - seq_b[[0, 0]]).abs() > 1e-10);
}
}