1use crate::error::FinError;
17use rust_decimal::prelude::ToPrimitive;
18use rust_decimal::Decimal;
19
20fn phi(x: f64) -> f64 {
24 crate::normal::pdf(x)
25}
26
27fn big_phi(x: f64) -> f64 {
29 crate::normal::cdf(x)
30}
31
32fn to_f64(d: Decimal) -> Result<f64, FinError> {
34 d.to_f64().ok_or(FinError::ArithmeticOverflow)
35}
36
37fn from_f64(f: f64) -> Result<Decimal, FinError> {
39 if !f.is_finite() {
40 return Err(FinError::ArithmeticOverflow);
41 }
42 Decimal::try_from(f).map_err(|_| FinError::ArithmeticOverflow)
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
50pub enum OptionType {
51 Call,
53 Put,
55}
56
57#[derive(Debug, Clone)]
59#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
60pub struct OptionSpec {
61 pub strike: Decimal,
63 pub expiry_days: u32,
65 pub spot: Decimal,
67 pub risk_free_rate: Decimal,
69 pub volatility: Decimal,
71 pub option_type: OptionType,
73}
74
75impl OptionSpec {
76 fn validate(&self) -> Result<(), FinError> {
78 if self.strike <= Decimal::ZERO {
79 return Err(FinError::InvalidInput(
80 "strike must be positive".to_owned(),
81 ));
82 }
83 if self.expiry_days == 0 {
84 return Err(FinError::InvalidInput(
85 "expiry_days must be > 0".to_owned(),
86 ));
87 }
88 if self.spot <= Decimal::ZERO {
89 return Err(FinError::InvalidInput(
90 "spot must be positive".to_owned(),
91 ));
92 }
93 if self.volatility <= Decimal::ZERO {
94 return Err(FinError::InvalidInput(
95 "volatility must be positive".to_owned(),
96 ));
97 }
98 Ok(())
99 }
100}
101
102#[derive(Debug, Clone)]
104#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
105pub struct OptionGreeks {
106 pub delta: Decimal,
108 pub gamma: Decimal,
110 pub theta: Decimal,
112 pub vega: Decimal,
114 pub rho: Decimal,
116}
117
118pub struct BlackScholes;
125
126impl BlackScholes {
127 pub fn greeks(spec: &OptionSpec) -> Result<OptionGreeks, FinError> {
133 spec.validate()?;
134
135 let s = to_f64(spec.spot)?;
136 let k = to_f64(spec.strike)?;
137 let r = to_f64(spec.risk_free_rate)?;
138 let v = to_f64(spec.volatility)?;
139 let t = f64::from(spec.expiry_days) / 365.0;
141
142 let sqrt_t = t.sqrt();
143 let ln_sk = (s / k).ln();
144 let d1 = (ln_sk + (r + 0.5 * v * v) * t) / (v * sqrt_t);
145 let d2 = d1 - v * sqrt_t;
146 let exp_rt = (-r * t).exp();
147
148 let (delta, gamma, theta, vega, rho) = match spec.option_type {
149 OptionType::Call => {
150 let nd1 = big_phi(d1);
151 let nd2 = big_phi(d2);
152 let phi_d1 = phi(d1);
153
154 let delta = nd1;
155 let gamma = phi_d1 / (s * v * sqrt_t);
156 let theta =
158 (-(s * phi_d1 * v) / (2.0 * sqrt_t) - r * k * exp_rt * nd2) / 365.0;
159 let vega = s * phi_d1 * sqrt_t / 100.0; let rho = k * t * exp_rt * nd2 / 100.0; (delta, gamma, theta, vega, rho)
163 }
164 OptionType::Put => {
165 let nd1_neg = big_phi(-d1); let nd2_neg = big_phi(-d2); let phi_d1 = phi(d1);
169
170 let delta = -nd1_neg;
172 let gamma = phi_d1 / (s * v * sqrt_t);
173 let theta =
174 (-(s * phi_d1 * v) / (2.0 * sqrt_t) + r * k * exp_rt * nd2_neg) / 365.0;
175 let vega = s * phi_d1 * sqrt_t / 100.0;
176 let rho = -k * t * exp_rt * nd2_neg / 100.0;
177
178 (delta, gamma, theta, vega, rho)
179 }
180 };
181
182 Ok(OptionGreeks {
183 delta: from_f64(delta)?,
184 gamma: from_f64(gamma)?,
185 theta: from_f64(theta)?,
186 vega: from_f64(vega)?,
187 rho: from_f64(rho)?,
188 })
189 }
190
191 pub fn price(spec: &OptionSpec) -> Result<Decimal, FinError> {
197 spec.validate()?;
198
199 let s = to_f64(spec.spot)?;
200 let k = to_f64(spec.strike)?;
201 let r = to_f64(spec.risk_free_rate)?;
202 let v = to_f64(spec.volatility)?;
203 let t = f64::from(spec.expiry_days) / 365.0;
204
205 let sqrt_t = t.sqrt();
206 let d1 = ((s / k).ln() + (r + 0.5 * v * v) * t) / (v * sqrt_t);
207 let d2 = d1 - v * sqrt_t;
208 let exp_rt = (-r * t).exp();
209
210 let price = match spec.option_type {
211 OptionType::Call => s * big_phi(d1) - k * exp_rt * big_phi(d2),
212 OptionType::Put => k * exp_rt * big_phi(-d2) - s * big_phi(-d1),
213 };
214
215 from_f64(price)
216 }
217
218 pub fn implied_vol(market_price: Decimal, spec: &OptionSpec) -> Result<Decimal, FinError> {
229 if market_price <= Decimal::ZERO {
230 return Err(FinError::InvalidInput(
231 "market_price must be positive".to_owned(),
232 ));
233 }
234 if spec.strike <= Decimal::ZERO {
236 return Err(FinError::InvalidInput(
237 "strike must be positive".to_owned(),
238 ));
239 }
240 if spec.expiry_days == 0 {
241 return Err(FinError::InvalidInput(
242 "expiry_days must be > 0".to_owned(),
243 ));
244 }
245 if spec.spot <= Decimal::ZERO {
246 return Err(FinError::InvalidInput(
247 "spot must be positive".to_owned(),
248 ));
249 }
250
251 let target = to_f64(market_price)?;
252
253 const LOW_VOL: f64 = 1e-6;
254 const HIGH_VOL: f64 = 5.0;
255 const TOL: f64 = 1e-7;
256 const MAX_ITER: usize = 200;
257
258 let price_at = |vol: f64| -> Result<f64, FinError> {
259 let trial_spec = OptionSpec {
260 volatility: from_f64(vol)?,
261 ..spec.clone()
262 };
263 to_f64(Self::price(&trial_spec)?)
264 };
265
266 let mut lo = LOW_VOL;
267 let mut hi = HIGH_VOL;
268
269 let p_lo = price_at(lo)?;
270 let p_hi = price_at(hi)?;
271
272 if target < p_lo || target > p_hi {
274 return Err(FinError::InvalidInput(
275 "market_price is outside no-arbitrage vol bounds [1e-6, 500%]".to_owned(),
276 ));
277 }
278
279 for _ in 0..MAX_ITER {
280 let mid = (lo + hi) / 2.0;
281 let p_mid = price_at(mid)?;
282 let err = p_mid - target;
283 if err.abs() < TOL {
284 return from_f64(mid);
285 }
286 if err < 0.0 {
287 lo = mid;
288 } else {
289 hi = mid;
290 }
291 }
292
293 from_f64((lo + hi) / 2.0)
295 }
296}
297
298#[derive(Debug, Clone)]
302pub struct Leg {
303 pub spec: OptionSpec,
305 pub quantity: i32,
307}
308
309impl Leg {
310 pub fn new(spec: OptionSpec, quantity: i32) -> Self {
312 Self { spec, quantity }
313 }
314
315 fn scaled_greeks(&self) -> Result<OptionGreeks, FinError> {
317 let g = BlackScholes::greeks(&self.spec)?;
318 let q = from_f64(f64::from(self.quantity))?;
319 Ok(OptionGreeks {
320 delta: g.delta * q,
321 gamma: g.gamma * q,
322 theta: g.theta * q,
323 vega: g.vega * q,
324 rho: g.rho * q,
325 })
326 }
327}
328
329#[derive(Debug, Clone)]
331pub struct SpreadGreeks {
332 legs: Vec<Leg>,
333}
334
335impl SpreadGreeks {
336 pub fn new(legs: Vec<Leg>) -> Self {
338 Self { legs }
339 }
340
341 pub fn bull_call_spread(
345 spot: Decimal,
346 low_strike: Decimal,
347 high_strike: Decimal,
348 expiry_days: u32,
349 risk_free_rate: Decimal,
350 volatility: Decimal,
351 ) -> Self {
352 let base = |strike| OptionSpec {
353 strike,
354 expiry_days,
355 spot,
356 risk_free_rate,
357 volatility,
358 option_type: OptionType::Call,
359 };
360 Self::new(vec![
361 Leg::new(base(low_strike), 1),
362 Leg::new(base(high_strike), -1),
363 ])
364 }
365
366 pub fn bear_put_spread(
368 spot: Decimal,
369 low_strike: Decimal,
370 high_strike: Decimal,
371 expiry_days: u32,
372 risk_free_rate: Decimal,
373 volatility: Decimal,
374 ) -> Self {
375 let base = |strike, ot| OptionSpec {
376 strike,
377 expiry_days,
378 spot,
379 risk_free_rate,
380 volatility,
381 option_type: ot,
382 };
383 Self::new(vec![
384 Leg::new(base(high_strike, OptionType::Put), 1),
385 Leg::new(base(low_strike, OptionType::Put), -1),
386 ])
387 }
388
389 pub fn straddle(
391 spot: Decimal,
392 strike: Decimal,
393 expiry_days: u32,
394 risk_free_rate: Decimal,
395 volatility: Decimal,
396 ) -> Self {
397 let base = |ot| OptionSpec {
398 strike,
399 expiry_days,
400 spot,
401 risk_free_rate,
402 volatility,
403 option_type: ot,
404 };
405 Self::new(vec![
406 Leg::new(base(OptionType::Call), 1),
407 Leg::new(base(OptionType::Put), 1),
408 ])
409 }
410
411 #[allow(clippy::too_many_arguments)]
418 pub fn iron_condor(
419 spot: Decimal,
420 put_low: Decimal,
421 put_high: Decimal,
422 call_low: Decimal,
423 call_high: Decimal,
424 expiry_days: u32,
425 risk_free_rate: Decimal,
426 volatility: Decimal,
427 ) -> Self {
428 let mk = |strike, ot, qty| {
429 Leg::new(
430 OptionSpec {
431 strike,
432 expiry_days,
433 spot,
434 risk_free_rate,
435 volatility,
436 option_type: ot,
437 },
438 qty,
439 )
440 };
441 Self::new(vec![
442 mk(put_low, OptionType::Put, 1),
443 mk(put_high, OptionType::Put, -1),
444 mk(call_low, OptionType::Call, -1),
445 mk(call_high, OptionType::Call, 1),
446 ])
447 }
448
449 pub fn net_greeks(&self) -> Result<OptionGreeks, FinError> {
454 let mut delta = Decimal::ZERO;
455 let mut gamma = Decimal::ZERO;
456 let mut theta = Decimal::ZERO;
457 let mut vega = Decimal::ZERO;
458 let mut rho = Decimal::ZERO;
459
460 for leg in &self.legs {
461 let g = leg.scaled_greeks()?;
462 delta += g.delta;
463 gamma += g.gamma;
464 theta += g.theta;
465 vega += g.vega;
466 rho += g.rho;
467 }
468
469 Ok(OptionGreeks { delta, gamma, theta, vega, rho })
470 }
471
472 pub fn leg_count(&self) -> usize {
474 self.legs.len()
475 }
476}
477
478#[derive(Debug, Clone, Copy)]
487pub struct BSParams {
488 pub spot: f64,
490 pub strike: f64,
492 pub time_to_expiry: f64,
494 pub risk_free_rate: f64,
496 pub volatility: f64,
498 pub option_type: OptionType,
500}
501
502#[derive(Debug, Clone, Copy)]
509pub struct Greeks {
510 pub delta: f64,
512 pub gamma: f64,
514 pub theta: f64,
516 pub vega: f64,
518 pub rho: f64,
520 pub vanna: f64,
522 pub volga: f64,
524 pub charm: f64,
526 pub speed: f64,
528}
529
530pub struct BSCalculator;
535
536impl BSCalculator {
537 pub fn price(p: &BSParams) -> Option<f64> {
542 let (d1, d2) = Self::d1_d2(p)?;
543 let exp_rt = (-p.risk_free_rate * p.time_to_expiry).exp();
544 let price = match p.option_type {
545 OptionType::Call => p.spot * norm_cdf(d1) - p.strike * exp_rt * norm_cdf(d2),
546 OptionType::Put => p.strike * exp_rt * norm_cdf(-d2) - p.spot * norm_cdf(-d1),
547 };
548 Some(price)
549 }
550
551 pub fn greeks(p: &BSParams) -> Option<Greeks> {
557 let (d1, d2) = Self::d1_d2(p)?;
558 let s = p.spot;
559 let k = p.strike;
560 let r = p.risk_free_rate;
561 let v = p.volatility;
562 let t = p.time_to_expiry;
563 let sqrt_t = t.sqrt();
564 let exp_rt = (-r * t).exp();
565 let phi_d1 = norm_pdf(d1);
566
567 let (delta, theta, rho) = match p.option_type {
569 OptionType::Call => {
570 let nd1 = norm_cdf(d1);
571 let nd2 = norm_cdf(d2);
572 let delta = nd1;
573 let theta = (-(s * phi_d1 * v) / (2.0 * sqrt_t)
574 - r * k * exp_rt * nd2)
575 / 365.0;
576 let rho = k * t * exp_rt * nd2 / 100.0;
577 (delta, theta, rho)
578 }
579 OptionType::Put => {
580 let nd1_neg = norm_cdf(-d1);
581 let nd2_neg = norm_cdf(-d2);
582 let delta = nd1_neg - 1.0;
583 let theta = (-(s * phi_d1 * v) / (2.0 * sqrt_t)
584 + r * k * exp_rt * nd2_neg)
585 / 365.0;
586 let rho = -k * t * exp_rt * nd2_neg / 100.0;
587 (delta, theta, rho)
588 }
589 };
590
591 let gamma = phi_d1 / (s * v * sqrt_t);
592 let vega = s * phi_d1 * sqrt_t / 100.0; let vanna = -phi_d1 * d2 / v;
597
598 let volga = vega * d1 * d2 / v;
602
603 let charm = match p.option_type {
606 OptionType::Call => {
607 (-phi_d1
608 * (2.0 * r * t - d2 * v * sqrt_t)
609 / (2.0 * t * v * sqrt_t))
610 / 365.0
611 }
612 OptionType::Put => {
613 (phi_d1
614 * (2.0 * r * t - d2 * v * sqrt_t)
615 / (2.0 * t * v * sqrt_t))
616 / 365.0
617 }
618 };
619
620 let speed = -gamma / s * (d1 / (v * sqrt_t) + 1.0);
622
623 Some(Greeks { delta, gamma, theta, vega, rho, vanna, volga, charm, speed })
624 }
625
626 pub fn delta(p: &BSParams) -> Option<f64> {
628 let (d1, _) = Self::d1_d2(p)?;
629 Some(match p.option_type {
630 OptionType::Call => norm_cdf(d1),
631 OptionType::Put => norm_cdf(-d1) - 1.0,
632 })
633 }
634
635 pub fn gamma(p: &BSParams) -> Option<f64> {
637 let (d1, _) = Self::d1_d2(p)?;
638 Some(norm_pdf(d1) / (p.spot * p.volatility * p.time_to_expiry.sqrt()))
639 }
640
641 pub fn theta(p: &BSParams) -> Option<f64> {
643 let (d1, d2) = Self::d1_d2(p)?;
644 let phi_d1 = norm_pdf(d1);
645 let exp_rt = (-p.risk_free_rate * p.time_to_expiry).exp();
646 let sqrt_t = p.time_to_expiry.sqrt();
647 let base = -(p.spot * phi_d1 * p.volatility) / (2.0 * sqrt_t);
648 let theta = match p.option_type {
649 OptionType::Call => (base - p.risk_free_rate * p.strike * exp_rt * norm_cdf(d2)) / 365.0,
650 OptionType::Put => (base + p.risk_free_rate * p.strike * exp_rt * norm_cdf(-d2)) / 365.0,
651 };
652 Some(theta)
653 }
654
655 pub fn vega(p: &BSParams) -> Option<f64> {
657 let (d1, _) = Self::d1_d2(p)?;
658 Some(p.spot * norm_pdf(d1) * p.time_to_expiry.sqrt() / 100.0)
659 }
660
661 pub fn rho(p: &BSParams) -> Option<f64> {
663 let (_, d2) = Self::d1_d2(p)?;
664 let exp_rt = (-p.risk_free_rate * p.time_to_expiry).exp();
665 Some(match p.option_type {
666 OptionType::Call => p.strike * p.time_to_expiry * exp_rt * norm_cdf(d2) / 100.0,
667 OptionType::Put => -p.strike * p.time_to_expiry * exp_rt * norm_cdf(-d2) / 100.0,
668 })
669 }
670
671 pub fn implied_volatility(
680 market_price: f64,
681 p: &BSParams,
682 tolerance: f64,
683 max_iter: usize,
684 ) -> Option<f64> {
685 if market_price <= 0.0 || p.spot <= 0.0 || p.strike <= 0.0 || p.time_to_expiry <= 0.0 {
686 return None;
687 }
688
689 let mut sigma = (2.0 * std::f64::consts::PI / p.time_to_expiry).sqrt()
691 * market_price
692 / p.spot;
693 sigma = sigma.clamp(1e-4, 5.0);
695
696 for _ in 0..max_iter {
697 let trial = BSParams { volatility: sigma, ..*p };
698 let price = Self::price(&trial)?;
699 let error = price - market_price;
700 if error.abs() < tolerance {
701 return Some(sigma);
702 }
703 let v = Self::vega(&trial)?;
704 let vega_raw = v * 100.0;
706 if vega_raw.abs() < 1e-10 {
707 break; }
709 sigma -= error / vega_raw;
710 sigma = sigma.clamp(1e-4, 5.0);
711 }
712
713 let trial = BSParams { volatility: sigma, ..*p };
715 let final_price = Self::price(&trial)?;
716 if (final_price - market_price).abs() < tolerance * 10.0 {
717 Some(sigma)
718 } else {
719 None
720 }
721 }
722
723 fn d1_d2(p: &BSParams) -> Option<(f64, f64)> {
727 if p.spot <= 0.0 || p.strike <= 0.0 || p.volatility <= 0.0 || p.time_to_expiry <= 0.0 {
728 return None;
729 }
730 let sqrt_t = p.time_to_expiry.sqrt();
731 let d1 = ((p.spot / p.strike).ln()
732 + (p.risk_free_rate + 0.5 * p.volatility * p.volatility) * p.time_to_expiry)
733 / (p.volatility * sqrt_t);
734 let d2 = d1 - p.volatility * sqrt_t;
735 Some((d1, d2))
736 }
737}
738
739fn norm_pdf(x: f64) -> f64 {
741 crate::normal::pdf(x)
742}
743
744fn norm_cdf(x: f64) -> f64 {
746 crate::normal::cdf(x)
747}
748
749#[cfg(test)]
752mod bs_tests {
753 use super::*;
754
755 fn atm_call() -> BSParams {
756 BSParams {
757 spot: 100.0,
758 strike: 100.0,
759 time_to_expiry: 30.0 / 365.0,
760 risk_free_rate: 0.05,
761 volatility: 0.20,
762 option_type: OptionType::Call,
763 }
764 }
765
766 fn atm_put() -> BSParams {
767 BSParams { option_type: OptionType::Put, ..atm_call() }
768 }
769
770 #[test]
771 fn price_call_positive() {
772 let p = BSCalculator::price(&atm_call()).unwrap();
773 assert!(p > 0.0 && p < 10.0, "call price out of range: {p}");
774 }
775
776 #[test]
777 fn put_call_parity() {
778 let c = BSCalculator::price(&atm_call()).unwrap();
779 let p = BSCalculator::price(&atm_put()).unwrap();
780 let params = atm_call();
781 let forward = params.spot
782 - params.strike * (-params.risk_free_rate * params.time_to_expiry).exp();
783 assert!((c - p - forward).abs() < 1e-6, "put-call parity violated: {}", c - p - forward);
784 }
785
786 #[test]
787 fn delta_call_between_zero_and_one() {
788 let d = BSCalculator::delta(&atm_call()).unwrap();
789 assert!(d > 0.0 && d < 1.0, "call delta out of range: {d}");
790 }
791
792 #[test]
793 fn delta_put_between_neg_one_and_zero() {
794 let d = BSCalculator::delta(&atm_put()).unwrap();
795 assert!(d > -1.0 && d < 0.0, "put delta out of range: {d}");
796 }
797
798 #[test]
799 fn gamma_positive() {
800 let g = BSCalculator::gamma(&atm_call()).unwrap();
801 assert!(g > 0.0, "gamma should be positive: {g}");
802 }
803
804 #[test]
805 fn theta_negative_call() {
806 let t = BSCalculator::theta(&atm_call()).unwrap();
807 assert!(t < 0.0, "theta should be negative for long call: {t}");
808 }
809
810 #[test]
811 fn vega_positive() {
812 let v = BSCalculator::vega(&atm_call()).unwrap();
813 assert!(v > 0.0, "vega should be positive: {v}");
814 }
815
816 #[test]
817 fn all_greeks_available() {
818 let g = BSCalculator::greeks(&atm_call()).unwrap();
819 assert!(g.delta > 0.0);
820 assert!(g.gamma > 0.0);
821 assert!(g.theta < 0.0);
822 assert!(g.vega > 0.0);
823 }
824
825 #[test]
826 fn implied_vol_roundtrip() {
827 let params = atm_call();
828 let market_price = BSCalculator::price(¶ms).unwrap();
829 let iv = BSCalculator::implied_volatility(market_price, ¶ms, 1e-6, 100).unwrap();
830 assert!((iv - params.volatility).abs() < 1e-4, "IV roundtrip error: {}", iv - params.volatility);
831 }
832
833 #[test]
834 fn implied_vol_invalid_price_returns_none() {
835 assert!(BSCalculator::implied_volatility(-1.0, &atm_call(), 1e-6, 100).is_none());
836 }
837
838 #[test]
839 fn invalid_params_return_none() {
840 let bad = BSParams { spot: -1.0, ..atm_call() };
841 assert!(BSCalculator::price(&bad).is_none());
842 assert!(BSCalculator::greeks(&bad).is_none());
843 }
844
845 #[test]
846 fn vanna_sign_for_atm_call() {
847 let g = BSCalculator::greeks(&atm_call()).unwrap();
850 assert!(g.vanna.is_finite(), "vanna should be finite: {}", g.vanna);
852 }
853
854 #[test]
855 fn speed_is_finite() {
856 let g = BSCalculator::greeks(&atm_call()).unwrap();
857 assert!(g.speed.is_finite(), "speed should be finite: {}", g.speed);
858 }
859}
860
861#[cfg(test)]
864mod tests {
865 use super::*;
866 use rust_decimal_macros::dec;
867
868 fn sample_call() -> OptionSpec {
869 OptionSpec {
870 strike: dec!(100),
871 expiry_days: 30,
872 spot: dec!(100),
873 risk_free_rate: dec!(0.05),
874 volatility: dec!(0.20),
875 option_type: OptionType::Call,
876 }
877 }
878
879 fn sample_put() -> OptionSpec {
880 OptionSpec { option_type: OptionType::Put, ..sample_call() }
881 }
882
883 #[test]
884 fn test_bs_price_call_atm_sanity() {
885 let price = BlackScholes::price(&sample_call()).unwrap();
886 assert!(price > dec!(1) && price < dec!(5), "call price={price}");
888 }
889
890 #[test]
891 fn test_bs_price_put_atm_sanity() {
892 let price = BlackScholes::price(&sample_put()).unwrap();
893 assert!(price > dec!(1) && price < dec!(5), "put price={price}");
894 }
895
896 #[test]
897 fn test_put_call_parity() {
898 let call_price = to_f64(BlackScholes::price(&sample_call()).unwrap()).unwrap();
900 let put_price = to_f64(BlackScholes::price(&sample_put()).unwrap()).unwrap();
901 let s = 100.0_f64;
902 let k = 100.0_f64;
903 let r = 0.05_f64;
904 let t = 30.0_f64 / 365.0;
905 let parity_rhs = s - k * (-r * t).exp();
906 let diff = (call_price - put_price - parity_rhs).abs();
907 assert!(diff < 1e-6, "put-call parity violation: {diff}");
908 }
909
910 #[test]
911 fn test_greeks_call_delta_between_zero_and_one() {
912 let g = BlackScholes::greeks(&sample_call()).unwrap();
913 assert!(g.delta > dec!(0) && g.delta < dec!(1));
914 }
915
916 #[test]
917 fn test_greeks_put_delta_between_neg_one_and_zero() {
918 let g = BlackScholes::greeks(&sample_put()).unwrap();
919 assert!(g.delta > dec!(-1) && g.delta < dec!(0));
920 }
921
922 #[test]
923 fn test_greeks_gamma_positive() {
924 let g = BlackScholes::greeks(&sample_call()).unwrap();
925 assert!(g.gamma > dec!(0));
926 }
927
928 #[test]
929 fn test_greeks_theta_negative_call() {
930 let g = BlackScholes::greeks(&sample_call()).unwrap();
931 assert!(g.theta < dec!(0));
932 }
933
934 #[test]
935 fn test_greeks_vega_positive() {
936 let g = BlackScholes::greeks(&sample_call()).unwrap();
937 assert!(g.vega > dec!(0));
938 }
939
940 #[test]
941 fn test_implied_vol_roundtrip() {
942 let spec = sample_call();
943 let market_price = BlackScholes::price(&spec).unwrap();
944 let iv = BlackScholes::implied_vol(market_price, &spec).unwrap();
945 let diff = (iv - spec.volatility).abs();
946 assert!(diff < dec!(0.0001), "IV roundtrip error: {diff}");
947 }
948
949 #[test]
950 fn test_invalid_strike_errors() {
951 let spec = OptionSpec { strike: dec!(0), ..sample_call() };
952 assert!(BlackScholes::price(&spec).is_err());
953 }
954
955 #[test]
956 fn test_invalid_spot_errors() {
957 let spec = OptionSpec { spot: dec!(-1), ..sample_call() };
958 assert!(BlackScholes::greeks(&spec).is_err());
959 }
960
961 #[test]
962 fn test_straddle_delta_near_zero_atm() {
963 let spread = SpreadGreeks::straddle(
964 dec!(100),
965 dec!(100),
966 30,
967 dec!(0.05),
968 dec!(0.20),
969 );
970 let g = spread.net_greeks().unwrap();
971 assert!(g.delta.abs() < dec!(0.1), "straddle delta={}", g.delta);
973 }
974
975 #[test]
976 fn test_bull_call_spread_positive_delta() {
977 let spread = SpreadGreeks::bull_call_spread(
978 dec!(100),
979 dec!(95),
980 dec!(105),
981 30,
982 dec!(0.05),
983 dec!(0.20),
984 );
985 let g = spread.net_greeks().unwrap();
986 assert!(g.delta > dec!(0));
987 }
988
989 #[test]
990 fn test_iron_condor_has_four_legs() {
991 let spread = SpreadGreeks::iron_condor(
992 dec!(100),
993 dec!(85),
994 dec!(90),
995 dec!(110),
996 dec!(115),
997 30,
998 dec!(0.05),
999 dec!(0.20),
1000 );
1001 assert_eq!(spread.leg_count(), 4);
1002 }
1003
1004 #[test]
1005 fn test_bear_put_spread_negative_delta() {
1006 let spread = SpreadGreeks::bear_put_spread(
1007 dec!(100),
1008 dec!(95),
1009 dec!(105),
1010 30,
1011 dec!(0.05),
1012 dec!(0.20),
1013 );
1014 let g = spread.net_greeks().unwrap();
1015 assert!(g.delta < dec!(0));
1016 }
1017}