use ndarray::Array1;
use ndarray::Array2;
use ndarray_linalg::LeastSquaresSvd;
use stochastic_rs_stochastic::diffusion::gbm_log::GbmLog;
use crate::traits::ProcessExt;
#[derive(Debug, Clone)]
pub struct BermudanLsmPricer {
pub r: f64,
pub tau: f64,
pub n_basis: usize,
}
impl BermudanLsmPricer {
pub fn new(r: f64, tau: f64, n_basis: usize) -> Self {
assert!(n_basis >= 2, "need at least 2 basis functions");
Self { r, tau, n_basis }
}
pub fn price<F>(&self, paths: &Array2<f64>, exercise_steps: &[usize], payoff: F) -> f64
where
F: Fn(f64) -> f64,
{
assert!(!exercise_steps.is_empty(), "no exercise dates");
let n_paths = paths.nrows();
let n_steps = paths.ncols() - 1;
assert!(
exercise_steps.iter().all(|&s| s <= n_steps),
"exercise step out of range"
);
let dt = self.tau / n_steps as f64;
let disc_step = (-self.r * dt).exp();
let last_idx = *exercise_steps.last().unwrap();
let mut cf = Array1::<f64>::zeros(n_paths);
let mut cf_time: Vec<usize> = vec![last_idx; n_paths];
for i in 0..n_paths {
cf[i] = payoff(paths[[i, last_idx]]);
}
for &step in exercise_steps.iter().rev().skip(1) {
let mut itm: Vec<usize> = Vec::new();
for i in 0..n_paths {
if payoff(paths[[i, step]]) > 0.0 {
itm.push(i);
}
}
if itm.len() <= self.n_basis {
continue;
}
let n_itm = itm.len();
let n_b = self.n_basis.min(n_itm);
let mut y_vals = Vec::with_capacity(n_itm);
for &idx in &itm {
let steps_to_cf = cf_time[idx] - step;
let disc_factor = disc_step.powi(steps_to_cf as i32);
y_vals.push(cf[idx] * disc_factor);
}
let a_mat = Array2::from_shape_fn((n_itm, n_b), |(row, col)| {
paths[[itm[row], step]].powi(col as i32)
});
let b_vec = Array1::from_vec(y_vals);
let beta = match a_mat.least_squares(&b_vec) {
Ok(result) => result.solution,
Err(_) => continue,
};
for &idx in &itm {
let s = paths[[idx, step]];
let continuation: f64 = (0..n_b).map(|j| beta[j] * s.powi(j as i32)).sum();
let exercise_val = payoff(s);
if exercise_val > continuation {
cf[idx] = exercise_val;
cf_time[idx] = step;
}
}
}
let mut total = 0.0;
for i in 0..n_paths {
let disc = disc_step.powi(cf_time[i] as i32);
total += cf[i] * disc;
}
total / n_paths as f64
}
}
pub fn generate_gbm_paths(
s0: f64,
r: f64,
q: f64,
sigma: f64,
t: f64,
n_paths: usize,
n_steps: usize,
) -> Array2<f64> {
let b = r - q;
let process = GbmLog::<f64>::new(
None,
Some(b),
None,
None,
sigma,
n_steps + 1,
Some(s0),
Some(t),
);
let samples = process.sample_par(n_paths);
let mut paths = Array2::<f64>::zeros((n_paths, n_steps + 1));
for (i, sample) in samples.into_iter().enumerate() {
paths.row_mut(i).assign(&sample);
}
paths
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bermudan_full_grid_above_european() {
let s0 = 100.0;
let k = 100.0;
let r = 0.05;
let sigma = 0.20;
let t = 1.0;
let n_paths = 20_000;
let n_steps = 50;
let paths = generate_gbm_paths(s0, r, 0.0, sigma, t, n_paths, n_steps);
let exercise: Vec<usize> = (1..=n_steps).collect();
let bermudan = BermudanLsmPricer::new(r, t, 4);
let payoff = |s: f64| (k - s).max(0.0);
let berm_price = bermudan.price(&paths, &exercise, payoff);
let mut eu_sum = 0.0;
for i in 0..n_paths {
eu_sum += payoff(paths[[i, n_steps]]);
}
let eu = eu_sum / n_paths as f64 * (-r * t).exp();
assert!(
berm_price >= eu * 0.99,
"Bermudan ({berm_price}) should be ~>= European ({eu})"
);
assert!(berm_price > 0.0 && berm_price < 50.0);
}
#[test]
fn bermudan_price_monotone_in_exercise_count() {
let s0 = 100.0;
let k = 100.0;
let r = 0.05;
let sigma = 0.30;
let t = 1.0;
let n_paths = 20_000;
let n_steps = 60;
let paths = generate_gbm_paths(s0, r, 0.0, sigma, t, n_paths, n_steps);
let bermudan = BermudanLsmPricer::new(r, t, 4);
let payoff = |s: f64| (k - s).max(0.0);
let mut prev = f64::NEG_INFINITY;
for n_exercise in [2usize, 4, 12, 30, 60] {
let stride = n_steps / n_exercise;
let exercise: Vec<usize> = (1..=n_exercise).map(|i| i * stride).collect();
let p = bermudan.price(&paths, &exercise, payoff);
assert!(
p >= prev - 0.05,
"n_exercise={n_exercise} gives {p}, prev={prev}"
);
prev = p;
}
}
#[test]
fn bermudan_single_exercise_equals_european() {
let s0 = 100.0;
let k = 105.0;
let r = 0.05;
let sigma = 0.20;
let t = 1.0;
let n_paths = 50_000;
let n_steps = 30;
let paths = generate_gbm_paths(s0, r, 0.0, sigma, t, n_paths, n_steps);
let bermudan = BermudanLsmPricer::new(r, t, 4);
let payoff = |s: f64| (k - s).max(0.0);
let berm = bermudan.price(&paths, &[n_steps], payoff);
let mut eu_sum = 0.0;
for i in 0..n_paths {
eu_sum += payoff(paths[[i, n_steps]]);
}
let eu = eu_sum / n_paths as f64 * (-r * t).exp();
let diff = (berm - eu).abs();
assert!(diff < 1e-9, "berm={berm}, eu={eu}");
}
}