1use std::f64::consts::PI;
8use std::fmt;
9
10#[cfg(feature = "serde")]
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15#[cfg_attr(
16 feature = "serde",
17 derive(Serialize, Deserialize),
18 serde(rename_all = "snake_case")
19)]
20pub enum OptionType {
21 Call,
22 Put,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
27#[cfg_attr(
28 feature = "serde",
29 derive(Serialize, Deserialize),
30 serde(rename_all = "snake_case")
31)]
32pub enum OptionStyle {
33 #[default]
34 European,
35 American,
36}
37
38#[derive(Debug, Clone, PartialEq)]
40#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
41pub struct BlackScholesInputs {
42 pub spot: f64,
44 pub strike: f64,
46 pub time_to_expiry_years: f64,
48 pub risk_free_rate: f64,
50 pub dividend_yield: f64,
52 pub volatility: f64,
54}
55
56impl BlackScholesInputs {
57 pub fn validate(&self) -> Result<(), OptionError> {
59 if !self.spot.is_finite() || self.spot <= 0.0 {
60 return Err(OptionError::InvalidInput(
61 "spot price must be positive and finite",
62 ));
63 }
64 if !self.strike.is_finite() || self.strike <= 0.0 {
65 return Err(OptionError::InvalidInput(
66 "strike price must be positive and finite",
67 ));
68 }
69 if !self.time_to_expiry_years.is_finite() || self.time_to_expiry_years < 0.0 {
70 return Err(OptionError::InvalidInput(
71 "time to expiry must be non-negative and finite",
72 ));
73 }
74 if !self.risk_free_rate.is_finite() {
75 return Err(OptionError::InvalidInput("risk-free rate must be finite"));
76 }
77 if !self.dividend_yield.is_finite() {
78 return Err(OptionError::InvalidInput("dividend yield must be finite"));
79 }
80 if !self.volatility.is_finite() || self.volatility < 0.0 {
81 return Err(OptionError::InvalidInput(
82 "volatility must be non-negative and finite",
83 ));
84 }
85 Ok(())
86 }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq)]
91#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
92pub struct OptionGreeks {
93 pub delta: f64,
95 pub gamma: f64,
97 pub vega: f64,
99 pub theta_annual: f64,
101 pub theta_daily: f64,
103 pub rho: f64,
105}
106
107#[derive(Debug, Clone, PartialEq)]
109#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
110pub struct OptionPricingResult {
111 pub price: f64,
113 pub intrinsic_value: f64,
115 pub time_value: f64,
117 pub greeks: OptionGreeks,
119}
120
121#[derive(Debug, Clone, PartialEq)]
123pub enum OptionError {
124 InvalidInput(&'static str),
125 PriceBelowIntrinsic,
126 PriceAboveBoundary,
127 SolverMaxIterationsExceeded,
128 SolverFailedToConverge,
129}
130
131impl fmt::Display for OptionError {
132 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133 match self {
134 Self::InvalidInput(msg) => write!(f, "invalid option input: {msg}"),
135 Self::PriceBelowIntrinsic => write!(
136 f,
137 "option price violates lower arbitrage bound (below intrinsic value)"
138 ),
139 Self::PriceAboveBoundary => write!(f, "option price violates upper arbitrage bound"),
140 Self::SolverMaxIterationsExceeded => {
141 write!(f, "implied volatility solver exceeded maximum iterations")
142 }
143 Self::SolverFailedToConverge => {
144 write!(f, "implied volatility solver failed to converge")
145 }
146 }
147 }
148}
149
150impl std::error::Error for OptionError {}
151
152fn horner(x: f64, lead: f64, rest: &[f64]) -> f64 {
155 rest.iter()
156 .fold(lead, |acc, coefficient| acc * x + coefficient)
157}
158
159pub fn normal_cdf(x: f64) -> f64 {
171 let abs_x = x.abs();
172 if abs_x > 37.0 {
173 return if x > 0.0 { 1.0 } else { 0.0 };
174 }
175
176 let exponential = (-0.5 * abs_x * abs_x).exp();
177 let upper_tail = if abs_x < 7.071_067_811_865_475 {
178 let numerator = horner(
180 abs_x,
181 3.526_249_659_989_109e-2,
182 &[
183 0.700_383_064_443_688,
184 6.373_962_203_531_65,
185 33.912_866_078_383,
186 112.079_291_497_871,
187 221.213_596_169_931,
188 220.206_867_912_376,
189 ],
190 );
191 let denominator = horner(
192 abs_x,
193 8.838_834_764_831_844e-2,
194 &[
195 1.755_667_163_182_64,
196 16.064_177_579_207,
197 86.780_732_202_946_1,
198 296.564_248_779_674,
199 637.333_633_378_831,
200 793.826_512_519_948,
201 440.413_735_824_752,
202 ],
203 );
204 exponential * numerator / denominator
205 } else {
206 let mut fraction = abs_x + 0.65;
208 for term in [4.0, 3.0, 2.0, 1.0] {
209 fraction = abs_x + term / fraction;
210 }
211 exponential / (fraction * 2.506_628_274_631_000_5)
212 };
213
214 if x > 0.0 {
215 1.0 - upper_tail
216 } else {
217 upper_tail
218 }
219}
220
221pub fn normal_pdf(x: f64) -> f64 {
223 (1.0 / (2.0 * PI).sqrt()) * (-0.5 * x * x).exp()
224}
225
226pub fn black_scholes_merton(
228 option_type: OptionType,
229 inputs: &BlackScholesInputs,
230) -> Result<OptionPricingResult, OptionError> {
231 inputs.validate()?;
232
233 let s = inputs.spot;
234 let k = inputs.strike;
235 let t = inputs.time_to_expiry_years;
236 let r = inputs.risk_free_rate;
237 let q = inputs.dividend_yield;
238 let sigma = inputs.volatility;
239
240 let intrinsic = match option_type {
241 OptionType::Call => (s - k).max(0.0),
242 OptionType::Put => (k - s).max(0.0),
243 };
244
245 if t <= 1e-12 || sigma <= 1e-12 {
247 let delta = match option_type {
248 OptionType::Call => {
249 if s > k {
250 1.0
251 } else if (s - k).abs() < 1e-12 {
252 0.5
253 } else {
254 0.0
255 }
256 }
257 OptionType::Put => {
258 if s < k {
259 -1.0
260 } else if (s - k).abs() < 1e-12 {
261 -0.5
262 } else {
263 0.0
264 }
265 }
266 };
267 return Ok(OptionPricingResult {
268 price: intrinsic,
269 intrinsic_value: intrinsic,
270 time_value: 0.0,
271 greeks: OptionGreeks {
272 delta,
273 gamma: 0.0,
274 vega: 0.0,
275 theta_annual: 0.0,
276 theta_daily: 0.0,
277 rho: 0.0,
278 },
279 });
280 }
281
282 let sqrt_t = t.sqrt();
283 let d1 = ((s / k).ln() + (r - q + 0.5 * sigma * sigma) * t) / (sigma * sqrt_t);
284 let d2 = d1 - sigma * sqrt_t;
285
286 let df_q = (-q * t).exp();
287 let df_r = (-r * t).exp();
288
289 let pdf_d1 = normal_pdf(d1);
290
291 let price = match option_type {
292 OptionType::Call => s * df_q * normal_cdf(d1) - k * df_r * normal_cdf(d2),
293 OptionType::Put => k * df_r * normal_cdf(-d2) - s * df_q * normal_cdf(-d1),
294 };
295
296 let delta = match option_type {
298 OptionType::Call => df_q * normal_cdf(d1),
299 OptionType::Put => df_q * (normal_cdf(d1) - 1.0),
300 };
301
302 let gamma = (df_q * pdf_d1) / (s * sigma * sqrt_t);
303 let vega = s * df_q * sqrt_t * pdf_d1;
304
305 let theta_common = -(s * df_q * pdf_d1 * sigma) / (2.0 * sqrt_t);
306 let theta_annual = match option_type {
307 OptionType::Call => {
308 theta_common - r * k * df_r * normal_cdf(d2) + q * s * df_q * normal_cdf(d1)
309 }
310 OptionType::Put => {
311 theta_common + r * k * df_r * normal_cdf(-d2) - q * s * df_q * normal_cdf(-d1)
312 }
313 };
314 let theta_daily = theta_annual / 365.0;
315
316 let rho = match option_type {
317 OptionType::Call => k * t * df_r * normal_cdf(d2),
318 OptionType::Put => -k * t * df_r * normal_cdf(-d2),
319 };
320
321 let time_value = (price - intrinsic).max(0.0);
322
323 Ok(OptionPricingResult {
324 price,
325 intrinsic_value: intrinsic,
326 time_value,
327 greeks: OptionGreeks {
328 delta,
329 gamma,
330 vega,
331 theta_annual,
332 theta_daily,
333 rho,
334 },
335 })
336}
337
338pub fn black_76(
340 option_type: OptionType,
341 forward: f64,
342 strike: f64,
343 time_to_expiry_years: f64,
344 risk_free_rate: f64,
345 volatility: f64,
346) -> Result<OptionPricingResult, OptionError> {
347 if forward <= 0.0 {
348 return Err(OptionError::InvalidInput(
349 "forward must be positive and finite",
350 ));
351 }
352 let inputs = BlackScholesInputs {
354 spot: forward,
355 strike,
356 time_to_expiry_years,
357 risk_free_rate,
358 dividend_yield: risk_free_rate,
359 volatility,
360 };
361 black_scholes_merton(option_type, &inputs)
362}
363
364pub fn implied_volatility(
368 option_type: OptionType,
369 market_price: f64,
370 spot: f64,
371 strike: f64,
372 time_to_expiry_years: f64,
373 risk_free_rate: f64,
374 dividend_yield: f64,
375) -> Result<f64, OptionError> {
376 if !market_price.is_finite() || market_price <= 0.0 {
377 return Err(OptionError::InvalidInput(
378 "market price must be positive and finite",
379 ));
380 }
381 if time_to_expiry_years <= 1e-12 {
382 return Err(OptionError::InvalidInput(
383 "cannot solve IV for expired option (T=0)",
384 ));
385 }
386
387 let df_q = (-dividend_yield * time_to_expiry_years).exp();
388 let df_r = (-risk_free_rate * time_to_expiry_years).exp();
389
390 let (lower_bound, upper_bound) = match option_type {
392 OptionType::Call => ((spot * df_q - strike * df_r).max(0.0), spot * df_q),
393 OptionType::Put => ((strike * df_r - spot * df_q).max(0.0), strike * df_r),
394 };
395
396 if market_price < lower_bound - 1e-7 {
397 return Err(OptionError::PriceBelowIntrinsic);
398 }
399 if market_price > upper_bound + 1e-7 {
400 return Err(OptionError::PriceAboveBoundary);
401 }
402
403 let mut inputs = BlackScholesInputs {
405 spot,
406 strike,
407 time_to_expiry_years,
408 risk_free_rate,
409 dividend_yield,
410 volatility: 0.20,
411 };
412
413 let mut vol_low = 1e-4f64;
415 let mut vol_high = 5.0f64;
416
417 inputs.volatility = vol_low;
418 let price_low = black_scholes_merton(option_type, &inputs)?.price;
419 if (price_low - market_price).abs() < 1e-7 {
420 return Ok(vol_low);
421 }
422
423 inputs.volatility = vol_high;
424 let mut price_high = black_scholes_merton(option_type, &inputs)?.price;
425 while price_high < market_price && vol_high < 20.0 {
426 vol_high *= 2.0;
427 inputs.volatility = vol_high;
428 price_high = black_scholes_merton(option_type, &inputs)?.price;
429 }
430
431 if price_high < market_price {
432 return Err(OptionError::PriceAboveBoundary);
433 }
434
435 let mut current_vol = 0.5 * (vol_low + vol_high);
437 let max_iter = 100;
438 let tol = 1e-8;
439
440 for _ in 0..max_iter {
441 inputs.volatility = current_vol;
442 let res = black_scholes_merton(option_type, &inputs)?;
443 let diff = res.price - market_price;
444
445 if diff.abs() < tol {
446 return Ok(current_vol);
447 }
448
449 if diff > 0.0 {
451 vol_high = current_vol;
452 } else {
453 vol_low = current_vol;
454 }
455
456 let vega = res.greeks.vega;
457 let mut step_accepted = false;
458
459 if vega > 1e-12 {
460 let next_newton = current_vol - diff / vega;
461 if next_newton > vol_low && next_newton < vol_high {
462 current_vol = next_newton;
463 step_accepted = true;
464 }
465 }
466
467 if !step_accepted {
468 current_vol = 0.5 * (vol_low + vol_high);
470 }
471
472 if (vol_high - vol_low) < 1e-10 {
473 return Ok(current_vol);
474 }
475 }
476
477 Ok(current_vol)
478}
479
480pub fn verify_put_call_parity(
486 call_price: f64,
487 put_price: f64,
488 spot: f64,
489 strike: f64,
490 time_to_expiry_years: f64,
491 risk_free_rate: f64,
492 dividend_yield: f64,
493) -> f64 {
494 let forward_term = spot * (-dividend_yield * time_to_expiry_years).exp();
495 let discount_strike = strike * (-risk_free_rate * time_to_expiry_years).exp();
496 (call_price - put_price) - (forward_term - discount_strike)
497}