1use core::fmt;
10use core::ops::{Add, Div, Mul, Neg, Sub};
11
12use crate::errors::{IvError, PricingError};
13use crate::iv::IvSolver;
14use crate::models::bachelier::BachelierParams;
15use crate::models::black76::Black76Params;
16use crate::models::displaced::DisplacedParams;
17
18pub trait Float:
24 Copy
25 + PartialOrd
26 + fmt::Debug
27 + fmt::Display
28 + Add<Output = Self>
29 + Sub<Output = Self>
30 + Mul<Output = Self>
31 + Div<Output = Self>
32 + Neg<Output = Self>
33{
34 fn zero() -> Self;
36
37 fn one() -> Self;
39
40 #[must_use]
42 fn ln(self) -> Self;
43
44 #[must_use]
46 fn exp(self) -> Self;
47
48 #[must_use]
50 fn sqrt(self) -> Self;
51
52 #[must_use]
54 fn abs(self) -> Self;
55
56 #[must_use]
60 fn mul_add(self, a: Self, b: Self) -> Self;
61
62 fn from_f64(val: f64) -> Self;
64
65 fn to_f64(self) -> f64;
67
68 fn pi() -> Self;
70
71 fn is_nan(self) -> bool;
73
74 fn is_infinite(self) -> bool;
76}
77
78impl Float for f64 {
79 #[inline]
80 fn zero() -> Self {
81 0.0_f64
82 }
83
84 #[inline]
85 fn one() -> Self {
86 1.0_f64
87 }
88
89 #[inline]
90 fn ln(self) -> Self {
91 f64::ln(self)
92 }
93
94 #[inline]
95 fn exp(self) -> Self {
96 f64::exp(self)
97 }
98
99 #[inline]
100 fn sqrt(self) -> Self {
101 f64::sqrt(self)
102 }
103
104 #[inline]
105 fn abs(self) -> Self {
106 f64::abs(self)
107 }
108
109 #[inline]
110 fn mul_add(self, a: Self, b: Self) -> Self {
111 f64::mul_add(self, a, b)
112 }
113
114 #[inline]
115 fn from_f64(val: f64) -> Self {
116 val
117 }
118
119 #[inline]
120 fn to_f64(self) -> f64 {
121 self
122 }
123
124 #[inline]
125 fn pi() -> Self {
126 core::f64::consts::PI
127 }
128
129 #[inline]
130 fn is_nan(self) -> bool {
131 f64::is_nan(self)
132 }
133
134 #[inline]
135 fn is_infinite(self) -> bool {
136 f64::is_infinite(self)
137 }
138}
139
140impl Float for f32 {
141 #[inline]
142 fn zero() -> Self {
143 0.0_f32
144 }
145
146 #[inline]
147 fn one() -> Self {
148 1.0_f32
149 }
150
151 #[inline]
152 fn ln(self) -> Self {
153 f32::ln(self)
154 }
155
156 #[inline]
157 fn exp(self) -> Self {
158 f32::exp(self)
159 }
160
161 #[inline]
162 fn sqrt(self) -> Self {
163 f32::sqrt(self)
164 }
165
166 #[inline]
167 fn abs(self) -> Self {
168 f32::abs(self)
169 }
170
171 #[inline]
172 fn mul_add(self, a: Self, b: Self) -> Self {
173 f32::mul_add(self, a, b)
174 }
175
176 #[inline]
177 #[allow(clippy::cast_possible_truncation)] fn from_f64(val: f64) -> Self {
179 val as f32
180 }
181
182 #[inline]
183 fn to_f64(self) -> f64 {
184 f64::from(self)
185 }
186
187 #[inline]
188 fn pi() -> Self {
189 core::f32::consts::PI
190 }
191
192 #[inline]
193 fn is_nan(self) -> bool {
194 f32::is_nan(self)
195 }
196
197 #[inline]
198 fn is_infinite(self) -> bool {
199 f32::is_infinite(self)
200 }
201}
202
203#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
216pub enum OptionType {
217 Call,
219 Put,
221}
222
223#[derive(Debug, Clone, Copy)]
256pub struct OptionParams<F: Float> {
257 pub option_type: OptionType,
259 pub spot: F,
261 pub strike: F,
263 pub rate: F,
265 pub div_yield: F,
267 pub vol: F,
269 pub time: F,
271}
272
273#[derive(Debug, Clone, Copy)]
302pub struct Greeks<F: Float> {
303 pub delta: F,
305 pub gamma: F,
307 pub theta: F,
310 pub vega: F,
312 pub rho: F,
314 pub epsilon: F,
316 pub lambda: F,
319 pub vanna: F,
322 pub charm: F,
325 pub veta: F,
328 pub vomma: F,
331 pub speed: F,
333 pub zomma: F,
335 pub color: F,
338 pub ultima: F,
340 pub dual_delta: F,
342 pub dual_gamma: F,
344}
345
346pub trait Pricing {
369 fn price(&self) -> Result<f64, PricingError>;
375}
376
377pub trait GreeksCalc {
396 fn greeks(&self) -> Result<Greeks<f64>, PricingError>;
402}
403
404pub trait ImpliedVol {
424 fn implied_vol(&self, market_price: f64, solver: IvSolver) -> Result<f64, IvError>;
431}
432
433impl Pricing for OptionParams<f64> {
436 #[inline]
437 fn price(&self) -> Result<f64, PricingError> {
438 crate::models::black_scholes::price(self)
439 }
440}
441
442impl GreeksCalc for OptionParams<f64> {
443 #[inline]
444 fn greeks(&self) -> Result<Greeks<f64>, PricingError> {
445 crate::greeks::compute_greeks(self)
446 }
447}
448
449impl ImpliedVol for OptionParams<f64> {
450 #[inline]
451 fn implied_vol(&self, market_price: f64, solver: IvSolver) -> Result<f64, IvError> {
452 crate::iv::implied_vol(self, market_price, solver)
453 }
454}
455
456impl Pricing for Black76Params {
457 #[inline]
458 fn price(&self) -> Result<f64, PricingError> {
459 crate::models::black76::price(self)
460 }
461}
462
463impl Pricing for BachelierParams {
464 #[inline]
465 fn price(&self) -> Result<f64, PricingError> {
466 crate::models::bachelier::price(self)
467 }
468}
469
470impl Pricing for DisplacedParams {
471 #[inline]
472 fn price(&self) -> Result<f64, PricingError> {
473 crate::models::displaced::price(self)
474 }
475}
476
477#[derive(Debug, Clone, Copy)]
500pub enum Model {
501 BlackScholes(OptionParams<f64>),
503 Black76(Black76Params),
505 Bachelier(BachelierParams),
507 Displaced(DisplacedParams),
509}
510
511impl Pricing for Model {
512 #[inline]
513 fn price(&self) -> Result<f64, PricingError> {
514 match self {
515 Self::BlackScholes(p) => p.price(),
516 Self::Black76(p) => p.price(),
517 Self::Bachelier(p) => p.price(),
518 Self::Displaced(p) => p.price(),
519 }
520 }
521}
522
523#[cfg(test)]
524#[allow(clippy::float_cmp)]
528mod tests {
529 use super::*;
530
531 #[test]
532 fn test_option_type_clone_copy() {
533 let call = OptionType::Call;
534 let call2 = call;
535 assert_eq!(call, call2);
536
537 let put = OptionType::Put;
538 assert_ne!(call, put);
539 }
540
541 #[test]
542 fn test_option_type_debug() {
543 let call = OptionType::Call;
544 let debug_str = format!("{call:?}");
545 assert_eq!(debug_str, "Call");
546 }
547
548 #[test]
549 fn test_option_params_f64_construction() {
550 let params = OptionParams {
551 option_type: OptionType::Call,
552 spot: 100.0_f64,
553 strike: 100.0_f64,
554 rate: 0.05_f64,
555 div_yield: 0.02_f64,
556 vol: 0.20_f64,
557 time: 1.0_f64,
558 };
559 assert_eq!(params.spot.to_f64(), 100.0_f64);
560 assert_eq!(params.vol.to_f64(), 0.20_f64);
561 }
562
563 #[test]
564 fn test_option_params_f32_construction() {
565 let params = OptionParams {
566 option_type: OptionType::Put,
567 spot: 100.0_f32,
568 strike: 110.0_f32,
569 rate: 0.05_f32,
570 div_yield: 0.02_f32,
571 vol: 0.20_f32,
572 time: 1.0_f32,
573 };
574 assert!((params.spot.to_f64() - 100.0_f64).abs() < 1e-5_f64);
575 }
576
577 #[test]
578 fn test_option_params_copy() {
579 let p1 = OptionParams {
580 option_type: OptionType::Call,
581 spot: 100.0_f64,
582 strike: 100.0_f64,
583 rate: 0.05_f64,
584 div_yield: 0.02_f64,
585 vol: 0.20_f64,
586 time: 1.0_f64,
587 };
588 let p2 = p1;
589 assert_eq!(p1.spot.to_f64(), p2.spot.to_f64());
590 }
591
592 #[test]
593 fn test_greeks_f64_construction() {
594 let g = Greeks {
595 delta: 0.5987_f64,
596 gamma: 0.0185_f64,
597 theta: -0.0152_f64,
598 vega: 0.3702_f64,
599 rho: 0.4174_f64,
600 epsilon: -0.5702_f64,
601 lambda: 6.47_f64,
602 vanna: -0.1314_f64,
603 charm: -0.0265_f64,
604 veta: 0.0_f64,
605 vomma: 0.1499_f64,
606 speed: -0.0006_f64,
607 zomma: -0.0009_f64,
608 color: 0.0_f64,
609 ultima: 0.0_f64,
610 dual_delta: -0.4741_f64,
611 dual_gamma: 0.0_f64,
612 };
613 assert!(g.delta > 0.0_f64);
614 assert!(g.gamma > 0.0_f64);
615 assert!(g.theta < 0.0_f64);
616 }
617
618 #[test]
619 fn test_greeks_copy() {
620 let g1 = Greeks {
621 delta: 0.5_f64,
622 gamma: 0.01_f64,
623 theta: -0.01_f64,
624 vega: 0.3_f64,
625 rho: 0.4_f64,
626 epsilon: -0.5_f64,
627 lambda: 6.0_f64,
628 vanna: -0.1_f64,
629 charm: -0.02_f64,
630 veta: 0.0_f64,
631 vomma: 0.1_f64,
632 speed: -0.0006_f64,
633 zomma: -0.0009_f64,
634 color: 0.0_f64,
635 ultima: 0.0_f64,
636 dual_delta: -0.4_f64,
637 dual_gamma: 0.0_f64,
638 };
639 let g2 = g1;
640 assert_eq!(g1.delta.to_f64(), g2.delta.to_f64());
641 }
642
643 #[test]
644 fn test_float_f64_zero_one() {
645 assert_eq!(f64::zero(), 0.0_f64);
646 assert_eq!(f64::one(), 1.0_f64);
647 }
648
649 #[test]
650 fn test_float_f64_ln_exp() {
651 let val = 2.0_f64;
652 let result = val.ln().exp();
653 assert!((result - 2.0_f64).abs() < 1e-15_f64);
654 }
655
656 #[test]
657 fn test_float_f64_sqrt() {
658 let val = 4.0_f64;
659 assert!((val.sqrt() - 2.0_f64).abs() < 1e-15_f64);
660 }
661
662 #[test]
663 fn test_float_f64_abs() {
664 assert_eq!((-3.0_f64).abs(), 3.0_f64);
665 assert_eq!(3.0_f64.abs(), 3.0_f64);
666 }
667
668 #[test]
669 fn test_float_f64_mul_add() {
670 let result = 2.0_f64.mul_add(3.0_f64, 4.0_f64);
672 assert!((result - 10.0_f64).abs() < 1e-15_f64);
673 }
674
675 #[test]
676 fn test_float_f64_from_f64() {
677 let val = f64::from_f64(3.25_f64);
678 assert!((val - 3.25_f64).abs() < 1e-15_f64);
679 }
680
681 #[test]
682 fn test_float_f32_roundtrip() {
683 let val = f32::from_f64(3.25_f64);
684 let back = val.to_f64();
685 assert!((back - 3.25_f64).abs() < 1e-5_f64);
686 }
687
688 #[test]
689 fn test_float_f64_pi() {
690 assert!((f64::pi() - core::f64::consts::PI).abs() < 1e-15_f64);
691 }
692
693 #[test]
694 fn test_float_f64_is_nan() {
695 assert!(f64::NAN.is_nan());
696 assert!(!1.0_f64.is_nan());
697 }
698
699 #[test]
700 fn test_float_f64_is_infinite() {
701 assert!(f64::INFINITY.is_infinite());
702 assert!(!1.0_f64.is_infinite());
703 }
704
705 #[test]
706 fn test_float_f32_basic_ops() {
707 let a = 2.0_f32;
708 let b = 3.0_f32;
709 assert!((a + b - 5.0_f32).abs() < 1e-6_f32);
710 assert!((a * b - 6.0_f32).abs() < 1e-6_f32);
711 assert!((b - a - 1.0_f32).abs() < 1e-6_f32);
712 assert!((b / a - 1.5_f32).abs() < 1e-6_f32);
713 assert!((-a + 2.0_f32).abs() < 1e-6_f32);
714 }
715
716 #[test]
719 fn test_pricing_trait_bs_call() {
720 let params = OptionParams {
721 option_type: OptionType::Call,
722 spot: 100.0_f64,
723 strike: 100.0_f64,
724 rate: 0.05_f64,
725 div_yield: 0.02_f64,
726 vol: 0.20_f64,
727 time: 1.0_f64,
728 };
729 let p = params.price().unwrap();
730 assert!(
731 (p - 9.2270_f64).abs() < 1e-4_f64,
732 "BS call via trait: got {p}"
733 );
734 }
735
736 #[test]
737 fn test_pricing_trait_black76() {
738 let params = Black76Params {
739 option_type: OptionType::Call,
740 forward: 100.0_f64,
741 strike: 100.0_f64,
742 rate: 0.05_f64,
743 vol: 0.20_f64,
744 time: 1.0_f64,
745 };
746 let p = params.price().unwrap();
747 assert!(p > 0.0_f64, "Black76 via trait: got {p}");
748 }
749
750 #[test]
751 fn test_pricing_trait_bachelier() {
752 let params = BachelierParams {
753 option_type: OptionType::Call,
754 forward: 100.0_f64,
755 strike: 100.0_f64,
756 rate: 0.05_f64,
757 normal_vol: 5.0_f64,
758 time: 1.0_f64,
759 };
760 let p = params.price().unwrap();
761 assert!(p > 0.0_f64, "Bachelier via trait: got {p}");
762 }
763
764 #[test]
765 fn test_pricing_trait_displaced() {
766 let params = DisplacedParams {
767 option_type: OptionType::Call,
768 forward: 100.0_f64,
769 strike: 100.0_f64,
770 rate: 0.05_f64,
771 vol: 0.20_f64,
772 time: 1.0_f64,
773 displacement: 50.0_f64,
774 };
775 let p = params.price().unwrap();
776 assert!(p > 0.0_f64, "Displaced via trait: got {p}");
777 }
778
779 #[test]
782 fn test_greeks_calc_trait_call() {
783 let params = OptionParams {
784 option_type: OptionType::Call,
785 spot: 100.0_f64,
786 strike: 100.0_f64,
787 rate: 0.05_f64,
788 div_yield: 0.02_f64,
789 vol: 0.20_f64,
790 time: 1.0_f64,
791 };
792 let g = params.greeks().unwrap();
793 assert!(g.delta > 0.0_f64 && g.delta < 1.0_f64);
794 assert!(g.gamma > 0.0_f64);
795 assert!(g.vega > 0.0_f64);
796 }
797
798 #[test]
801 fn test_implied_vol_trait_roundtrip() {
802 let params = OptionParams {
803 option_type: OptionType::Call,
804 spot: 100.0_f64,
805 strike: 100.0_f64,
806 rate: 0.05_f64,
807 div_yield: 0.02_f64,
808 vol: 0.20_f64,
809 time: 1.0_f64,
810 };
811 let market_price = params.price().unwrap();
812 let iv_params = OptionParams {
813 vol: 0.0_f64,
814 ..params
815 };
816 let iv = iv_params.implied_vol(market_price, IvSolver::Auto).unwrap();
817 assert!(
818 (iv - 0.20_f64).abs() < 1e-6_f64,
819 "IV roundtrip: expected ~0.20, got {iv}"
820 );
821 }
822
823 #[test]
826 fn test_model_enum_bs() {
827 let m = Model::BlackScholes(OptionParams {
828 option_type: OptionType::Call,
829 spot: 100.0_f64,
830 strike: 100.0_f64,
831 rate: 0.05_f64,
832 div_yield: 0.02_f64,
833 vol: 0.20_f64,
834 time: 1.0_f64,
835 });
836 let p = m.price().unwrap();
837 assert!((p - 9.2270_f64).abs() < 1e-4_f64, "Model::BS: got {p}");
838 }
839
840 #[test]
841 fn test_model_enum_black76() {
842 let m = Model::Black76(Black76Params {
843 option_type: OptionType::Call,
844 forward: 100.0_f64,
845 strike: 100.0_f64,
846 rate: 0.05_f64,
847 vol: 0.20_f64,
848 time: 1.0_f64,
849 });
850 let p = m.price().unwrap();
851 assert!(p > 0.0_f64, "Model::Black76: got {p}");
852 }
853
854 #[test]
855 fn test_model_enum_bachelier() {
856 let m = Model::Bachelier(BachelierParams {
857 option_type: OptionType::Call,
858 forward: 100.0_f64,
859 strike: 100.0_f64,
860 rate: 0.05_f64,
861 normal_vol: 5.0_f64,
862 time: 1.0_f64,
863 });
864 let p = m.price().unwrap();
865 assert!(p > 0.0_f64, "Model::Bachelier: got {p}");
866 }
867
868 #[test]
869 fn test_model_enum_displaced() {
870 let m = Model::Displaced(DisplacedParams {
871 option_type: OptionType::Call,
872 forward: 100.0_f64,
873 strike: 100.0_f64,
874 rate: 0.05_f64,
875 vol: 0.20_f64,
876 time: 1.0_f64,
877 displacement: 50.0_f64,
878 });
879 let p = m.price().unwrap();
880 assert!(p > 0.0_f64, "Model::Displaced: got {p}");
881 }
882
883 #[test]
884 fn test_model_enum_matches_direct_call() {
885 let params = OptionParams {
886 option_type: OptionType::Call,
887 spot: 100.0_f64,
888 strike: 100.0_f64,
889 rate: 0.05_f64,
890 div_yield: 0.02_f64,
891 vol: 0.20_f64,
892 time: 1.0_f64,
893 };
894 let direct = params.price().unwrap();
895 let via_model = Model::BlackScholes(params).price().unwrap();
896 assert!(
897 (direct - via_model).abs() < 1e-15_f64,
898 "Model dispatch must match direct call"
899 );
900 }
901}