use super::black_scholes::BlackScholes;
use super::error::IVError;
use super::solver::{SolverConfig, solve_iv};
use super::types::{IVParams, IVQuality, IVResult, PriceSource};
use crate::orderbook::book::OrderBook;
use pricelevel::Side;
const HIGH_QUALITY_SPREAD_BPS: f64 = 100.0;
const MEDIUM_QUALITY_SPREAD_BPS: f64 = 500.0;
#[derive(Debug, Clone)]
pub struct IVConfig {
pub solver: SolverConfig,
pub max_spread_bps: f64,
pub price_scale: f64,
}
impl Default for IVConfig {
fn default() -> Self {
Self {
solver: SolverConfig::default(),
max_spread_bps: 1000.0,
price_scale: 1.0,
}
}
}
impl IVConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_max_spread(mut self, max_spread_bps: f64) -> Self {
self.max_spread_bps = max_spread_bps;
self
}
#[must_use]
pub fn with_price_scale(mut self, price_scale: f64) -> Self {
self.price_scale = price_scale;
self
}
#[must_use]
pub fn with_solver(mut self, solver: SolverConfig) -> Self {
self.solver = solver;
self
}
}
impl<T> OrderBook<T>
where
T: Clone + Send + Sync + Default + 'static,
{
pub fn implied_volatility(
&self,
params: &IVParams,
price_source: PriceSource,
) -> Result<IVResult, IVError> {
self.implied_volatility_with_config(params, price_source, &IVConfig::default())
}
pub fn implied_volatility_with_config(
&self,
params: &IVParams,
price_source: PriceSource,
config: &IVConfig,
) -> Result<IVResult, IVError> {
let (price, spread_bps) = self.extract_price_for_iv(price_source, config.price_scale)?;
if spread_bps > config.max_spread_bps {
return Err(IVError::SpreadTooWide {
spread_bps,
threshold_bps: config.max_spread_bps,
});
}
let intrinsic = params.intrinsic_value();
if price < intrinsic - config.solver.tolerance {
return Err(IVError::PriceBelowIntrinsic { price, intrinsic });
}
let quality = spread_to_quality(spread_bps);
let (iv, iterations) = solve_iv(params, price, &config.solver)?;
Ok(IVResult::new(iv, price, spread_bps, iterations, quality))
}
fn extract_price_for_iv(
&self,
source: PriceSource,
price_scale: f64,
) -> Result<(f64, f64), IVError> {
let best_bid = self.best_bid();
let best_ask = self.best_ask();
match (best_bid, best_ask) {
(Some(bid), Some(ask)) => {
let bid_f = bid as f64 / price_scale;
let ask_f = ask as f64 / price_scale;
let mid = (bid_f + ask_f) / 2.0;
let spread_bps = if mid > 0.0 {
((ask_f - bid_f) / mid) * 10_000.0
} else {
0.0
};
let price = match source {
PriceSource::MidPrice => mid,
PriceSource::WeightedMid => {
self.weighted_mid_price_for_iv(bid, ask, price_scale)
}
PriceSource::LastTrade => self
.last_trade_price()
.map(|p| p as f64 / price_scale)
.unwrap_or(mid),
};
Ok((price, spread_bps))
}
(Some(bid), None) => {
let price = bid as f64 / price_scale;
Ok((price, 10_000.0)) }
(None, Some(ask)) => {
let price = ask as f64 / price_scale;
Ok((price, 10_000.0)) }
(None, None) => Err(IVError::NoPriceAvailable),
}
}
fn weighted_mid_price_for_iv(&self, bid: u128, ask: u128, price_scale: f64) -> f64 {
let bid_f = bid as f64 / price_scale;
let ask_f = ask as f64 / price_scale;
let bid_qty = self.quantity_at_price(bid, Side::Buy);
let ask_qty = self.quantity_at_price(ask, Side::Sell);
let total_qty = bid_qty + ask_qty;
if total_qty == 0 {
(bid_f + ask_f) / 2.0
} else {
let bid_weight = ask_qty as f64 / total_qty as f64;
let ask_weight = bid_qty as f64 / total_qty as f64;
bid_f * bid_weight + ask_f * ask_weight
}
}
fn quantity_at_price(&self, price: u128, side: Side) -> u64 {
let price_levels = match side {
Side::Buy => &self.bids,
Side::Sell => &self.asks,
};
price_levels
.get(&price)
.and_then(|entry| entry.value().total_quantity().ok())
.unwrap_or(0)
}
#[must_use]
pub fn theoretical_price(params: &IVParams, volatility: f64) -> f64 {
BlackScholes::price(params, volatility)
}
#[must_use]
pub fn option_vega(params: &IVParams, volatility: f64) -> f64 {
BlackScholes::vega(params, volatility)
}
#[must_use]
pub fn option_delta(params: &IVParams, volatility: f64) -> f64 {
BlackScholes::delta(params, volatility)
}
#[must_use]
pub fn option_gamma(params: &IVParams, volatility: f64) -> f64 {
BlackScholes::gamma(params, volatility)
}
#[must_use]
pub fn option_theta(params: &IVParams, volatility: f64) -> f64 {
BlackScholes::theta(params, volatility)
}
}
fn spread_to_quality(spread_bps: f64) -> IVQuality {
if spread_bps < HIGH_QUALITY_SPREAD_BPS {
IVQuality::High
} else if spread_bps < MEDIUM_QUALITY_SPREAD_BPS {
IVQuality::Medium
} else {
IVQuality::Low
}
}
#[cfg(test)]
mod tests {
use super::*;
use pricelevel::{Id, TimeInForce};
fn create_test_book() -> OrderBook<()> {
let book = OrderBook::<()>::new("TEST-OPT");
let _ = book.add_limit_order(
Id::new(),
450, 100,
Side::Buy,
TimeInForce::Gtc,
None,
);
let _ = book.add_limit_order(
Id::new(),
470, 100,
Side::Sell,
TimeInForce::Gtc,
None,
);
book
}
#[test]
fn test_extract_price_mid() {
let book = create_test_book();
let config = IVConfig::default().with_price_scale(100.0);
let (price, spread_bps) = book
.extract_price_for_iv(PriceSource::MidPrice, config.price_scale)
.unwrap();
assert!((price - 4.60).abs() < 0.01);
assert!(spread_bps > 400.0 && spread_bps < 500.0);
}
#[test]
fn test_extract_price_weighted_mid() {
let book = OrderBook::<()>::new("TEST-OPT");
let _ = book.add_limit_order(Id::new(), 450, 1000, Side::Buy, TimeInForce::Gtc, None);
let _ = book.add_limit_order(Id::new(), 470, 100, Side::Sell, TimeInForce::Gtc, None);
let config = IVConfig::default().with_price_scale(100.0);
let (price, _) = book
.extract_price_for_iv(PriceSource::WeightedMid, config.price_scale)
.unwrap();
assert!(price > 4.60); }
#[test]
fn test_extract_price_last_trade() {
let book = create_test_book();
let _ = book.match_market_order(Id::new(), 50, Side::Buy);
let config = IVConfig::default().with_price_scale(100.0);
let (price, _) = book
.extract_price_for_iv(PriceSource::LastTrade, config.price_scale)
.unwrap();
assert!((price - 4.70).abs() < 0.01);
}
#[test]
fn test_extract_price_no_orders() {
let book = OrderBook::<()>::new("EMPTY");
let result = book.extract_price_for_iv(PriceSource::MidPrice, 1.0);
assert!(matches!(result, Err(IVError::NoPriceAvailable)));
}
#[test]
fn test_implied_volatility_calculation() {
let book = OrderBook::<()>::new("TEST-OPT");
let _ = book.add_limit_order(Id::new(), 540, 100, Side::Buy, TimeInForce::Gtc, None);
let _ = book.add_limit_order(Id::new(), 550, 100, Side::Sell, TimeInForce::Gtc, None);
let params = IVParams::call(100.0, 100.0, 0.25, 0.05);
let config = IVConfig::default().with_price_scale(100.0);
let result = book
.implied_volatility_with_config(¶ms, PriceSource::MidPrice, &config)
.unwrap();
assert!(result.iv > 0.20 && result.iv < 0.30);
assert!(result.iterations < 20);
}
#[test]
fn test_implied_volatility_spread_too_wide() {
let book = OrderBook::<()>::new("TEST-OPT");
let _ = book.add_limit_order(Id::new(), 100, 100, Side::Buy, TimeInForce::Gtc, None);
let _ = book.add_limit_order(Id::new(), 500, 100, Side::Sell, TimeInForce::Gtc, None);
let params = IVParams::call(100.0, 100.0, 0.25, 0.05);
let config = IVConfig::default()
.with_price_scale(100.0)
.with_max_spread(500.0);
let result = book.implied_volatility_with_config(¶ms, PriceSource::MidPrice, &config);
assert!(matches!(result, Err(IVError::SpreadTooWide { .. })));
}
#[test]
fn test_spread_to_quality() {
assert_eq!(spread_to_quality(50.0), IVQuality::High);
assert_eq!(spread_to_quality(99.0), IVQuality::High);
assert_eq!(spread_to_quality(100.0), IVQuality::Medium);
assert_eq!(spread_to_quality(300.0), IVQuality::Medium);
assert_eq!(spread_to_quality(499.0), IVQuality::Medium);
assert_eq!(spread_to_quality(500.0), IVQuality::Low);
assert_eq!(spread_to_quality(1000.0), IVQuality::Low);
}
#[test]
fn test_iv_config_builder() {
let config = IVConfig::new()
.with_max_spread(2000.0)
.with_price_scale(100.0)
.with_solver(SolverConfig::default().with_max_iterations(50));
assert!((config.max_spread_bps - 2000.0).abs() < 1e-10);
assert!((config.price_scale - 100.0).abs() < 1e-10);
assert_eq!(config.solver.max_iterations, 50);
}
#[test]
fn test_theoretical_price() {
let params = IVParams::call(100.0, 100.0, 0.25, 0.05);
let price = OrderBook::<()>::theoretical_price(¶ms, 0.25);
assert!(price > 4.0 && price < 7.0);
}
#[test]
fn test_option_greeks() {
let params = IVParams::call(100.0, 100.0, 0.25, 0.05);
let vol = 0.25;
let delta = OrderBook::<()>::option_delta(¶ms, vol);
let gamma = OrderBook::<()>::option_gamma(¶ms, vol);
let vega = OrderBook::<()>::option_vega(¶ms, vol);
let theta = OrderBook::<()>::option_theta(¶ms, vol);
assert!(delta > 0.4 && delta < 0.6);
assert!(gamma > 0.0);
assert!(vega > 0.0);
assert!(theta < 0.0);
}
#[test]
fn test_one_sided_market_bid_only() {
let book = OrderBook::<()>::new("TEST-OPT");
let _ = book.add_limit_order(Id::new(), 450, 100, Side::Buy, TimeInForce::Gtc, None);
let (price, spread_bps) = book
.extract_price_for_iv(PriceSource::MidPrice, 100.0)
.unwrap();
assert!((price - 4.50).abs() < 0.01);
assert!((spread_bps - 10_000.0).abs() < 1.0); }
#[test]
fn test_one_sided_market_ask_only() {
let book = OrderBook::<()>::new("TEST-OPT");
let _ = book.add_limit_order(Id::new(), 470, 100, Side::Sell, TimeInForce::Gtc, None);
let (price, spread_bps) = book
.extract_price_for_iv(PriceSource::MidPrice, 100.0)
.unwrap();
assert!((price - 4.70).abs() < 0.01);
assert!((spread_bps - 10_000.0).abs() < 1.0); }
}