1use crate::derivatives::norm::{norm_cdf, norm_pdf};
61use crate::derivatives::types::{
62 forward_moneyness, intrinsic, time_value, validate_bsm_params, BsmParams, OptionType,
63 ValidatedBsm,
64};
65use crate::util::error::FinanceResult;
66use crate::{columns_with_strings, print_table_locale_opt};
67
68#[derive(Clone, Copy, Debug, PartialEq)]
83pub struct BsmGreeks {
84 pub delta: f64,
85 pub gamma: f64,
86 pub vega: f64,
88 pub theta: f64,
90 pub rho: f64,
91}
92
93impl BsmGreeks {
94 #[inline]
98 pub fn vega_per_vol_point(self) -> f64 {
99 self.vega / 100.0
100 }
101
102 #[inline]
106 pub fn theta_per_calendar_day(self) -> f64 {
107 self.theta / 365.25
108 }
109}
110
111#[derive(Clone, Copy, Debug, PartialEq)]
116pub struct BsmTerms {
117 pub d1: f64,
118 pub d2: f64,
119 pub discount: f64,
120 pub dividend_discount: f64,
121 pub sqrt_t: f64,
122}
123
124#[derive(Clone, Debug)]
128pub struct BsmSolution {
129 pub option_type: OptionType,
130 pub params: BsmParams,
131 pub price: f64,
132 pub greeks: BsmGreeks,
133 pub terms: BsmTerms,
134 pub intrinsic: f64,
135 pub time_value: f64,
136 pub forward_moneyness: f64,
137 pub parity_residual: f64,
139 formula: String,
140 symbolic_formula: String,
141}
142
143impl BsmSolution {
144 pub fn formula(&self) -> &str {
145 &self.formula
146 }
147 pub fn symbolic_formula(&self) -> &str {
148 &self.symbolic_formula
149 }
150
151 pub fn print_table(&self) {
159 self.print_table_locale_opt(None, None);
160 }
161
162 pub fn print_table_locale(&self, locale: &num_format::Locale, precision: usize) {
163 self.print_table_locale_opt(Some(locale), Some(precision));
164 }
165
166 fn print_table_locale_opt(
167 &self,
168 locale: Option<&num_format::Locale>,
169 precision: Option<usize>,
170 ) {
171 let columns = columns_with_strings(&[
172 ("type", "s", true),
173 ("price", "f", true),
174 ("delta", "f", true),
175 ("gamma", "f", true),
176 ("vega", "f", true),
177 ("theta", "f", true),
178 ("rho", "f", true),
179 ]);
180 let data = vec![vec![
181 self.option_type.to_string(),
182 self.price.to_string(),
183 self.greeks.delta.to_string(),
184 self.greeks.gamma.to_string(),
185 self.greeks.vega.to_string(),
186 self.greeks.theta.to_string(),
187 self.greeks.rho.to_string(),
188 ]];
189 print_table_locale_opt(&columns, data, locale, precision);
190 }
191}
192
193pub fn bsm_price(params: BsmParams, option_type: OptionType) -> FinanceResult<f64> {
201 validate_bsm_params(params)?;
202 Ok(price_unchecked(params, option_type))
203}
204
205pub fn bsm_greeks(params: BsmParams, option_type: OptionType) -> FinanceResult<BsmGreeks> {
210 validate_bsm_params(params)?;
211 Ok(greeks_unchecked(params, option_type))
212}
213
214pub fn bsm_terms(params: BsmParams) -> FinanceResult<BsmTerms> {
216 validate_bsm_params(params)?;
217 Ok(terms_unchecked(params))
218}
219
220pub fn put_call_parity_residual(params: BsmParams) -> FinanceResult<f64> {
226 let c = bsm_price(params, OptionType::Call)?;
227 let p = bsm_price(params, OptionType::Put)?;
228 let disc = (-params.rate * params.time_years).exp();
229 let div = (-params.dividend_yield * params.time_years).exp();
230 Ok(c - p - (params.spot * div - params.strike * disc))
231}
232
233pub fn bsm_solution(params: BsmParams, option_type: OptionType) -> FinanceResult<BsmSolution> {
245 let _ = ValidatedBsm::new(params)?;
246 let price = price_unchecked(params, option_type);
247 let greeks = greeks_unchecked(params, option_type);
248 let terms = terms_unchecked(params);
249 let intrinsic_v = intrinsic(params.spot, params.strike, option_type)?;
250 let tv = time_value(price, params.spot, params.strike, option_type)?;
251 let fm = forward_moneyness(params)?;
252 let parity = put_call_parity_residual(params)?;
253 let formula = format!(
254 "{option_type} BSM S={} K={} T={} r={} q={} σ={} → price={:.6}",
255 params.spot,
256 params.strike,
257 params.time_years,
258 params.rate,
259 params.dividend_yield,
260 params.vol,
261 price
262 );
263 let symbolic = match option_type {
264 OptionType::Call => {
265 "C = S e^{-qT} N(d1) - K e^{-rT} N(d2); d1 = [ln(S/K)+(r-q+σ²/2)T]/(σ√T); d2 = d1-σ√T"
266 .to_string()
267 }
268 OptionType::Put => "P = K e^{-rT} N(-d2) - S e^{-qT} N(-d1); d1,d2 as in call".to_string(),
269 };
270 Ok(BsmSolution {
271 option_type,
272 params,
273 price,
274 greeks,
275 terms,
276 intrinsic: intrinsic_v,
277 time_value: tv,
278 forward_moneyness: fm,
279 parity_residual: parity,
280 formula,
281 symbolic_formula: symbolic,
282 })
283}
284
285fn terms_unchecked(p: BsmParams) -> BsmTerms {
286 let sqrt_t = p.time_years.sqrt();
287 let discount = (-p.rate * p.time_years).exp();
288 let dividend_discount = (-p.dividend_yield * p.time_years).exp();
289
290 if p.time_years == 0.0 || p.vol == 0.0 {
291 let forward = p.spot * ((p.rate - p.dividend_yield) * p.time_years).exp();
292 let d1 = if forward > p.strike {
293 f64::INFINITY
294 } else if forward < p.strike {
295 f64::NEG_INFINITY
296 } else {
297 0.0
298 };
299 return BsmTerms {
300 d1,
301 d2: d1,
302 discount,
303 dividend_discount,
304 sqrt_t,
305 };
306 }
307
308 let sig_s = p.vol * sqrt_t;
309 let d1 = ((p.spot / p.strike).ln()
310 + (p.rate - p.dividend_yield + 0.5 * p.vol * p.vol) * p.time_years)
311 / sig_s;
312 let d2 = d1 - sig_s;
313 BsmTerms {
314 d1,
315 d2,
316 discount,
317 dividend_discount,
318 sqrt_t,
319 }
320}
321
322fn price_unchecked(p: BsmParams, option_type: OptionType) -> f64 {
323 if p.time_years == 0.0 {
324 return match option_type {
325 OptionType::Call => (p.spot - p.strike).max(0.0),
326 OptionType::Put => (p.strike - p.spot).max(0.0),
327 };
328 }
329 if p.vol == 0.0 {
330 let f = p.spot * ((p.rate - p.dividend_yield) * p.time_years).exp();
331 let disc = (-p.rate * p.time_years).exp();
332 return match option_type {
333 OptionType::Call => disc * (f - p.strike).max(0.0),
334 OptionType::Put => disc * (p.strike - f).max(0.0),
335 };
336 }
337
338 let t = terms_unchecked(p);
339 let df_q = t.dividend_discount;
340 let df_r = t.discount;
341 match option_type {
342 OptionType::Call => p.spot * df_q * norm_cdf(t.d1) - p.strike * df_r * norm_cdf(t.d2),
343 OptionType::Put => p.strike * df_r * norm_cdf(-t.d2) - p.spot * df_q * norm_cdf(-t.d1),
344 }
345}
346
347fn greeks_unchecked(p: BsmParams, option_type: OptionType) -> BsmGreeks {
348 if p.time_years == 0.0 {
349 let delta = match option_type {
350 OptionType::Call => {
351 if p.spot > p.strike {
352 1.0
353 } else if p.spot < p.strike {
354 0.0
355 } else {
356 0.5
357 }
358 }
359 OptionType::Put => {
360 if p.spot < p.strike {
361 -1.0
362 } else if p.spot > p.strike {
363 0.0
364 } else {
365 -0.5
366 }
367 }
368 };
369 return BsmGreeks {
370 delta,
371 gamma: 0.0,
372 vega: 0.0,
373 theta: 0.0,
374 rho: 0.0,
375 };
376 }
377
378 if p.vol == 0.0 {
379 let price_up = {
380 let mut q = p;
381 q.spot *= 1.0 + 1e-6;
382 price_unchecked(q, option_type)
383 };
384 let price_0 = price_unchecked(p, option_type);
385 let delta = (price_up - price_0) / (p.spot * 1e-6);
386 return BsmGreeks {
387 delta,
388 gamma: 0.0,
389 vega: 0.0,
390 theta: 0.0,
391 rho: 0.0,
392 };
393 }
394
395 let t = terms_unchecked(p);
396 let df_q = t.dividend_discount;
397 let df_r = t.discount;
398 let n_d1 = norm_pdf(t.d1);
399 let sqrt_t = t.sqrt_t;
400 let gamma = df_q * n_d1 / (p.spot * p.vol * sqrt_t);
401 let vega = p.spot * df_q * n_d1 * sqrt_t;
402
403 let (delta, theta, rho) = match option_type {
404 OptionType::Call => {
405 let delta = df_q * norm_cdf(t.d1);
406 let theta = -p.spot * df_q * n_d1 * p.vol / (2.0 * sqrt_t)
407 - p.rate * p.strike * df_r * norm_cdf(t.d2)
408 + p.dividend_yield * p.spot * df_q * norm_cdf(t.d1);
409 let rho = p.strike * p.time_years * df_r * norm_cdf(t.d2);
410 (delta, theta, rho)
411 }
412 OptionType::Put => {
413 let delta = df_q * (norm_cdf(t.d1) - 1.0);
414 let theta = -p.spot * df_q * n_d1 * p.vol / (2.0 * sqrt_t)
415 + p.rate * p.strike * df_r * norm_cdf(-t.d2)
416 - p.dividend_yield * p.spot * df_q * norm_cdf(-t.d1);
417 let rho = -p.strike * p.time_years * df_r * norm_cdf(-t.d2);
418 (delta, theta, rho)
419 }
420 };
421
422 BsmGreeks {
423 delta,
424 gamma,
425 vega,
426 theta,
427 rho,
428 }
429}
430
431pub(crate) fn price_raw(params: BsmParams, option_type: OptionType) -> FinanceResult<f64> {
432 bsm_price(params, option_type)
433}
434
435pub(crate) fn vega_raw(params: BsmParams, option_type: OptionType) -> FinanceResult<f64> {
436 Ok(bsm_greeks(params, option_type)?.vega)
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442
443 #[test]
444 fn atm_call_textbook() {
445 let p = BsmParams::atm_one_year(100.0, 0.05, 0.20);
446 let c = bsm_price(p, OptionType::Call).unwrap();
447 assert!((c - 10.450_583_57).abs() < 1e-4);
448 }
449
450 #[test]
451 fn put_call_parity() {
452 let p = BsmParams {
453 spot: 100.0,
454 strike: 95.0,
455 time_years: 0.5,
456 rate: 0.03,
457 dividend_yield: 0.01,
458 vol: 0.25,
459 };
460 assert!(put_call_parity_residual(p).unwrap().abs() < 1e-10);
461 }
462
463 #[test]
464 fn expiry_intrinsic() {
465 let p = BsmParams {
466 spot: 110.0,
467 strike: 100.0,
468 time_years: 0.0,
469 rate: 0.05,
470 dividend_yield: 0.0,
471 vol: 0.2,
472 };
473 assert!((bsm_price(p, OptionType::Call).unwrap() - 10.0).abs() < 1e-12);
474 assert!((bsm_price(p, OptionType::Put).unwrap()).abs() < 1e-12);
475 }
476
477 #[test]
478 fn delta_bounds_call() {
479 let p = BsmParams::atm_one_year(100.0, 0.05, 0.2);
480 let d = bsm_greeks(p, OptionType::Call).unwrap().delta;
481 assert!(d > 0.0 && d < 1.0);
482 }
483
484 #[test]
485 fn vega_scale_helpers() {
486 let g = bsm_greeks(BsmParams::atm_one_year(100.0, 0.05, 0.2), OptionType::Call).unwrap();
487 assert!((g.vega_per_vol_point() * 100.0 - g.vega).abs() < 1e-12);
488 assert!((g.theta_per_calendar_day() * 365.25 - g.theta).abs() < 1e-12);
489 }
490
491 #[test]
492 fn rejects_bad_spot() {
493 let mut p = BsmParams::atm_one_year(100.0, 0.05, 0.2);
494 p.spot = 0.0;
495 assert!(bsm_price(p, OptionType::Call).is_err());
496 }
497}