use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::black_scholes;
pub struct ImpliedVol {
pub input: Input,
pub output: Result<Output, ImpliedVolError>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Input {
pub price: f32,
pub spot: f32,
pub strike: f32,
pub mat: f32,
pub rate: f32,
pub div: f32,
pub iter: u32,
pub prec: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Output {
pub vol: f32,
pub iter: u32,
pub prec: f32,
}
#[derive(Error, Debug, Serialize, Deserialize)]
pub enum ImpliedVolError {
#[error("negative spot: {0} - must be positive")]
NegativeSpot(f32),
#[error("negative strike: {0} - must be positive")]
NegativeStrike(f32),
#[error("negative maturity: {0} - must be positive")]
NegativeMat(f32),
#[error("out of bound price: {0} - must be between max(0, S-PV(K)) and spot")]
OutOfBoundPrice(f32),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Method {
Newton,
Halley,
}
impl ImpliedVol {
pub fn new(input: Input, method: Method) -> ImpliedVol {
let output = find_vol(&input, method);
ImpliedVol { input, output }
}
}
fn find_vol(input: &Input, method: Method) -> Result<Output, ImpliedVolError> {
if input.spot < 0.0 {
return Err(ImpliedVolError::NegativeSpot(input.spot));
}
if input.strike < 0.0 {
return Err(ImpliedVolError::NegativeStrike(input.strike));
}
if input.mat < 0.0 {
return Err(ImpliedVolError::NegativeMat(input.mat));
}
let pv_r = (-input.rate * input.mat).exp();
let pv_q = (-input.div * input.mat).exp();
let min_price = (input.spot * pv_q - input.strike * pv_r).max(0.0);
let max_price = input.spot;
if input.price > max_price || input.price < min_price {
return Err(ImpliedVolError::OutOfBoundPrice(input.price));
}
let output = match method {
Method::Newton => newton(input),
Method::Halley => halley(input),
};
Ok(output)
}
const VOL_START_MIN: f32 = 1e-10;
fn newton(input: &Input) -> Output {
let pv_r = (-input.rate * input.mat).exp();
let pv_q = (-input.div * input.mat).exp();
let moneyness = (input.spot * pv_q) / (input.strike * pv_r);
let vol_start = ((2.0 * moneyness.ln().abs() / input.mat).sqrt()).max(VOL_START_MIN);
let mut vol = vol_start;
let mut iter = 0;
let prec = input.prec;
while iter < input.iter {
let calc = black_scholes::call_price_vega(
input.spot,
input.strike,
input.rate,
input.mat,
vol,
input.div,
);
let diff = calc.price - input.price;
if diff.abs() < input.prec {
break;
}
let f = diff;
let f_prime = calc.vega;
let step = -f / f_prime;
vol += step;
iter += 1;
}
Output { vol, iter, prec }
}
fn halley(input: &Input) -> Output {
let pv_r = (-input.rate * input.mat).exp();
let pv_q = (-input.div * input.mat).exp();
let moneyness = (input.spot * pv_q) / (input.strike * pv_r);
let vol_start = ((2.0 * moneyness.ln().abs() / input.mat).sqrt()).max(VOL_START_MIN);
let mut vol = vol_start;
let mut iter = 0;
let mut prec = 0.0;
while iter < input.iter {
let calc = black_scholes::call_price_vega_voma(
input.spot,
input.strike,
input.rate,
input.mat,
vol,
input.div,
);
let diff = calc.price - input.price;
if diff.abs() < input.prec {
break;
}
let f = diff;
let f_prime = calc.vega;
let f_second = calc.voma;
let step = -(2.0 * f * f_prime) / (2.0 * f_prime.powi(2) - f * f_second);
vol += step;
iter += 1;
prec = diff;
}
Output { vol, iter, prec }
}
#[cfg(test)]
mod tests {
use crate::black_scholes::BlackScholes;
use crate::black_scholes::Input as BSInput;
use crate::implied_vol::Input as IVInput;
use crate::implied_vol::{ImpliedVol, Method};
#[test]
fn test_newton() {
let test_data: Vec<BSInput> = vec![
BSInput {
is_call: true,
spot: 135.6,
strike: 100.0,
mat: 3.2,
vol: 0.25,
rate: 0.03,
div: 0.01,
},
BSInput {
is_call: true,
spot: 60.6,
strike: 100.0,
mat: 3.2,
vol: 0.25,
rate: 0.03,
div: 0.01,
},
BSInput {
is_call: true,
spot: 100.0,
strike: 100.0,
mat: 3.2,
vol: 0.35,
rate: 0.03,
div: 0.00,
},
BSInput {
is_call: true,
spot: 100.0,
strike: 100.0,
mat: 1.0,
vol: 0.10,
rate: 0.00,
div: 0.00,
},
];
test_data.iter().for_each(|input| {
let bs_call = BlackScholes::new(input.clone());
let bs_vol = bs_call.input.vol;
let iv_input = IVInput {
price: bs_call.output.unwrap().price,
spot: input.spot,
strike: input.strike,
mat: input.mat,
rate: input.rate,
div: input.div,
iter: 10,
prec: 1e-6,
};
let iv = ImpliedVol::new(iv_input, Method::Newton);
let iv_vol = iv.output.unwrap().vol;
println!("bs_vol: {}, iv_vol: {}", bs_vol, iv_vol);
let epsilon = 1e-5;
assert!((bs_vol - iv_vol).abs() < epsilon);
})
}
#[test]
fn test_halley() {
let test_data: Vec<BSInput> = vec![
BSInput {
is_call: true,
spot: 135.6,
strike: 100.0,
mat: 3.2,
vol: 0.25,
rate: 0.03,
div: 0.01,
},
BSInput {
is_call: true,
spot: 60.6,
strike: 100.0,
mat: 3.2,
vol: 0.25,
rate: 0.03,
div: 0.01,
},
BSInput {
is_call: true,
spot: 100.0,
strike: 100.0,
mat: 3.2,
vol: 0.35,
rate: 0.03,
div: 0.00,
},
BSInput {
is_call: true,
spot: 100.0,
strike: 100.0,
mat: 1.0,
vol: 0.10,
rate: 0.00,
div: 0.00,
},
];
test_data.iter().for_each(|input| {
let bs_call = BlackScholes::new(input.clone());
let bs_vol = bs_call.input.vol;
let iv_input = IVInput {
price: bs_call.output.unwrap().price,
spot: input.spot,
strike: input.strike,
mat: input.mat,
rate: input.rate,
div: input.div,
iter: 10,
prec: 1e-6,
};
let iv = ImpliedVol::new(iv_input, Method::Halley);
let iv_vol = iv.output.unwrap().vol;
println!("bs_vol: {}, iv_vol: {}", bs_vol, iv_vol);
let epsilon = 1e-5;
assert!((bs_vol - iv_vol).abs() < epsilon);
})
}
}