use ndarray::Array1;
use rayon::prelude::*;
use crate::traits::FloatExt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KnockInStyle {
EuropeanAtMaturity,
Discrete,
Continuous,
}
#[derive(Debug, Clone)]
pub struct AutocallablePricer {
pub s: f64,
pub notional: f64,
pub observation_times: Array1<f64>,
pub autocall_barrier: f64,
pub coupon_barrier: f64,
pub coupon: f64,
pub knock_in_barrier: f64,
pub knock_in_style: KnockInStyle,
pub memory: bool,
pub r: f64,
pub q: f64,
pub sigma: f64,
pub n_paths: usize,
pub steps_per_period: usize,
}
impl AutocallablePricer {
pub fn price(&self) -> f64 {
let n_obs = self.observation_times.len();
assert!(n_obs > 0, "need at least one observation date");
let auto_b = self.autocall_barrier * self.s;
let coup_b = self.coupon_barrier * self.s;
let ki_b = self.knock_in_barrier * self.s;
let times = self.observation_times.clone();
let style = self.knock_in_style;
let memory = self.memory;
let coupon = self.coupon;
let s0 = self.s;
let notional = self.notional;
let n_paths = self.n_paths;
let steps_per_period = self.steps_per_period.max(1);
let max_normals_per_path = match style {
KnockInStyle::Continuous => n_obs * steps_per_period,
_ => n_obs,
};
let mut all_z = vec![0.0_f64; n_paths * max_normals_per_path];
<f64 as FloatExt>::fill_standard_normal_slice(&mut all_z);
let sum: f64 = (0..n_paths)
.into_par_iter()
.map(|p| {
let z_path = &all_z[p * max_normals_per_path..(p + 1) * max_normals_per_path];
let mut z_idx = 0_usize;
let mut s_prev = s0;
let mut t_prev = 0.0;
let mut pv: f64 = 0.0;
let mut missed_coupons: f64 = 0.0;
let mut autocalled = false;
let mut autocall_time = 0.0;
let mut breached_ki = false;
let mut s_at_t: f64 = 0.0;
for i in 0..n_obs {
let t_i = times[i];
let dt = t_i - t_prev;
let sub_steps = match style {
KnockInStyle::Continuous => steps_per_period,
_ => 1,
};
let dt_sub = dt / sub_steps as f64;
let drift = (self.r - self.q - 0.5 * self.sigma * self.sigma) * dt_sub;
let vol = self.sigma * dt_sub.sqrt();
let mut s_curr = s_prev;
for _ in 0..sub_steps {
let z = z_path[z_idx];
z_idx += 1;
s_curr *= (drift + vol * z).exp();
if matches!(style, KnockInStyle::Continuous) && s_curr <= ki_b {
breached_ki = true;
}
}
if matches!(style, KnockInStyle::Discrete) && s_curr <= ki_b {
breached_ki = true;
}
s_at_t = s_curr;
if s_curr >= auto_b {
let cf = notional * (1.0 + coupon + missed_coupons);
pv += cf * (-self.r * t_i).exp();
autocalled = true;
autocall_time = t_i;
break;
}
if s_curr >= coup_b {
let cf = notional * (coupon + if memory { missed_coupons } else { 0.0 });
pv += cf * (-self.r * t_i).exp();
missed_coupons = 0.0;
} else if memory {
missed_coupons += coupon;
}
s_prev = s_curr;
t_prev = t_i;
}
if !autocalled {
if matches!(style, KnockInStyle::EuropeanAtMaturity) && s_at_t <= ki_b {
breached_ki = true;
}
let t_mat = times[n_obs - 1];
let principal = if breached_ki {
notional * (s_at_t / s0)
} else {
notional
};
pv += principal * (-self.r * t_mat).exp();
}
let _ = autocall_time;
pv
})
.sum();
sum / n_paths as f64
}
}
#[cfg(test)]
mod tests {
use ndarray::array;
use super::*;
fn quarterly_obs(years: f64, n: usize) -> Array1<f64> {
let dt = years / n as f64;
Array1::from_iter((1..=n).map(|i| i as f64 * dt))
}
#[test]
fn deep_in_autocall_redeems_first_period() {
let p = AutocallablePricer {
s: 100.0,
notional: 100.0,
observation_times: array![0.25, 0.5, 0.75, 1.0],
autocall_barrier: 0.5, coupon_barrier: 0.7,
coupon: 0.04,
knock_in_barrier: 0.6,
knock_in_style: KnockInStyle::EuropeanAtMaturity,
memory: false,
r: 0.03,
q: 0.0,
sigma: 0.20,
n_paths: 20_000,
steps_per_period: 4,
};
let price = p.price();
let expected = 100.0 * 1.04 * (-0.03_f64 * 0.25).exp();
let rel = (price - expected).abs() / expected;
assert!(rel < 0.01, "price={price}, expected={expected}, rel={rel}");
}
#[test]
fn memory_premium_over_phoenix() {
let base = AutocallablePricer {
s: 100.0,
notional: 100.0,
observation_times: quarterly_obs(2.0, 8),
autocall_barrier: 1.0,
coupon_barrier: 0.75,
coupon: 0.025,
knock_in_barrier: 0.65,
knock_in_style: KnockInStyle::EuropeanAtMaturity,
memory: false,
r: 0.03,
q: 0.01,
sigma: 0.30,
n_paths: 30_000,
steps_per_period: 4,
};
let phoenix = base.clone();
let athena = AutocallablePricer {
memory: true,
..base
};
assert!(athena.price() >= phoenix.price() - 1e-3);
}
#[test]
fn continuous_ki_below_european_ki() {
let base = AutocallablePricer {
s: 100.0,
notional: 100.0,
observation_times: quarterly_obs(1.0, 4),
autocall_barrier: 1.05,
coupon_barrier: 0.70,
coupon: 0.03,
knock_in_barrier: 0.65,
knock_in_style: KnockInStyle::EuropeanAtMaturity,
memory: false,
r: 0.03,
q: 0.0,
sigma: 0.30,
n_paths: 50_000,
steps_per_period: 8,
};
let euro = base.clone();
let cont = AutocallablePricer {
knock_in_style: KnockInStyle::Continuous,
..base
};
assert!(cont.price() <= euro.price() + 1e-2);
}
#[test]
fn zero_vol_spot_above_autocall() {
let p = AutocallablePricer {
s: 120.0,
notional: 100.0,
observation_times: array![0.5, 1.0],
autocall_barrier: 1.0, coupon_barrier: 0.5,
coupon: 0.05,
knock_in_barrier: 0.4,
knock_in_style: KnockInStyle::EuropeanAtMaturity,
memory: false,
r: 0.0,
q: 0.0,
sigma: 1e-6,
n_paths: 5_000,
steps_per_period: 1,
};
let mut p = p;
p.autocall_barrier = 0.95;
let price = p.price();
let expected = 100.0 * 1.05;
let rel = (price - expected).abs() / expected;
assert!(rel < 0.005, "price={price}");
}
}