use crate::options::{
types::BinaryType::{AssetOrNothing, CashOrNothing},
Instrument, Option, OptionGreeks, OptionPricing, OptionStrategy, OptionStyle, OptionType,
Permutation, RainbowType,
};
use rand_distr::num_traits::Pow;
use statrs::distribution::{Continuous, ContinuousCDF, Normal};
#[derive(Debug, Default)]
pub struct BlackScholesModel {
pub risk_free_rate: f64,
pub volatility: f64,
}
impl BlackScholesModel {
pub fn new(risk_free_rate: f64, volatility: f64) -> Self {
Self {
risk_free_rate,
volatility,
}
}
fn calculate_d1_d2(&self, instrument: &Instrument, strike: f64, ttm: f64) -> (f64, f64) {
let sqrt_t = ttm.sqrt();
let d1 = ((instrument.calculate_adjusted_spot(ttm) / strike).ln()
+ (self.risk_free_rate - instrument.continuous_dividend_yield
+ 0.5 * self.volatility.powi(2))
* ttm)
/ (self.volatility * sqrt_t);
let d2 = d1 - self.volatility * sqrt_t;
(d1, d2)
}
pub fn price_euro_call(
&self,
instrument: &Instrument,
strike: f64,
ttm: f64,
normal: &Normal,
) -> f64 {
let (d1, d2) = self.calculate_d1_d2(instrument, strike, ttm);
instrument.calculate_adjusted_spot(ttm)
* (-instrument.continuous_dividend_yield * ttm).exp()
* normal.cdf(d1)
- strike * (-self.risk_free_rate * ttm).exp() * normal.cdf(d2)
}
pub fn price_euro_put(
&self,
instrument: &Instrument,
strike: f64,
ttm: f64,
normal: &Normal,
) -> f64 {
let (d1, d2) = self.calculate_d1_d2(instrument, strike, ttm);
strike * (-self.risk_free_rate * ttm).exp() * normal.cdf(-d2)
- instrument.calculate_adjusted_spot(ttm)
* (-instrument.continuous_dividend_yield * ttm).exp()
* normal.cdf(-d1)
}
pub fn price_cash_or_nothing<T: Option>(&self, option: &T, normal: &Normal) -> f64 {
let (_, d2) = self.calculate_d1_d2(
option.instrument(),
option.strike(),
option.time_to_maturity(),
);
match option.option_type() {
OptionType::Call => {
(-self.risk_free_rate * option.time_to_maturity()).exp() * normal.cdf(d2)
}
OptionType::Put => {
(-self.risk_free_rate * option.time_to_maturity()).exp() * normal.cdf(-d2)
}
}
}
pub fn price_asset_or_nothing<T: Option>(&self, option: &T, normal: &Normal) -> f64 {
let (d1, d2) = self.calculate_d1_d2(
option.instrument(),
option.strike(),
option.time_to_maturity(),
);
match option.option_type() {
OptionType::Call => {
option.instrument().spot()
* (-option.instrument().continuous_dividend_yield * option.time_to_maturity())
.exp()
* normal.cdf(d1)
}
OptionType::Put => {
option.instrument().spot()
* (-option.instrument().continuous_dividend_yield * option.time_to_maturity())
.exp()
* normal.cdf(-d1)
}
}
}
pub fn price_rainbow_call<T: Option>(&self, option: &T, normal: &Normal) -> f64 {
if matches!(option.style(), OptionStyle::Rainbow(RainbowType::AllITM))
&& option.payoff(None) <= 0.0
{
return 0.0;
}
let price = self.price_euro_call(
option.instrument(),
option.strike(),
option.time_to_maturity(),
normal,
);
if matches!(option.style(), OptionStyle::Rainbow(RainbowType::BestOf))
|| matches!(option.style(), OptionStyle::Rainbow(RainbowType::WorstOf))
{
panic!("BestOf/WorstOf options not supported by Black-Scholes model");
}
price
}
pub fn price_rainbow_put<T: Option>(&self, option: &T, normal: &Normal) -> f64 {
if matches!(option.style(), OptionStyle::Rainbow(RainbowType::AllOTM))
&& option.payoff(None) <= 0.0
{
return 0.0;
}
self.price_euro_put(
option.instrument(),
option.strike(),
option.time_to_maturity(),
normal,
)
}
pub fn price_lookback<T: Option>(&self, option: &T, normal: &Normal) -> f64 {
let max = option.instrument().max_spot();
let min = option.instrument().min_spot();
let sqrt_t = option.time_to_maturity().sqrt();
let s = option.instrument().spot();
let r = self.risk_free_rate;
let t = option.time_to_maturity();
let vola = self.volatility;
assert!(s > 0.0 && max > 0.0 && min > 0.0, "Spot prices must be > 0");
println!("max: {max}, min: {min}");
let a1 = |s: f64, h: f64| ((s / h).ln() + (r + 0.5 * vola.powi(2)) * t) / (vola * sqrt_t);
let a2 = |s: f64, h: f64| a1(s, h) - vola * sqrt_t;
let a3 = |s: f64, h: f64| a1(s, h) - 2.0 * r * sqrt_t / vola;
let phi = |x: f64| normal.cdf(x);
match option.option_type() {
OptionType::Call => {
s * phi(a1(s, min))
- min * (-r * t).exp() * phi(a2(s, min))
- (0.5 * s * vola.powi(2)) / (r)
* (phi(-a1(s, min))
- (-r * t).exp()
* (min / s).pow((2f64 * r) / (vola.powi(2)))
* phi(-a3(s, min)))
}
OptionType::Put => {
-s * phi(-a1(s, max))
+ max * (-r * t).exp() * phi(-a2(s, max))
+ (0.5 * s * vola.powi(2)) / (r)
* (phi(a1(s, max))
- (-r * t).exp()
* (max / s).pow((2f64 * r) / (vola.powi(2)))
* phi(a3(s, max)))
}
}
}
fn price_with_volatility<T: Option>(
&self,
option: &T,
volatility: f64,
normal: &Normal,
) -> f64 {
let sqrt_t = option.time_to_maturity().sqrt();
let n_dividends = option
.instrument()
.dividend_times
.iter()
.filter(|&&t| t <= option.time_to_maturity())
.count() as f64;
let adjusted_spot = option.instrument().spot()
* (1.0 - option.instrument().discrete_dividend_yield).powf(n_dividends);
let d1 = ((adjusted_spot / option.strike()).ln()
+ (self.risk_free_rate - option.instrument().continuous_dividend_yield
+ 0.5 * volatility.powi(2))
* option.time_to_maturity())
/ (volatility * sqrt_t);
let d2 = d1 - volatility * sqrt_t;
match option.option_type() {
OptionType::Call => {
let nd1 = normal.cdf(d1);
let nd2 = normal.cdf(d2);
adjusted_spot
* (-option.instrument().continuous_dividend_yield * option.time_to_maturity())
.exp()
* nd1
- option.strike()
* (-self.risk_free_rate * option.time_to_maturity()).exp()
* nd2
}
OptionType::Put => {
let nd1 = normal.cdf(-d1);
let nd2 = normal.cdf(-d2);
option.strike() * (-self.risk_free_rate * option.time_to_maturity()).exp() * nd2
- adjusted_spot
* (-option.instrument().continuous_dividend_yield
* option.time_to_maturity())
.exp()
* nd1
}
}
}
}
impl OptionPricing for BlackScholesModel {
#[rustfmt::skip]
fn price<T: Option>(&self, option: &T) -> f64 {
let normal = Normal::new(0.0, 1.0).unwrap();
match (option.option_type(), option.style()) {
(OptionType::Call, OptionStyle::European) => self.price_euro_call(option.instrument(), option.strike(),option.time_to_maturity(), &normal),
(OptionType::Put, OptionStyle::European) => self.price_euro_put(option.instrument(), option.strike(), option.time_to_maturity(),&normal),
(_, OptionStyle::Binary(CashOrNothing)) => self.price_cash_or_nothing(option, &normal),
(_, OptionStyle::Binary(AssetOrNothing)) => self.price_asset_or_nothing(option, &normal),
(OptionType::Call, OptionStyle::Rainbow(_)) => self.price_rainbow_call(option, &normal),
(OptionType::Put, OptionStyle::Rainbow(_)) => self.price_rainbow_put(option, &normal),
(_, OptionStyle::Lookback(Permutation::Floating)) => self.price_lookback(option, &normal),
_ => panic!("BlackScholesModel does not support this option type or style"),
}
}
fn implied_volatility<T: Option>(&self, option: &T, market_price: f64) -> f64 {
let mut sigma = 0.2; let tolerance = 1e-5;
let max_iterations = 100;
let normal = Normal::new(0.0, 1.0).unwrap();
for _ in 0..max_iterations {
let price = self.price_with_volatility(option, sigma, &normal);
let bump = 1e-5;
let price_up = self.price_with_volatility(option, sigma + bump, &normal);
let vega = (price_up - price) / bump;
let diff = market_price - price;
if diff.abs() < tolerance {
return sigma;
}
if vega.abs() < 1e-12 {
break;
}
sigma += diff / vega;
}
sigma
}
}
impl OptionGreeks for BlackScholesModel {
fn delta<T: Option>(&self, option: &T) -> f64 {
let (d1, d2) = self.calculate_d1_d2(
option.instrument(),
option.strike(),
option.time_to_maturity(),
);
let normal = Normal::new(0.0, 1.0).unwrap();
match option.style() {
OptionStyle::European => match option.option_type() {
OptionType::Call => {
(-option.instrument().continuous_dividend_yield * option.time_to_maturity())
.exp()
* normal.cdf(d1)
}
OptionType::Put => {
(-option.instrument().continuous_dividend_yield * option.time_to_maturity())
.exp()
* (normal.cdf(d1) - 1.0)
}
},
OptionStyle::Binary(CashOrNothing) => {
let delta = (-self.risk_free_rate * option.time_to_maturity()).exp()
* normal.pdf(d2)
/ (self.volatility
* option.instrument().spot()
* option.time_to_maturity().sqrt());
match option.option_type() {
OptionType::Call => delta,
OptionType::Put => -delta,
}
}
OptionStyle::Binary(AssetOrNothing) => match option.option_type() {
OptionType::Call => {
(-option.instrument().continuous_dividend_yield * option.time_to_maturity())
.exp()
* normal.pdf(d1)
/ (self.volatility * option.time_to_maturity().sqrt())
+ (-option.instrument().continuous_dividend_yield
* option.time_to_maturity())
.exp()
* normal.cdf(d1)
}
OptionType::Put => {
-(-option.instrument().continuous_dividend_yield * option.time_to_maturity())
.exp()
* normal.pdf(d1)
/ (self.volatility * option.time_to_maturity().sqrt())
+ (-option.instrument().continuous_dividend_yield
* option.time_to_maturity())
.exp()
* normal.cdf(-d1)
}
},
_ => panic!("Unsupported option style for delta calculation"),
}
}
fn gamma<T: Option>(&self, option: &T) -> f64 {
let (d1, d2) = self.calculate_d1_d2(
option.instrument(),
option.strike(),
option.time_to_maturity(),
);
let adjusted_spot = option
.instrument()
.calculate_adjusted_spot(option.time_to_maturity());
let normal = Normal::new(0.0, 1.0).unwrap();
match option.style() {
OptionStyle::European => {
normal.pdf(d1)
/ (adjusted_spot * self.volatility * option.time_to_maturity().sqrt())
}
OptionStyle::Binary(CashOrNothing) => {
let gamma =
-(-self.risk_free_rate * option.time_to_maturity()).exp() * normal.pdf(d2) * d1
/ (self.volatility.powi(2)
* option.instrument().spot().powi(2)
* option.time_to_maturity());
match option.option_type() {
OptionType::Call => gamma,
OptionType::Put => -gamma,
}
}
OptionStyle::Binary(AssetOrNothing) => {
let gamma = -(-option.instrument().continuous_dividend_yield
* option.time_to_maturity())
.exp()
* normal.pdf(d1)
* d2
/ (option.instrument().spot()
* self.volatility.powi(2)
* option.time_to_maturity());
match option.option_type() {
OptionType::Call => gamma,
OptionType::Put => -gamma,
}
}
_ => panic!("Unsupported option style for gamma calculation"),
}
}
fn theta<T: Option>(&self, option: &T) -> f64 {
let (d1, d2) = self.calculate_d1_d2(
option.instrument(),
option.strike(),
option.time_to_maturity(),
);
let adjusted_spot = option
.instrument()
.calculate_adjusted_spot(option.time_to_maturity());
let normal = Normal::new(0.0, 1.0).unwrap();
match option.style() {
OptionStyle::European => match option.option_type() {
OptionType::Call => {
adjusted_spot * normal.pdf(d1) * self.volatility
/ (2.0 * option.time_to_maturity().sqrt())
+ self.risk_free_rate
* option.strike()
* (-self.risk_free_rate * option.time_to_maturity()).exp()
* normal.cdf(d2)
- option.instrument().continuous_dividend_yield
* adjusted_spot
* (-option.instrument().continuous_dividend_yield
* option.time_to_maturity())
.exp()
* normal.cdf(d1)
}
OptionType::Put => {
adjusted_spot * normal.pdf(d1) * self.volatility
/ (2.0 * option.time_to_maturity().sqrt())
- self.risk_free_rate
* option.strike()
* (-self.risk_free_rate * option.time_to_maturity()).exp()
* normal.cdf(-d2)
+ option.instrument().continuous_dividend_yield
* adjusted_spot
* (-option.instrument().continuous_dividend_yield
* option.time_to_maturity())
.exp()
* normal.cdf(-d1)
}
},
OptionStyle::Binary(CashOrNothing) => match option.option_type() {
OptionType::Call => {
(-self.risk_free_rate * option.time_to_maturity()).exp()
* (normal.pdf(d2)
/ (2.0
* option.time_to_maturity()
* self.volatility
* option.time_to_maturity().sqrt())
* ((option.instrument().spot() / option.strike()).ln()
- (self.risk_free_rate
- option.instrument().continuous_dividend_yield
- self.volatility.powi(2) * 0.5)
* option.time_to_maturity())
+ self.risk_free_rate * normal.cdf(d2))
}
OptionType::Put => {
-(-self.risk_free_rate * option.time_to_maturity()).exp()
* (normal.pdf(d2)
/ (2.0
* option.time_to_maturity()
* self.volatility
* option.time_to_maturity().sqrt())
* ((option.instrument().spot() / option.strike()).ln()
- (self.risk_free_rate
- option.instrument().continuous_dividend_yield
- self.volatility.powi(2) * 0.5)
* option.time_to_maturity())
- self.risk_free_rate * normal.cdf(-d2))
}
},
OptionStyle::Binary(AssetOrNothing) => match option.option_type() {
OptionType::Call => {
option.instrument().spot()
* (-option.instrument().continuous_dividend_yield
* option.time_to_maturity())
.exp()
* (normal.pdf(d1) * 1.0
/ (2.0
* option.time_to_maturity()
* self.volatility
* option.time_to_maturity().sqrt())
* ((option.instrument().spot() / option.strike()).ln()
- (self.risk_free_rate
- option.instrument().continuous_dividend_yield
+ 0.5 * self.volatility.powi(2))
* option.time_to_maturity())
+ option.instrument().continuous_dividend_yield * normal.cdf(d1))
}
OptionType::Put => {
option.instrument().spot()
* (-option.instrument().continuous_dividend_yield
* option.time_to_maturity())
.exp()
* (-normal.pdf(d1) * 1.0
/ (2.0
* option.time_to_maturity()
* self.volatility
* option.time_to_maturity().sqrt())
* ((option.instrument().spot() / option.strike()).ln()
- (self.risk_free_rate
- option.instrument().continuous_dividend_yield
+ 0.5 * self.volatility.powi(2))
* option.time_to_maturity())
+ option.instrument().continuous_dividend_yield * -normal.cdf(d1))
}
},
_ => panic!("Unsupported option style for theta calculation"),
}
}
fn vega<T: Option>(&self, option: &T) -> f64 {
let (d1, d2) = self.calculate_d1_d2(
option.instrument(),
option.strike(),
option.time_to_maturity(),
);
let adjusted_spot = option
.instrument()
.calculate_adjusted_spot(option.time_to_maturity());
let normal = Normal::new(0.0, 1.0).unwrap();
match option.style() {
OptionStyle::European => {
adjusted_spot
* (-option.instrument().continuous_dividend_yield * option.time_to_maturity())
.exp()
* normal.pdf(d1)
* option.time_to_maturity().sqrt()
}
OptionStyle::Binary(CashOrNothing) => {
let vega =
-(-self.risk_free_rate * option.time_to_maturity()).exp() * d1 * normal.pdf(d2)
/ self.volatility;
match option.option_type() {
OptionType::Call => vega,
OptionType::Put => -vega,
}
}
OptionStyle::Binary(AssetOrNothing) => {
let vega = -option.instrument().spot()
* (-option.instrument().continuous_dividend_yield * option.time_to_maturity())
.exp()
* d2
* normal.pdf(d1)
/ (self.volatility);
match option.option_type() {
OptionType::Call => vega,
OptionType::Put => -vega,
}
}
_ => panic!("Unsupported option style for vega calculation"),
}
}
fn rho<T: Option>(&self, option: &T) -> f64 {
let (d1, d2) = self.calculate_d1_d2(
option.instrument(),
option.strike(),
option.time_to_maturity(),
);
let normal = Normal::new(0.0, 1.0).unwrap();
match option.style() {
OptionStyle::European => match option.option_type() {
OptionType::Call => {
option.strike()
* option.time_to_maturity()
* (-self.risk_free_rate * option.time_to_maturity()).exp()
* normal.cdf(d2)
}
OptionType::Put => {
-option.strike()
* option.time_to_maturity()
* (-self.risk_free_rate * option.time_to_maturity()).exp()
* normal.cdf(-d2)
}
},
OptionStyle::Binary(CashOrNothing) => match option.option_type() {
OptionType::Call => {
(-self.risk_free_rate * option.time_to_maturity()).exp()
* (option.time_to_maturity().sqrt() * normal.pdf(d2) / self.volatility
- option.time_to_maturity() * normal.cdf(d2))
}
OptionType::Put => {
-(-self.risk_free_rate * option.time_to_maturity()).exp()
* (option.time_to_maturity().sqrt() * normal.pdf(d2) / self.volatility
+ option.time_to_maturity() * normal.cdf(-d2))
}
},
OptionStyle::Binary(AssetOrNothing) => {
let rho = option.instrument().spot()
* (-option.instrument().continuous_dividend_yield * option.time_to_maturity())
.exp()
* option.time_to_maturity().sqrt()
* normal.pdf(d1)
/ (self.volatility);
match option.option_type() {
OptionType::Call => rho,
OptionType::Put => -rho,
}
}
_ => panic!("Unsupported option style for rho calculation"),
}
}
}
impl OptionStrategy for BlackScholesModel {}