use crate::{
instruments::options::TypeFlag,
math::distributions::{Distribution, Gaussian},
math::Statistic,
models::geometric_brownian_motion::GeometricBrownianMotion,
stochastics::process::StochasticProcess,
};
#[allow(clippy::module_name_repetitions)]
#[derive(Debug, Clone, Copy)]
pub enum LookbackStrike {
Floating,
Fixed,
}
#[allow(clippy::module_name_repetitions)]
#[derive(Debug, Clone, Copy)]
pub struct LookbackOption {
pub initial_price: f64,
pub risk_free_rate: f64,
pub strike_price: Option<f64>,
pub volatility: f64,
pub time_to_maturity: f64,
pub dividend_yield: f64,
pub s_min: f64,
pub s_max: f64,
pub strike_type: LookbackStrike,
}
impl LookbackOption {
#[must_use]
pub fn price_analytic(&self) -> (f64, f64) {
let s = self.initial_price;
let r = self.risk_free_rate;
let t = self.time_to_maturity;
let v = self.volatility;
let q = self.dividend_yield;
let s_min = self.s_min;
let s_max = self.s_max;
let b = r - q;
let norm = Gaussian::default();
let call: f64;
let put: f64;
match self.strike_type {
LookbackStrike::Floating => {
let a1 = ((s / s_min).ln() + (b + v * v / 2.0) * t) / (v * t.sqrt());
let a2 = a1 - v * t.sqrt();
let b1 = ((s / s_max).ln() + (b + v * v / 2.0) * t) / (v * t.sqrt());
let b2 = b1 - v * t.sqrt();
if b == 0.0 {
call = s * (-r * t).exp() * norm.cdf(a1)
- s_min * (-r * t).exp() * norm.cdf(a2)
+ s * (-r * t).exp()
* v
* t.sqrt()
* (norm.pdf(a1) + a1 * (norm.cdf(a1) - 1.0));
put = -s * ((b - r) * t).exp() * norm.cdf(-b1)
+ s_max * (-r * t).exp() * norm.cdf(-b2)
+ s * (-r * t).exp() * v * t.sqrt() * (norm.pdf(b1) + b1 * norm.cdf(b1));
} else {
call = s * ((b - r) * t).exp() * norm.cdf(a1)
- s_min * (-r * t).exp() * norm.cdf(a2)
+ s * (-r * t).exp()
* (v * v / (2.0 * b))
* ((s / s_min).powf(-2.0 * b / (v * v))
* norm.cdf(-a1 + 2.0 * b * t.sqrt() / v)
- (b * t).exp() * norm.cdf(-a1));
put = -s * ((b - r) * t).exp() * norm.cdf(-b1)
+ s_max * (-r * t).exp() * norm.cdf(-b2)
+ s * (-r * t).exp()
* (v * v / (2.0 * b))
* (-(s / s_max).powf(-2.0 * b / (v * v))
* norm.cdf(b1 - 2.0 * b * t.sqrt() / v)
+ (b * t).exp() * norm.cdf(b1));
}
(call, put)
}
LookbackStrike::Fixed => {
let x = self.strike_price.unwrap();
let d1 = ((s / x).ln() + (b + v * v / 2.0) * t) / (v * t.sqrt());
let d2 = d1 - v * t.sqrt();
let e1 = ((s / s_max).ln() + (b + v * v / 2.0) * t) / (v * t.sqrt());
let e2 = e1 - v * t.sqrt();
let f1 = ((s / s_min).ln() + (b + v * v / 2.0) * t) / (v * t.sqrt());
let f2 = f1 - v * t.sqrt();
let call = if x > s_max {
s * ((b - r) * t).exp() * norm.cdf(d1) - x * (-r * t).exp() * norm.cdf(d2)
+ s * (-r * t).exp()
* (v * v / (2.0 * b))
* (-(s / x).powf(-2.0 * b / (v * v))
* norm.cdf(d1 - 2.0 * b * t.sqrt() / v)
+ (b * t).exp() * norm.cdf(d1))
} else {
(-r * t).exp() * (s_max - x) + s * ((b - r) * t).exp() * norm.cdf(e1)
- s_max * (-r * t).exp() * norm.cdf(e2)
+ s * (-r * t).exp()
* (v * v / (2.0 * b))
* (-(s / s_max).powf(-2.0 * b / (v * v))
* norm.cdf(e1 - 2.0 * b * t.sqrt() / v)
+ (b * t).exp() * norm.cdf(e1))
};
let put = if x < s_min {
-s * ((b - r) * t).exp() * norm.cdf(-d1)
+ x * (-r * t).exp() * norm.cdf(-d2)
+ s * (-r * t).exp()
* (v * v / (2.0 * b))
* ((s / x).powf(-2.0 * b / (v * v))
* norm.cdf(-d1 + 2.0 * b * t.sqrt() / v)
- (b * t).exp() * norm.cdf(-d1))
} else {
(-r * t).exp() * (x - s_min) - s * ((b - r) * t).exp() * norm.cdf(-f1)
+ s_min * (-r * t).exp() * norm.cdf(-f2)
+ s * (-r * t).exp()
* (v * v / (2.0 * b))
* ((s / s_min).powf(-2.0 * b / (v * v))
* norm.cdf(-f1 + 2.0 * b * t.sqrt() / v)
- (b * t).exp() * norm.cdf(-f1))
};
(call, put)
}
}
}
fn payoff(&self, option_type: TypeFlag, strike_type: LookbackStrike, path: &[f64]) -> f64 {
let S_min = *path.iter().min_by(|a, b| a.total_cmp(b)).unwrap();
let S_max = *path.iter().max_by(|a, b| a.total_cmp(b)).unwrap();
let S_T = path.last().unwrap();
match option_type {
TypeFlag::Call => match strike_type {
LookbackStrike::Fixed => f64::max(S_max - self.strike_price.unwrap(), 0.0),
LookbackStrike::Floating => f64::max(S_T - S_min, 0.0),
},
TypeFlag::Put => match strike_type {
LookbackStrike::Fixed => f64::max(self.strike_price.unwrap() - S_min, 0.0),
LookbackStrike::Floating => f64::max(S_max - S_T, 0.0),
},
}
}
#[must_use]
pub fn price_simulated(&self, n_steps: usize, n_sims: usize, parallel: bool) -> (f64, f64) {
let x_0 = self.initial_price;
let r = self.risk_free_rate;
let q = self.dividend_yield;
let sigma = self.volatility;
let t_n = self.time_to_maturity;
let cost_of_carry = r - q;
let gbm = GeometricBrownianMotion::new(cost_of_carry, sigma);
let paths = gbm.euler_maruyama(x_0, 0.0, t_n, n_steps, n_sims, parallel);
let mut call_payoffs = Vec::with_capacity(n_sims);
let mut put_payoffs = Vec::with_capacity(n_sims);
match self.strike_type {
LookbackStrike::Fixed => {
for path in &paths.paths {
call_payoffs.push(Self::payoff(
self,
TypeFlag::Call,
LookbackStrike::Fixed,
path,
));
put_payoffs.push(Self::payoff(
self,
TypeFlag::Put,
LookbackStrike::Fixed,
path,
));
}
}
LookbackStrike::Floating => {
for path in &paths.paths {
call_payoffs.push(Self::payoff(
self,
TypeFlag::Call,
LookbackStrike::Floating,
path,
));
put_payoffs.push(Self::payoff(
self,
TypeFlag::Put,
LookbackStrike::Floating,
path,
));
}
}
}
(
(-r * t_n).exp() * call_payoffs.mean(),
(-r * t_n).exp() * put_payoffs.mean(),
)
}
}
#[cfg(test)]
mod tests_lookback {
use super::*;
use crate::assert_approx_equal;
#[test]
fn test_lookback_floating() {
let lbo_floating = LookbackOption {
initial_price: 50.0,
s_max: 50.0,
s_min: 50.0,
time_to_maturity: 0.25,
risk_free_rate: 0.1,
dividend_yield: 0.0,
volatility: 0.4,
strike_price: None, strike_type: LookbackStrike::Floating,
};
let prices_mc = lbo_floating.price_simulated(500, 10000, true);
let prices_cf = lbo_floating.price_analytic();
assert_approx_equal!(prices_mc.0, prices_cf.0, 0.5);
assert_approx_equal!(prices_mc.1, prices_cf.1, 0.5);
assert_approx_equal!(prices_mc.0, 8.04, 0.5);
assert_approx_equal!(prices_mc.1, 7.79, 0.5);
assert_approx_equal!(prices_cf.0, 8.04, 0.2);
assert_approx_equal!(prices_cf.1, 7.79, 0.2);
}
#[test]
fn test_lookback_fixed() {
let lbo_fixed = LookbackOption {
initial_price: 100.0,
s_max: 100.0,
s_min: 100.0,
time_to_maturity: 1.0,
risk_free_rate: 0.1,
volatility: 0.1,
strike_price: Some(95.0),
dividend_yield: 0.0,
strike_type: LookbackStrike::Fixed,
};
let prices_mc = lbo_fixed.price_simulated(1000, 10000, true);
let prices_cf = lbo_fixed.price_analytic();
assert_approx_equal!(prices_mc.0, prices_cf.0, 0.5);
assert_approx_equal!(prices_mc.1, prices_cf.1, 0.5);
assert_approx_equal!(prices_mc.0, 18.3241, 0.5);
assert_approx_equal!(prices_mc.1, 1.0534, 0.5);
assert_approx_equal!(prices_cf.0, 18.3241, 0.0001);
assert_approx_equal!(prices_cf.1, 1.0534, 0.0001);
}
#[test]
fn test_lookback_payoff_fixed() {
let lbo_fixed = LookbackOption {
initial_price: 50.0,
s_max: 50.0,
s_min: 50.0,
time_to_maturity: 0.25,
risk_free_rate: 0.1,
dividend_yield: 0.0,
volatility: 0.4,
strike_price: Some(60.0), strike_type: LookbackStrike::Fixed,
};
let path = vec![50.0, 55.0, 52.0, 58.0, 54.0];
let call_payoff = lbo_fixed.payoff(TypeFlag::Call, LookbackStrike::Fixed, &path);
let put_payoff = lbo_fixed.payoff(TypeFlag::Put, LookbackStrike::Fixed, &path);
assert_approx_equal!(call_payoff, 0.0, 0.1); assert_approx_equal!(put_payoff, 10.0, 0.1); }
#[test]
fn test_lookback_payoff_floating() {
let lbo_floating = LookbackOption {
initial_price: 50.0,
s_max: 50.0,
s_min: 50.0,
time_to_maturity: 0.25,
risk_free_rate: 0.1,
dividend_yield: 0.0,
volatility: 0.4,
strike_price: None, strike_type: LookbackStrike::Floating,
};
let path = vec![50.0, 55.0, 52.0, 58.0, 54.0];
let call_payoff = lbo_floating.payoff(TypeFlag::Call, LookbackStrike::Floating, &path);
let put_payoff = lbo_floating.payoff(TypeFlag::Put, LookbackStrike::Floating, &path);
assert_approx_equal!(call_payoff, 4.0, 0.1); assert_approx_equal!(put_payoff, 4.0, 0.1); }
}