use crate::calibration::rbergomi::RBergomiParams;
use crate::calibration::rbergomi::simulate_rbergomi_terminal_samples;
use crate::traits::ModelPricer;
#[derive(Debug, Clone)]
pub struct RBergomiPricer {
pub params: RBergomiParams,
pub n_paths: usize,
pub steps_per_year: usize,
pub msoe_terms: usize,
pub seed: u64,
pub antithetic: bool,
}
impl RBergomiPricer {
pub fn new(params: RBergomiParams) -> Self {
Self {
params,
n_paths: 100_000,
steps_per_year: 200,
msoe_terms: 12,
seed: 42,
antithetic: true,
}
}
pub fn with_paths(mut self, n_paths: usize) -> Self {
self.n_paths = n_paths;
self
}
pub fn with_steps_per_year(mut self, steps: usize) -> Self {
self.steps_per_year = steps;
self
}
pub fn with_msoe_terms(mut self, terms: usize) -> Self {
self.msoe_terms = terms;
self
}
pub fn with_seed(mut self, seed: u64) -> Self {
self.seed = seed;
self
}
pub fn with_antithetic(mut self, antithetic: bool) -> Self {
self.antithetic = antithetic;
self
}
fn mc_call_price(&self, s: f64, k: f64, r: f64, q: f64, tau: f64) -> f64 {
let samples = simulate_rbergomi_terminal_samples(
&self.params,
s,
r,
q,
tau,
self.n_paths,
self.steps_per_year,
self.msoe_terms,
self.seed,
);
let payoff_sum: f64 = samples.iter().map(|&st| (st - k).max(0.0)).sum();
let mut price = payoff_sum / samples.len() as f64;
if self.antithetic {
let samples_anti = simulate_rbergomi_terminal_samples(
&self.params,
s,
r,
q,
tau,
self.n_paths,
self.steps_per_year,
self.msoe_terms,
self.seed.wrapping_add(0x9E37_79B9_7F4A_7C15),
);
let payoff_sum_anti: f64 = samples_anti.iter().map(|&st| (st - k).max(0.0)).sum();
let price_anti = payoff_sum_anti / samples_anti.len() as f64;
price = 0.5 * (price + price_anti);
}
(-r * tau).exp() * price
}
}
impl ModelPricer for RBergomiPricer {
fn price_call(&self, s: f64, k: f64, r: f64, q: f64, tau: f64) -> f64 {
self.mc_call_price(s, k, r, q, tau)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::calibration::rbergomi::RBergomiXi0;
#[test]
fn rbergomi_pricer_call_positive() {
let params = RBergomiParams {
hurst: 0.1,
rho: -0.7,
eta: 1.9,
xi0: RBergomiXi0::Constant(0.04),
};
let pricer = RBergomiPricer::new(params).with_paths(50_000);
let call = pricer.price_call(100.0, 100.0, 0.05, 0.0, 0.5);
assert!(
call > 0.0 && call < 100.0,
"ATM call should be positive and bounded: {call}"
);
}
#[test]
fn rbergomi_pricer_call_decreases_with_strike() {
let params = RBergomiParams {
hurst: 0.1,
rho: -0.7,
eta: 1.9,
xi0: RBergomiXi0::Constant(0.04),
};
let pricer = RBergomiPricer::new(params).with_paths(50_000);
let c_90 = pricer.price_call(100.0, 90.0, 0.05, 0.0, 0.5);
let c_100 = pricer.price_call(100.0, 100.0, 0.05, 0.0, 0.5);
let c_110 = pricer.price_call(100.0, 110.0, 0.05, 0.0, 0.5);
assert!(
c_90 > c_100 && c_100 > c_110,
"Call should decrease with strike: c90={c_90}, c100={c_100}, c110={c_110}"
);
}
#[test]
fn rbergomi_pricer_respects_dividend_yield() {
let params = RBergomiParams {
hurst: 0.1,
rho: -0.7,
eta: 1.9,
xi0: RBergomiXi0::Constant(0.04),
};
let pricer = RBergomiPricer::new(params).with_paths(80_000);
let p_no_div = pricer.price_call(100.0, 100.0, 0.05, 0.0, 1.0);
let p_with_div = pricer.price_call(100.0, 100.0, 0.05, 0.05, 1.0);
assert!(
p_with_div < p_no_div - 0.5,
"ATM rBergomi call must drop when q > 0: q=0 → {p_no_div:.4}, q=0.05 → {p_with_div:.4}"
);
}
}