use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::math;
pub struct BlackScholes {
pub input: Input,
pub output: Result<Output, PriceError>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Input {
pub is_call: bool,
pub spot: f32,
pub strike: f32,
pub mat: f32,
pub vol: f32,
pub rate: f32,
pub div: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Output {
pub is_call: bool,
pub d1: f32,
pub d2: f32,
pub cdf_d1: f32,
pub cdf_d2: f32,
pub pdf_d1: f32,
pub pdf_d2: f32,
pub pv: f32,
pub pv_k: f32,
pub w: f32,
pub price: f32,
pub delta: f32,
pub gamma: f32,
pub vega: f32,
pub theta: f32,
pub rho: f32,
pub voma: f32,
pub payoff: f32,
pub pv_payoff: f32,
}
#[derive(Debug)]
pub struct OutputPriceVega {
pub price: f32,
pub vega: f32,
}
#[derive(Debug)]
pub struct OutputPriceVegaVoma {
pub price: f32,
pub vega: f32,
pub voma: f32,
}
const TEST_EPSILON: f32 = 1e-3;
impl PartialEq for Output {
fn eq(&self, other: &Self) -> bool {
if self.is_call != other.is_call {
println!("is_call: {} != {}", self.is_call, other.is_call);
return false;
}
if (self.d1 - other.d1).abs() > TEST_EPSILON {
println!("d1: {} != {}", self.d1, other.d1);
return false;
}
if (self.d2 - other.d2).abs() > TEST_EPSILON {
println!("d2: {} != {}", self.d2, other.d2);
return false;
}
if (self.cdf_d1 - other.cdf_d1).abs() > TEST_EPSILON {
println!("cdf_d1: {} != {}", self.cdf_d1, other.cdf_d1);
return false;
}
if (self.cdf_d2 - other.cdf_d2).abs() > TEST_EPSILON {
println!("cdf_d2: {} != {}", self.cdf_d2, other.cdf_d2);
return false;
}
if (self.pdf_d1 - other.pdf_d1).abs() > TEST_EPSILON {
println!("pdf_d1: {} != {}", self.pdf_d1, other.pdf_d1);
return false;
}
if (self.pdf_d2 - other.pdf_d2).abs() > TEST_EPSILON {
println!("pdf_d2: {} != {}", self.pdf_d2, other.pdf_d2);
return false;
}
if (self.pv - other.pv).abs() > TEST_EPSILON {
println!("pv: {} != {}", self.pv, other.pv);
return false;
}
if (self.pv_k - other.pv_k).abs() > TEST_EPSILON {
println!("pv_k: {} != {}", self.pv_k, other.pv_k);
return false;
}
if (self.w - other.w).abs() > TEST_EPSILON {
println!("w: {} != {}", self.w, other.w);
return false;
}
if (self.price - other.price).abs() > TEST_EPSILON {
println!("price: {} != {}", self.price, other.price);
return false;
}
if (self.delta - other.delta).abs() > TEST_EPSILON {
println!("delta: {} != {}", self.delta, other.delta);
return false;
}
if (self.gamma - other.gamma).abs() > TEST_EPSILON {
println!("gamma: {} != {}", self.gamma, other.gamma);
return false;
}
if (self.vega - other.vega).abs() > TEST_EPSILON {
println!("vega: {} != {}", self.vega, other.vega);
return false;
}
if (self.theta - other.theta).abs() > TEST_EPSILON {
println!("theta: {} != {}", self.theta, other.theta);
return false;
}
if (self.rho - other.rho).abs() > TEST_EPSILON {
println!("rho: {} != {}", self.rho, other.rho);
return false;
}
if (self.voma - other.voma).abs() > TEST_EPSILON {
println!("voma: {} != {}", self.voma, other.voma);
return false;
}
if (self.payoff - other.payoff).abs() > TEST_EPSILON {
println!("payoff: {} != {}", self.payoff, other.payoff);
return false;
}
if (self.pv_payoff - other.pv_payoff).abs() > TEST_EPSILON {
println!("pv_payoff: {} != {}", self.pv_payoff, other.pv_payoff);
return false;
}
return true;
}
}
#[derive(Error, Debug, Serialize, Deserialize)]
pub enum PriceError {
#[error("negative spot: {0} - must be positive")]
NegativeSpot(f32),
#[error("negative strike: {0} - must be positive")]
NegativeStrike(f32),
#[error("negative volatility: {0} - must be positive")]
NegativeVol(f32),
#[error("negative maturity: {0} - must be positive")]
NegativeMat(f32),
}
impl BlackScholes {
pub fn new(input: Input) -> BlackScholes {
let output = price(&input);
BlackScholes { input, output }
}
}
pub fn price(input: &Input) -> Result<Output, PriceError> {
if input.spot < 0.0 {
return Err(PriceError::NegativeSpot(input.spot));
}
if input.strike < 0.0 {
return Err(PriceError::NegativeStrike(input.strike));
}
if input.vol < 0.0 {
return Err(PriceError::NegativeVol(input.vol));
}
if input.mat < 0.0 {
return Err(PriceError::NegativeMat(input.mat));
}
let output = match input.is_call {
true => price_call(input),
false => price_put(input),
};
Ok(output)
}
fn price_call(input: &Input) -> Output {
let s = input.spot;
let k = input.strike;
let t = input.mat;
let v = input.vol;
let r = input.rate;
let q = input.div;
let sqrt_t = t.sqrt();
let v_sqrt_t = v * sqrt_t;
let d1 = (1.0 / v_sqrt_t) * ((s / k).ln() + (r - q + v * v / 2.0) * t);
let d2 = d1 - v_sqrt_t;
let cdf_d1 = math::cdf(d1);
let pdf_d1 = math::pdf(d1);
let cdf_d2 = math::cdf(d2);
let pdf_d2 = math::pdf(d2);
let pv = (-r * t).exp();
let pv_k = pv * k;
let w: f32 = (-q * t).exp();
let price = cdf_d1 * s * w - cdf_d2 * pv_k;
let delta = w * cdf_d1;
let gamma = w * pdf_d1 / (s * v_sqrt_t);
let vega = s * w * pdf_d1 * sqrt_t;
let theta = -s * w * pdf_d1 * v / (2.0 * sqrt_t) - r * pv_k * cdf_d2 + q * s * w * cdf_d1;
let rho = pv_k * t * cdf_d2;
let voma = s * w * pdf_d1 * sqrt_t * d1 * d2 / v;
let payoff = (s - k).max(0.0);
let pv_payoff = pv * payoff;
Output {
is_call: true,
d1,
d2,
cdf_d1,
cdf_d2,
pdf_d1,
pdf_d2,
pv,
pv_k,
w,
price,
delta,
gamma,
vega,
theta,
rho,
voma,
payoff,
pv_payoff,
}
}
fn price_put(input: &Input) -> Output {
let s = input.spot;
let k = input.strike;
let t = input.mat;
let v = input.vol;
let r = input.rate;
let q = input.div;
let sqrt_t = t.sqrt();
let v_sqrt_t = v * sqrt_t;
let d1 = (1.0 / v_sqrt_t) * ((s / k).ln() + (r - q + v * v / 2.0) * t);
let d2 = d1 - v_sqrt_t;
let cdf_minus_d1 = math::cdf(-d1);
let pdf_d1 = math::pdf(d1);
let cdf_minus_d2 = math::cdf(-d2);
let pdf_d2 = math::pdf(d2);
let pv = (-r * t).exp();
let pv_k = pv * k;
let w = (-q * t).exp();
let price = cdf_minus_d2 * pv_k - cdf_minus_d1 * s * w;
let delta = -w * cdf_minus_d1;
let gamma = w * pdf_d1 / (s * v_sqrt_t);
let vega = s * w * pdf_d1 * sqrt_t;
let theta =
-s * w * pdf_d1 * v / (2.0 * sqrt_t) + r * pv_k * cdf_minus_d2 - q * s * w * cdf_minus_d1;
let rho = -pv_k * t * cdf_minus_d2;
let voma = s * w * pdf_d1 * sqrt_t * d1 * d2 / v;
let payoff = (k - s).max(0.0);
let pv_payoff = pv * payoff;
Output {
is_call: false,
d1,
d2,
cdf_d1: cdf_minus_d1,
cdf_d2: cdf_minus_d2,
pdf_d1,
pdf_d2,
pv,
pv_k,
w,
price,
delta,
gamma,
vega,
theta,
rho,
voma,
payoff,
pv_payoff,
}
}
pub fn call_price_vega(s: f32, k: f32, r: f32, t: f32, v: f32, q: f32) -> OutputPriceVega {
let sqrt_t = t.sqrt();
let v_sqrt_t = v * sqrt_t;
let d1 = (1.0 / v_sqrt_t) * ((s / k).ln() + (r - q + v * v / 2.0) * t);
let d2 = d1 - v_sqrt_t;
let cdf_d1 = math::cdf(d1);
let pdf_d1 = math::pdf(d1);
let cdf_d2 = math::cdf(d2);
let pv = (-r * t).exp();
let pv_k = pv * k;
let w = (-q * t).exp();
let price = cdf_d1 * s * w - cdf_d2 * pv_k;
let vega = s * w * pdf_d1 * sqrt_t;
OutputPriceVega { price, vega }
}
pub fn call_price_vega_voma(s: f32, k: f32, r: f32, t: f32, v: f32, q: f32) -> OutputPriceVegaVoma {
let sqrt_t = t.sqrt();
let v_sqrt_t = v * sqrt_t;
let d1 = (1.0 / v_sqrt_t) * ((s / k).ln() + (r - q + v * v / 2.0) * t);
let d2 = d1 - v_sqrt_t;
let cdf_d1 = math::cdf(d1);
let pdf_d1 = math::pdf(d1);
let cdf_d2 = math::cdf(d2);
let pv = (-r * t).exp();
let pv_k = pv * k;
let w = (-q * t).exp();
let price = cdf_d1 * s * w - cdf_d2 * pv_k;
let vega = s * w * pdf_d1 * sqrt_t;
let voma = s * w * pdf_d1 * sqrt_t * d1 * d2 / v;
OutputPriceVegaVoma { price, vega, voma }
}
#[cfg(test)]
mod tests {
use crate::black_scholes::{BlackScholes, Input, Output, TEST_EPSILON};
struct InputTest {
input: Input,
output: Output,
}
struct InputTestParity {
input_call: Input,
input_put: Input,
}
#[test]
fn sample_prices() {
let test_data: Vec<InputTest> = vec![
InputTest {
input: Input {
is_call: true,
spot: 105.0,
strike: 100.0,
mat: 3.0,
vol: 0.15,
rate: 0.03,
div: 0.01,
},
output: Output {
is_call: true,
d1: 0.5486,
d2: 0.2888,
cdf_d1: 0.7084,
cdf_d2: 0.6136,
pdf_d1: 0.3432,
pdf_d2: 0.3826,
pv: 0.9139,
pv_k: 91.3931,
w: 0.9704,
price: 16.098,
delta: 0.6874,
gamma: 0.0122,
vega: 60.5716,
theta: -2.4750,
rho: 168.2485,
voma: 63.9890,
payoff: 5.0,
pv_payoff: 4.569,
},
},
InputTest {
input: Input {
is_call: false,
spot: 90.0,
strike: 100.0,
mat: 3.0,
vol: 0.15,
rate: 0.03,
div: 0.01,
},
output: Output {
is_call: false,
d1: -0.0447,
d2: -0.3045,
cdf_d1: 0.5178,
cdf_d2: 0.6196,
pdf_d1: 0.3985,
pdf_d2: 0.3809,
pv: 0.9139,
pv_k: 91.3931,
w: 0.9704,
price: 11.402,
delta: -0.5025,
gamma: 0.0165,
vega: 60.2908,
theta: -0.2607,
rho: -169.8884,
voma: 5.4694,
payoff: 10.0,
pv_payoff: 9.1393,
},
},
];
println!("epsilon used in tests: {}", TEST_EPSILON);
test_data.iter().for_each(|test| {
let bs = BlackScholes::new(test.input.clone());
assert!(bs.output.is_ok());
let out = bs.output.unwrap();
assert_eq!(out, test.output);
});
}
#[test]
fn put_call_parity() {
let test_data: Vec<InputTestParity> = vec![InputTestParity {
input_call: Input {
is_call: true,
spot: 105.6,
strike: 100.0,
mat: 3.2,
vol: 0.25,
rate: 0.03,
div: 0.01,
},
input_put: Input {
is_call: false,
spot: 105.6,
strike: 100.0,
mat: 3.2,
vol: 0.25,
rate: 0.03,
div: 0.01,
},
}];
test_data.iter().for_each(|test| {
let bs_call = BlackScholes::new(test.input_call.clone());
let bs_put = BlackScholes::new(test.input_put.clone());
assert!(bs_call.output.is_ok());
assert!(bs_put.output.is_ok());
let call_out = bs_call.output.unwrap();
let put_out = bs_put.output.unwrap();
let r = test.input_call.rate;
let q = test.input_call.div;
let mat = test.input_call.mat;
let call = call_out.price;
let put = put_out.price;
let pv = call_out.pv;
let fwd = test.input_call.spot * ((r - q) * mat).exp();
let strike = test.input_call.strike;
let parity = (call - put) - pv * (fwd - strike);
let epsilon = 1e-5;
assert!(parity.abs() < epsilon);
});
}
}