use owens_t::biv_norm;
use stochastic_rs_distributions::special::norm_cdf;
use crate::OptionType;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CompoundType {
CallOnCall,
CallOnPut,
PutOnCall,
PutOnPut,
}
impl CompoundType {
pub fn inner(&self) -> OptionType {
match self {
Self::CallOnCall | Self::PutOnCall => OptionType::Call,
Self::CallOnPut | Self::PutOnPut => OptionType::Put,
}
}
pub fn outer(&self) -> OptionType {
match self {
Self::CallOnCall | Self::CallOnPut => OptionType::Call,
Self::PutOnCall | Self::PutOnPut => OptionType::Put,
}
}
}
#[derive(Debug, Clone)]
pub struct CompoundPricer {
pub s: f64,
pub k1: f64,
pub k2: f64,
pub t1: f64,
pub t2: f64,
pub r: f64,
pub q: f64,
pub sigma: f64,
pub compound_type: CompoundType,
}
impl CompoundPricer {
pub fn price(&self) -> f64 {
let v = self.sigma;
let v2 = v * v;
let b = self.r - self.q;
let s_star = self.critical_inner_price();
let cdf2 = |x: f64, y: f64, rho: f64| -> f64 { biv_norm(-x, -y, rho) };
let z1 = ((self.s / s_star).ln() + (b + 0.5 * v2) * self.t1) / (v * self.t1.sqrt());
let z2 = z1 - v * self.t1.sqrt();
let y1 = ((self.s / self.k2).ln() + (b + 0.5 * v2) * self.t2) / (v * self.t2.sqrt());
let y2 = y1 - v * self.t2.sqrt();
let rho = (self.t1 / self.t2).sqrt();
let coc_t2 = ((b - self.r) * self.t2).exp();
let disc_t1 = (-self.r * self.t1).exp();
let disc_t2 = (-self.r * self.t2).exp();
match self.compound_type {
CompoundType::CallOnCall => {
self.s * coc_t2 * cdf2(z1, y1, rho)
- self.k2 * disc_t2 * cdf2(z2, y2, rho)
- self.k1 * disc_t1 * norm_cdf(z2)
}
CompoundType::PutOnCall => {
self.k2 * disc_t2 * cdf2(-z2, y2, -rho) - self.s * coc_t2 * cdf2(-z1, y1, -rho)
+ self.k1 * disc_t1 * norm_cdf(-z2)
}
CompoundType::CallOnPut => {
self.k2 * disc_t2 * cdf2(-z2, -y2, rho)
- self.s * coc_t2 * cdf2(-z1, -y1, rho)
- self.k1 * disc_t1 * norm_cdf(-z2)
}
CompoundType::PutOnPut => {
self.s * coc_t2 * cdf2(z1, -y1, -rho) - self.k2 * disc_t2 * cdf2(z2, -y2, -rho)
+ self.k1 * disc_t1 * norm_cdf(z2)
}
}
}
fn critical_inner_price(&self) -> f64 {
let mut lo = 1e-6;
let mut hi = (self.k2 + self.k1).max(self.s) * 100.0;
for _ in 0..200 {
let mid = 0.5 * (lo + hi);
let inner = self.inner_price(mid);
let diff = inner - self.k1;
if diff.abs() < 1e-10 {
return mid;
}
match self.compound_type.inner() {
OptionType::Call => {
if diff > 0.0 {
hi = mid;
} else {
lo = mid;
}
}
OptionType::Put => {
if diff > 0.0 {
lo = mid;
} else {
hi = mid;
}
}
}
}
0.5 * (lo + hi)
}
fn inner_price(&self, s: f64) -> f64 {
let v = self.sigma;
let v2 = v * v;
let b = self.r - self.q;
let tau = self.t2 - self.t1;
let d1 = ((s / self.k2).ln() + (b + 0.5 * v2) * tau) / (v * tau.sqrt());
let d2 = d1 - v * tau.sqrt();
match self.compound_type.inner() {
OptionType::Call => {
s * ((b - self.r) * tau).exp() * norm_cdf(d1)
- self.k2 * (-self.r * tau).exp() * norm_cdf(d2)
}
OptionType::Put => {
self.k2 * (-self.r * tau).exp() * norm_cdf(-d2)
- s * ((b - self.r) * tau).exp() * norm_cdf(-d1)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn call_on_call_basic() {
use crate::pricing::bsm::BSMCoc;
use crate::pricing::bsm::BSMPricer;
use crate::traits::PricerExt;
let p = CompoundPricer {
s: 50.0,
k1: 10.0,
k2: 50.0,
t1: 0.5,
t2: 1.0,
r: 0.05,
q: 0.0,
sigma: 0.40,
compound_type: CompoundType::CallOnCall,
};
let coc_price = p.price();
let inner = BSMPricer::builder(50.0, 0.40, 50.0, 0.05)
.tau(1.0)
.option_type(OptionType::Call)
.coc(BSMCoc::Bsm1973)
.build()
.calculate_call_put()
.0;
assert!(coc_price > 0.0, "CoC={coc_price}");
assert!(
coc_price < inner,
"CoC={coc_price} should be < inner call={inner}"
);
}
#[test]
fn coc_bounded_below_by_call_minus_strike() {
use crate::pricing::bsm::BSMCoc;
use crate::pricing::bsm::BSMPricer;
use crate::traits::PricerExt;
let s = 100.0;
let r = 0.05;
let sigma = 0.25;
let p = CompoundPricer {
s,
k1: 5.0,
k2: 100.0,
t1: 0.25,
t2: 1.0,
r,
q: 0.0,
sigma,
compound_type: CompoundType::CallOnCall,
};
let coc = p.price();
let outer = BSMPricer::builder(s, sigma, 100.0, r)
.tau(1.0)
.option_type(OptionType::Call)
.coc(BSMCoc::Bsm1973)
.build()
.calculate_call_put()
.0;
assert!(coc < outer);
assert!(coc > 0.0);
}
#[test]
fn put_on_call_otm() {
let p = CompoundPricer {
s: 100.0,
k1: 0.001,
k2: 100.0,
t1: 0.25,
t2: 1.0,
r: 0.05,
q: 0.0,
sigma: 0.25,
compound_type: CompoundType::PutOnCall,
};
let price = p.price();
assert!(price < 0.05, "PoC OTM={price}");
}
#[test]
fn compound_put_call_parity() {
use crate::pricing::bsm::BSMCoc;
use crate::pricing::bsm::BSMPricer;
use crate::traits::PricerExt;
let s = 100.0;
let k1 = 5.0;
let k2 = 100.0;
let t1 = 0.25;
let t2 = 1.0;
let r = 0.05;
let sigma = 0.25;
let coc = CompoundPricer {
s,
k1,
k2,
t1,
t2,
r,
q: 0.0,
sigma,
compound_type: CompoundType::CallOnCall,
};
let poc = CompoundPricer {
compound_type: CompoundType::PutOnCall,
..coc.clone()
};
let inner_call = BSMPricer::builder(s, sigma, k2, r)
.tau(t2)
.option_type(OptionType::Call)
.coc(BSMCoc::Bsm1973)
.build()
.calculate_call_put()
.0;
let lhs = coc.price() - poc.price();
let rhs = inner_call - k1 * (-r * t1).exp();
assert!(
(lhs - rhs).abs() < 0.05,
"compound parity violated: lhs={lhs}, rhs={rhs}"
);
}
}