#[derive(Debug, Clone, Copy)]
pub struct VarianceSwapPricer {
pub s: f64,
pub r: f64,
pub q: f64,
pub t: f64,
}
impl VarianceSwapPricer {
pub fn forward(&self) -> f64 {
self.s * ((self.r - self.q) * self.t).exp()
}
pub fn fair_strike_bsm(&self, sigma: f64) -> f64 {
sigma * sigma
}
pub fn fair_strike_replication(&self, strikes: &[f64], otm_prices: &[f64]) -> f64 {
assert_eq!(
strikes.len(),
otm_prices.len(),
"strikes / prices length mismatch"
);
let n = strikes.len();
if n < 2 || self.t <= 0.0 {
return 0.0;
}
debug_assert!(
strikes.windows(2).all(|w| w[0] <= w[1]),
"strikes must be sorted ascending"
);
let fwd = self.forward();
let disc = (self.r * self.t).exp();
let k0_idx = strikes
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| (*a - fwd).abs().partial_cmp(&(*b - fwd).abs()).unwrap())
.map(|(i, _)| i)
.unwrap_or(0);
let k0 = strikes[k0_idx];
let mut integral = 0.0;
for i in 0..n {
let dk = if i == 0 {
strikes[1] - strikes[0]
} else if i == n - 1 {
strikes[n - 1] - strikes[n - 2]
} else {
0.5 * (strikes[i + 1] - strikes[i - 1])
};
integral += dk * otm_prices[i] / (strikes[i] * strikes[i]);
}
let drift = (self.r - self.q) * self.t;
let fair = (2.0 / self.t) * (drift - (fwd / k0 - 1.0) - (k0 / self.s).ln() + disc * integral);
fair.max(0.0)
}
pub fn fair_strike_heston(&self, v0: f64, kappa: f64, theta: f64) -> f64 {
let t = self.t;
if t <= 0.0 {
return v0;
}
if kappa.abs() < 1e-10 {
return v0;
}
let factor = (1.0 - (-kappa * t).exp()) / (kappa * t);
theta + (v0 - theta) * factor
}
pub fn fair_strike_heston_discrete(
&self,
v0: f64,
kappa: f64,
theta: f64,
sigma: f64,
rho: f64,
n_obs: usize,
) -> f64 {
let cont = self.fair_strike_heston(v0, kappa, theta);
if n_obs == 0 {
return cont;
}
let t = self.t;
let dt = t / n_obs as f64;
let xi = cont;
let bias = 0.25 * xi * xi * dt + rho * sigma * xi * dt;
cont + bias
}
pub fn realized_variance(prices: &[f64], dt: f64) -> f64 {
if prices.len() < 2 {
return 0.0;
}
let n = prices.len() - 1;
let mut rv = 0.0;
for i in 1..=n {
let lr = (prices[i] / prices[i - 1]).ln();
rv += lr * lr;
}
rv / (n as f64 * dt)
}
pub fn pnl(realized_var: f64, fair_strike: f64, notional: f64) -> f64 {
notional * (realized_var - fair_strike)
}
}
pub fn replication_weights(strikes: &[f64], maturity: f64) -> Vec<f64> {
let n = strikes.len();
let mut w = vec![0.0; n];
if n < 2 || maturity <= 0.0 {
return w;
}
for i in 0..n {
let dk = if i == 0 {
strikes[1] - strikes[0]
} else if i == n - 1 {
strikes[n - 1] - strikes[n - 2]
} else {
0.5 * (strikes[i + 1] - strikes[i - 1])
};
w[i] = (2.0 / maturity) * dk / (strikes[i] * strikes[i]);
}
w
}
pub struct VolatilitySwapPricer;
impl VolatilitySwapPricer {
pub fn fair_strike_bsm(sigma: f64) -> f64 {
sigma
}
pub fn fair_strike_from_var(k_var: f64, var_of_var: f64) -> f64 {
if k_var <= 0.0 {
return 0.0;
}
k_var.sqrt() - var_of_var / (8.0 * k_var.powf(1.5))
}
pub fn fair_strike_heston(v0: f64, kappa: f64, theta: f64, sigma: f64, t: f64) -> f64 {
let pricer = VarianceSwapPricer {
s: 1.0,
r: 0.0,
q: 0.0,
t,
};
let k_var = pricer.fair_strike_heston(v0, kappa, theta);
if kappa.abs() < 1e-10 || t <= 0.0 {
return k_var.max(0.0).sqrt();
}
let dispersion = (sigma * sigma * (v0 - theta).powi(2) * (1.0 - (-2.0 * kappa * t).exp()))
/ (2.0 * kappa.powi(3) * t * t);
Self::fair_strike_from_var(k_var, dispersion.max(0.0))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn pricer() -> VarianceSwapPricer {
VarianceSwapPricer {
s: 100.0,
r: 0.05,
q: 0.0,
t: 1.0,
}
}
#[test]
fn bsm_fair_strike_is_sigma_squared() {
assert!((pricer().fair_strike_bsm(0.2) - 0.04).abs() < 1e-12);
}
#[test]
fn forward_under_zero_dividend() {
let p = pricer();
assert!((p.forward() - 100.0 * 0.05_f64.exp()).abs() < 1e-10);
}
#[test]
fn realized_variance_constant_path_is_zero() {
let prices = vec![100.0; 252];
let rv = VarianceSwapPricer::realized_variance(&prices, 1.0 / 252.0);
assert!((rv - 0.0).abs() < 1e-15);
}
#[test]
fn realized_variance_recovers_known_drift() {
let dt: f64 = 1.0 / 252.0;
let daily = 0.20 * dt.sqrt();
let prices: Vec<f64> = (0..253).map(|i| 100.0 * (daily * i as f64).exp()).collect();
let rv = VarianceSwapPricer::realized_variance(&prices, dt);
assert!((rv - 0.04).abs() < 0.005, "rv={rv}, expected≈0.04");
}
#[test]
fn pnl_scales_with_notional() {
assert!((VarianceSwapPricer::pnl(0.06, 0.04, 100_000.0) - 2_000.0).abs() < 1e-9);
}
#[test]
fn vol_swap_convexity_lowers_strike() {
let k_vol = VolatilitySwapPricer::fair_strike_from_var(0.04, 0.001);
assert!(k_vol < 0.04_f64.sqrt());
assert!(k_vol > 0.0);
}
#[test]
fn vol_swap_zero_dispersion_recovers_sqrt_var() {
let k_vol = VolatilitySwapPricer::fair_strike_from_var(0.04, 0.0);
assert!((k_vol - 0.2).abs() < 1e-10);
}
#[test]
fn heston_fair_strike_equals_v0_when_at_long_run_mean() {
let p = pricer();
let k_var = p.fair_strike_heston(0.04, 1.5, 0.04);
assert!((k_var - 0.04).abs() < 1e-12);
}
#[test]
fn heston_fair_strike_blends_v0_to_theta() {
let p = pricer();
let k_strong = p.fair_strike_heston(0.09, 5.0, 0.04);
let k_weak = p.fair_strike_heston(0.09, 0.1, 0.04);
assert!(k_strong < k_weak);
assert!(k_weak <= 0.09);
assert!(k_strong > 0.04);
}
#[test]
fn heston_kappa_zero_limit_equals_v0() {
let p = pricer();
assert!((p.fair_strike_heston(0.04, 0.0, 0.10) - 0.04).abs() < 1e-12);
}
#[test]
fn heston_long_t_limit_approaches_theta() {
let p = VarianceSwapPricer {
s: 100.0,
r: 0.0,
q: 0.0,
t: 50.0,
};
let k_var = p.fair_strike_heston(0.09, 2.0, 0.04);
assert!(
(k_var - 0.04).abs() < 0.01,
"K_var={k_var} should approach θ=0.04"
);
}
#[test]
fn heston_discrete_correction_vanishes_with_n() {
let p = pricer();
let k_cont = p.fair_strike_heston(0.04, 1.5, 0.04);
let k_disc_fine = p.fair_strike_heston_discrete(0.04, 1.5, 0.04, 0.3, -0.7, 100_000);
let k_disc_coarse = p.fair_strike_heston_discrete(0.04, 1.5, 0.04, 0.3, -0.7, 12);
assert!((k_disc_fine - k_cont).abs() < (k_disc_coarse - k_cont).abs());
}
#[test]
fn replication_weights_are_positive_and_decay() {
let strikes: Vec<f64> = (50..=150).step_by(10).map(|i| i as f64).collect();
let w = replication_weights(&strikes, 1.0);
assert_eq!(w.len(), strikes.len());
for &wi in &w {
assert!(wi > 0.0);
}
assert!(w[0] > *w.last().unwrap());
}
#[test]
fn replication_strike_within_one_percent_of_bsm_for_dense_strip() {
use stochastic_rs_distributions::special::norm_cdf;
let p = VarianceSwapPricer {
s: 100.0,
r: 0.0,
q: 0.0,
t: 1.0,
};
let sigma = 0.25;
let strikes: Vec<f64> = (10..=400).map(|i| i as f64 * 0.5).collect();
let prices: Vec<f64> = strikes
.iter()
.map(|&k| {
let d1 = ((p.s / k).ln() + 0.5 * sigma * sigma * p.t) / (sigma * p.t.sqrt());
let d2 = d1 - sigma * p.t.sqrt();
if k >= p.s {
p.s * norm_cdf(d1) - k * norm_cdf(d2)
} else {
k * norm_cdf(-d2) - p.s * norm_cdf(-d1)
}
})
.collect();
let k_var = p.fair_strike_replication(&strikes, &prices);
let target = sigma * sigma;
let rel_err = (k_var - target).abs() / target;
assert!(
rel_err < 0.02,
"K_var={k_var}, expected≈{target}, rel_err={rel_err}"
);
}
}