1use std::{
19 any::Any,
20 fmt::Display,
21 ops::{Add, Deref, Mul},
22 sync::Arc,
23};
24
25use implied_vol::{DefaultSpecialFn, ImpliedBlackVolatility, SpecialFn};
26use nautilus_core::{
27 UnixNanos,
28 correctness::{CorrectnessResult, check_equal_usize, check_predicate_true},
29 datetime::unix_nanos_to_iso8601,
30 math::{quadratic_interpolation, too_close_for_interpolation},
31};
32use serde::{Deserialize, Serialize};
33
34use crate::{
35 data::{
36 CustomDataTrait, HasTsInit,
37 black_scholes::{
38 REFINEMENT_VOL_MAX, REFINEMENT_VOL_MIN, compute_greeks, compute_iv_and_greeks,
39 },
40 },
41 identifiers::InstrumentId,
42};
43
44const FRAC_SQRT_2_PI: f64 = f64::from_bits(0x3fd9_8845_33d4_3651);
45const THETA_DAILY_FACTOR: f64 = 1.0 / 365.25;
47const VEGA_PERCENT_FACTOR: f64 = 0.01;
49const RHO_PERCENT_FACTOR: f64 = 0.01;
50const PRICE_RELATIVE_TOLERANCE: f64 = 1e-4;
51
52#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
54pub enum OptionValuationError {
55 #[error("Invalid option valuation input: {0}")]
56 InvalidInput(&'static str),
57 #[error("No finite positive implied volatility for the option price")]
58 ImpliedVolatilityUnavailable,
59 #[error("Nonfinite or inconsistent option valuation result")]
60 InvalidResult,
61 #[error("Option {0} has expired")]
62 Expired(InstrumentId),
63 #[error("Local valuation of option spread {0} is unsupported")]
64 UnsupportedSpread(InstrumentId),
65}
66
67#[repr(C)]
70#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Default, Serialize, Deserialize)]
71#[cfg_attr(
72 feature = "python",
73 pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
74)]
75#[cfg_attr(
76 feature = "python",
77 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
78)]
79pub struct OptionGreekValues {
80 pub delta: f64,
81 pub gamma: f64,
82 pub vega: f64,
83 pub theta: f64,
84 pub rho: f64,
85}
86
87impl Add for OptionGreekValues {
88 type Output = Self;
89
90 fn add(self, rhs: Self) -> Self {
91 Self {
92 delta: self.delta + rhs.delta,
93 gamma: self.gamma + rhs.gamma,
94 vega: self.vega + rhs.vega,
95 theta: self.theta + rhs.theta,
96 rho: self.rho + rhs.rho,
97 }
98 }
99}
100
101impl Mul<f64> for OptionGreekValues {
102 type Output = Self;
103
104 fn mul(self, scalar: f64) -> Self {
105 Self {
106 delta: self.delta * scalar,
107 gamma: self.gamma * scalar,
108 vega: self.vega * scalar,
109 theta: self.theta * scalar,
110 rho: self.rho * scalar,
111 }
112 }
113}
114
115impl Mul<OptionGreekValues> for f64 {
116 type Output = OptionGreekValues;
117
118 fn mul(self, greeks: OptionGreekValues) -> OptionGreekValues {
119 greeks * self
120 }
121}
122
123impl Display for OptionGreekValues {
124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125 write!(
126 f,
127 "OptionGreekValues(delta={:.4}, gamma={:.4}, vega={:.4}, theta={:.4}, rho={:.4})",
128 self.delta, self.gamma, self.vega, self.theta, self.rho
129 )
130 }
131}
132
133pub trait HasGreeks {
135 fn greeks(&self) -> OptionGreekValues;
136}
137
138#[inline(always)]
139fn norm_pdf(x: f64) -> f64 {
140 FRAC_SQRT_2_PI * (-0.5 * x * x).exp()
141}
142
143#[repr(C)]
149#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
150#[cfg_attr(
151 feature = "python",
152 pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
153)]
154#[cfg_attr(
155 feature = "python",
156 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
157)]
158pub struct BlackScholesGreeksResult {
159 pub price: f64,
160 pub vol: f64,
161 pub delta: f64,
162 pub gamma: f64,
163 pub vega: f64,
164 pub theta: f64,
165 pub itm_prob: f64,
166 pub rho: f64,
167}
168
169#[must_use]
173pub fn black_scholes_greeks_exact(
174 s: f64,
175 r: f64,
176 b: f64,
177 vol: f64,
178 is_call: bool,
179 k: f64,
180 t: f64,
181) -> BlackScholesGreeksResult {
182 let phi = if is_call { 1.0 } else { -1.0 };
183 let sqrt_t = t.sqrt();
184 let scaled_vol = vol * sqrt_t;
185
186 let d1 = ((s / k).ln() + (b + 0.5 * vol.powi(2)) * t) / scaled_vol;
188 let d2 = d1 - scaled_vol;
189
190 let cdf_phi_d1 = DefaultSpecialFn::norm_cdf(phi * d1);
192 let cdf_phi_d2 = DefaultSpecialFn::norm_cdf(phi * d2);
193 let pdf_d1 = norm_pdf(d1);
194
195 let df_b = ((b - r) * t).exp();
197 let df_r = (-r * t).exp();
198
199 let price = phi * (s * df_b * cdf_phi_d1 - k * df_r * cdf_phi_d2);
201 let delta = phi * df_b * cdf_phi_d1;
202 let gamma = (df_b * pdf_d1) / (s * scaled_vol);
203 let vega = s * df_b * sqrt_t * pdf_d1 * VEGA_PERCENT_FACTOR;
204
205 let theta_v = -(s * df_b * pdf_d1 * vol) / (2.0 * sqrt_t);
207 let theta_b = -phi * (b - r) * s * df_b * cdf_phi_d1;
208 let theta_r = -phi * r * k * df_r * cdf_phi_d2;
209 let theta = (theta_v + theta_b + theta_r) * THETA_DAILY_FACTOR;
210
211 BlackScholesGreeksResult {
212 price,
213 vol,
214 delta,
215 gamma,
216 vega,
217 theta,
218 itm_prob: cdf_phi_d2,
219 rho: -t * price * RHO_PERCENT_FACTOR,
220 }
221}
222
223#[must_use]
224pub fn imply_vol(s: f64, r: f64, b: f64, is_call: bool, k: f64, t: f64, price: f64) -> f64 {
225 let forward = s * (b * t).exp();
226 let forward_price = price * (r * t).exp();
227
228 ImpliedBlackVolatility::builder()
229 .option_price(forward_price)
230 .forward(forward)
231 .strike(k)
232 .expiry(t)
233 .is_call(is_call)
234 .build()
235 .and_then(|iv| iv.calculate::<DefaultSpecialFn>())
236 .unwrap_or(0.0)
237}
238
239#[must_use]
242pub fn black_scholes_greeks(
243 s: f64,
244 r: f64,
245 b: f64,
246 vol: f64,
247 is_call: bool,
248 k: f64,
249 t: f64,
250) -> BlackScholesGreeksResult {
251 let greeks = compute_greeks::<f32>(
253 s as f32, k as f32, t as f32, r as f32, b as f32, vol as f32, is_call,
254 );
255
256 BlackScholesGreeksResult {
257 price: f64::from(greeks.price),
258 vol,
259 delta: f64::from(greeks.delta),
260 gamma: f64::from(greeks.gamma),
261 vega: f64::from(greeks.vega) * VEGA_PERCENT_FACTOR,
262 theta: f64::from(greeks.theta) * THETA_DAILY_FACTOR,
263 itm_prob: f64::from(greeks.itm_prob),
264 rho: -t * f64::from(greeks.price) * RHO_PERCENT_FACTOR,
265 }
266}
267
268#[must_use]
271pub fn imply_vol_and_greeks(
272 s: f64,
273 r: f64,
274 b: f64,
275 is_call: bool,
276 k: f64,
277 t: f64,
278 price: f64,
279) -> BlackScholesGreeksResult {
280 let vol = imply_vol(s, r, b, is_call, k, t, price);
281 let safe_vol = if vol < 1e-8 { 1e-8 } else { vol };
285 black_scholes_greeks(s, r, b, safe_vol, is_call, k, t)
286}
287
288#[expect(clippy::too_many_arguments)]
292#[must_use]
293pub fn refine_vol_and_greeks(
294 s: f64,
295 r: f64,
296 b: f64,
297 is_call: bool,
298 k: f64,
299 t: f64,
300 target_price: f64,
301 initial_vol: f64,
302) -> BlackScholesGreeksResult {
303 let greeks = compute_iv_and_greeks::<f32>(
305 target_price as f32,
306 s as f32,
307 k as f32,
308 t as f32,
309 r as f32,
310 b as f32,
311 is_call,
312 initial_vol as f32,
313 );
314
315 BlackScholesGreeksResult {
316 price: f64::from(greeks.price),
317 vol: f64::from(greeks.vol),
318 delta: f64::from(greeks.delta),
319 gamma: f64::from(greeks.gamma),
320 vega: f64::from(greeks.vega) * VEGA_PERCENT_FACTOR,
321 theta: f64::from(greeks.theta) * THETA_DAILY_FACTOR,
322 itm_prob: f64::from(greeks.itm_prob),
323 rho: -t * f64::from(greeks.price) * RHO_PERCENT_FACTOR,
324 }
325}
326
327#[expect(clippy::too_many_arguments)]
339pub fn black_scholes_greeks_checked(
340 s: f64,
341 r: f64,
342 b: f64,
343 vol: f64,
344 is_call: bool,
345 k: f64,
346 t: f64,
347 db_dr: f64,
348) -> Result<BlackScholesGreeksResult, OptionValuationError> {
349 check_option_inputs(s, r, b, vol, k, t)?;
350 let result = black_scholes_greeks(s, r, b, vol, is_call, k, t);
351 match apply_rate_sensitivity(result, s, r, is_call, k, t, db_dr) {
352 Err(OptionValuationError::InvalidResult) => {
353 let result = black_scholes_greeks_exact(s, r, b, vol, is_call, k, t);
354 apply_rate_sensitivity(result, s, r, is_call, k, t, db_dr)
355 }
356 result => result,
357 }
358}
359
360#[expect(clippy::too_many_arguments)]
374pub fn imply_vol_and_greeks_checked(
375 s: f64,
376 r: f64,
377 b: f64,
378 is_call: bool,
379 k: f64,
380 t: f64,
381 price: f64,
382 db_dr: f64,
383 initial_vol: Option<f64>,
384) -> Result<BlackScholesGreeksResult, OptionValuationError> {
385 check_option_inputs(s, r, b, 1.0, k, t)?;
386
387 if !price.is_finite() || price <= 0.0 {
388 return Err(OptionValuationError::InvalidInput("option price"));
389 }
390
391 let forward = s * (b * t).exp();
392 let forward_price = price * (r * t).exp();
393
394 let intrinsic = if is_call {
395 (forward - k).max(0.0)
396 } else {
397 (k - forward).max(0.0)
398 };
399
400 let upper = if is_call { forward } else { k };
401 if !forward.is_finite()
402 || !forward_price.is_finite()
403 || forward_price <= intrinsic
404 || forward_price >= upper
405 {
406 return Err(OptionValuationError::ImpliedVolatilityUnavailable);
407 }
408
409 let price_tolerance = price * PRICE_RELATIVE_TOLERANCE;
410
411 if let Some(seed) = initial_vol {
412 check_option_inputs(s, r, b, seed, k, t)?;
413 let result = refine_vol_and_greeks(s, r, b, is_call, k, t, price, seed);
414 if result.vol > REFINEMENT_VOL_MIN
415 && result.vol < REFINEMENT_VOL_MAX
416 && (result.price - price).abs() <= price_tolerance
417 {
418 return apply_rate_sensitivity(result, s, r, is_call, k, t, db_dr);
419 }
420 }
421
422 let vol = imply_vol_checked(s, r, b, is_call, k, t, price)?;
423 let mut result = black_scholes_greeks_checked(s, r, b, vol, is_call, k, t, db_dr)?;
424 if (result.price - price).abs() > price_tolerance {
425 result = apply_rate_sensitivity(
426 black_scholes_greeks_exact(s, r, b, vol, is_call, k, t),
427 s,
428 r,
429 is_call,
430 k,
431 t,
432 db_dr,
433 )?;
434 }
435
436 if (result.price - price).abs() > price_tolerance {
437 return Err(OptionValuationError::InvalidResult);
438 }
439
440 Ok(result)
441}
442
443pub(crate) fn imply_vol_checked(
451 s: f64,
452 r: f64,
453 b: f64,
454 is_call: bool,
455 k: f64,
456 t: f64,
457 price: f64,
458) -> Result<f64, OptionValuationError> {
459 let vol = imply_vol(s, r, b, is_call, k, t, price);
460 if !vol.is_finite() || vol <= 0.0 {
461 return Err(OptionValuationError::ImpliedVolatilityUnavailable);
462 }
463
464 Ok(vol)
465}
466
467pub fn check_option_inputs(
475 s: f64,
476 r: f64,
477 b: f64,
478 vol: f64,
479 k: f64,
480 t: f64,
481) -> Result<(), OptionValuationError> {
482 for (name, value) in [
483 ("underlying price", s),
484 ("strike", k),
485 ("time to expiry", t),
486 ("volatility", vol),
487 ] {
488 let kernel_value = value as f32;
489 if !value.is_finite() || value <= 0.0 || !kernel_value.is_finite() || kernel_value <= 0.0 {
490 return Err(OptionValuationError::InvalidInput(name));
491 }
492 }
493
494 for (name, value) in [("interest rate", r), ("cost of carry", b)] {
495 if !(value as f32).is_finite() {
496 return Err(OptionValuationError::InvalidInput(name));
497 }
498 }
499
500 Ok(())
501}
502
503fn apply_rate_sensitivity(
504 mut result: BlackScholesGreeksResult,
505 s: f64,
506 r: f64,
507 is_call: bool,
508 k: f64,
509 t: f64,
510 db_dr: f64,
511) -> Result<BlackScholesGreeksResult, OptionValuationError> {
512 if !db_dr.is_finite() {
513 return Err(OptionValuationError::InvalidInput("carry rate sensitivity"));
514 }
515
516 if db_dr != 0.0 {
517 let phi = if is_call { 1.0 } else { -1.0 };
518 result.rho = t
520 * ((db_dr - 1.0) * s * result.delta + phi * k * (-r * t).exp() * result.itm_prob)
521 * RHO_PERCENT_FACTOR;
522 }
523
524 check_valuation_result(result)
525}
526
527fn check_valuation_result(
528 result: BlackScholesGreeksResult,
529) -> Result<BlackScholesGreeksResult, OptionValuationError> {
530 if [
531 result.price,
532 result.vol,
533 result.delta,
534 result.gamma,
535 result.vega,
536 result.theta,
537 result.rho,
538 result.itm_prob,
539 ]
540 .iter()
541 .any(|value| !value.is_finite())
542 || result.price < 0.0
543 || result.gamma < 0.0
544 || result.vega < 0.0
545 || !(0.0..=1.0).contains(&result.itm_prob)
546 {
547 return Err(OptionValuationError::InvalidResult);
548 }
549
550 Ok(result)
551}
552
553#[repr(C)]
554#[derive(Debug, Clone)]
555#[cfg_attr(
556 feature = "python",
557 pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
558)]
559#[cfg_attr(
560 feature = "python",
561 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
562)]
563pub struct GreeksData {
564 pub instrument_id: InstrumentId,
565 pub is_call: bool,
566 pub strike: f64,
567 pub expiry: i32,
568 pub expiry_in_days: i32,
569 pub expiry_in_years: f64,
570 pub multiplier: f64,
571 pub quantity: f64,
572 pub underlying_price: f64,
573 pub interest_rate: f64,
574 pub cost_of_carry: f64,
575 pub vol: f64,
576 pub pnl: f64,
577 pub price: f64,
578 pub greeks: OptionGreekValues,
580 pub itm_prob: f64,
582 pub ts_event: UnixNanos,
583 pub ts_init: UnixNanos,
584}
585
586impl GreeksData {
587 #[expect(clippy::too_many_arguments)]
588 #[must_use]
589 pub fn new(
590 instrument_id: InstrumentId,
591 is_call: bool,
592 strike: f64,
593 expiry: i32,
594 expiry_in_days: i32,
595 expiry_in_years: f64,
596 multiplier: f64,
597 quantity: f64,
598 underlying_price: f64,
599 interest_rate: f64,
600 cost_of_carry: f64,
601 vol: f64,
602 pnl: f64,
603 price: f64,
604 greeks: OptionGreekValues,
605 itm_prob: f64,
606 ts_event: UnixNanos,
607 ts_init: UnixNanos,
608 ) -> Self {
609 Self {
610 instrument_id,
611 is_call,
612 strike,
613 expiry,
614 expiry_in_days,
615 expiry_in_years,
616 multiplier,
617 quantity,
618 underlying_price,
619 interest_rate,
620 cost_of_carry,
621 vol,
622 pnl,
623 price,
624 greeks,
625 itm_prob,
626 ts_event,
627 ts_init,
628 }
629 }
630
631 #[must_use]
632 pub fn from_delta(
633 instrument_id: InstrumentId,
634 delta: f64,
635 multiplier: f64,
636 ts_event: UnixNanos,
637 ) -> Self {
638 Self {
639 instrument_id,
640 is_call: true,
641 strike: 0.0,
642 expiry: 0,
643 expiry_in_days: 0,
644 expiry_in_years: 0.0,
645 multiplier,
646 quantity: 1.0,
647 underlying_price: 0.0,
648 interest_rate: 0.0,
649 cost_of_carry: 0.0,
650 vol: 0.0,
651 pnl: 0.0,
652 price: 0.0,
653 greeks: OptionGreekValues {
654 delta,
655 ..Default::default()
656 },
657 itm_prob: 0.0,
658 ts_event,
659 ts_init: ts_event,
660 }
661 }
662
663 #[must_use]
667 pub fn to_portfolio_greeks(&self) -> PortfolioGreeks {
668 PortfolioGreeks {
669 pnl: self.pnl * self.multiplier,
670 price: self.price * self.multiplier,
671 greeks: self.greeks * self.multiplier,
672 ts_event: self.ts_event,
673 ts_init: self.ts_init,
674 }
675 }
676}
677
678impl Deref for GreeksData {
679 type Target = OptionGreekValues;
680 fn deref(&self) -> &Self::Target {
681 &self.greeks
682 }
683}
684
685impl HasGreeks for GreeksData {
686 fn greeks(&self) -> OptionGreekValues {
687 self.greeks
688 }
689}
690
691impl Default for GreeksData {
692 fn default() -> Self {
693 Self {
694 instrument_id: InstrumentId::from("ES.GLBX"),
695 is_call: true,
696 strike: 0.0,
697 expiry: 0,
698 expiry_in_days: 0,
699 expiry_in_years: 0.0,
700 multiplier: 0.0,
701 quantity: 0.0,
702 underlying_price: 0.0,
703 interest_rate: 0.0,
704 cost_of_carry: 0.0,
705 vol: 0.0,
706 pnl: 0.0,
707 price: 0.0,
708 greeks: OptionGreekValues::default(),
709 itm_prob: 0.0,
710 ts_event: UnixNanos::default(),
711 ts_init: UnixNanos::default(),
712 }
713 }
714}
715
716impl Display for GreeksData {
717 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
718 write!(
719 f,
720 "GreeksData(instrument_id={}, expiry={}, itm_prob={:.2}%, vol={:.2}%, pnl={:.2}, price={:.2}, delta={:.2}, gamma={:.2}, vega={:.2}, theta={:.2}, quantity={}, ts_event={}, ts_init={})",
721 self.instrument_id,
722 self.expiry,
723 self.itm_prob * 100.0,
724 self.vol * 100.0,
725 self.pnl,
726 self.price,
727 self.greeks.delta,
728 self.greeks.gamma,
729 self.greeks.vega,
730 self.greeks.theta,
731 self.quantity,
732 unix_nanos_to_iso8601(self.ts_event),
733 unix_nanos_to_iso8601(self.ts_init)
734 )
735 }
736}
737
738impl Mul<&GreeksData> for f64 {
740 type Output = GreeksData;
741
742 fn mul(self, g: &GreeksData) -> GreeksData {
743 GreeksData {
744 instrument_id: g.instrument_id,
745 is_call: g.is_call,
746 strike: g.strike,
747 expiry: g.expiry,
748 expiry_in_days: g.expiry_in_days,
749 expiry_in_years: g.expiry_in_years,
750 multiplier: g.multiplier,
751 quantity: g.quantity,
752 underlying_price: g.underlying_price,
753 interest_rate: g.interest_rate,
754 cost_of_carry: g.cost_of_carry,
755 vol: g.vol,
756 pnl: self * g.pnl,
757 price: self * g.price,
758 greeks: g.greeks * self,
759 itm_prob: g.itm_prob,
760 ts_event: g.ts_event,
761 ts_init: g.ts_init,
762 }
763 }
764}
765
766impl HasTsInit for GreeksData {
767 fn ts_init(&self) -> UnixNanos {
768 self.ts_init
769 }
770}
771
772#[derive(Debug, Clone)]
773#[cfg_attr(
774 feature = "python",
775 pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
776)]
777#[cfg_attr(
778 feature = "python",
779 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
780)]
781pub struct PortfolioGreeks {
782 pub pnl: f64,
783 pub price: f64,
784 pub greeks: OptionGreekValues,
785 pub ts_event: UnixNanos,
786 pub ts_init: UnixNanos,
787}
788
789impl PortfolioGreeks {
790 #[expect(clippy::too_many_arguments)]
791 #[must_use]
792 pub fn new(
793 pnl: f64,
794 price: f64,
795 delta: f64,
796 gamma: f64,
797 vega: f64,
798 theta: f64,
799 ts_event: UnixNanos,
800 ts_init: UnixNanos,
801 ) -> Self {
802 Self {
803 pnl,
804 price,
805 greeks: OptionGreekValues {
806 delta,
807 gamma,
808 vega,
809 theta,
810 rho: 0.0,
811 },
812 ts_event,
813 ts_init,
814 }
815 }
816}
817
818impl Deref for PortfolioGreeks {
819 type Target = OptionGreekValues;
820 fn deref(&self) -> &Self::Target {
821 &self.greeks
822 }
823}
824
825impl Default for PortfolioGreeks {
826 fn default() -> Self {
827 Self {
828 pnl: 0.0,
829 price: 0.0,
830 greeks: OptionGreekValues::default(),
831 ts_event: UnixNanos::default(),
832 ts_init: UnixNanos::default(),
833 }
834 }
835}
836
837impl Display for PortfolioGreeks {
838 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
839 write!(
840 f,
841 "PortfolioGreeks(pnl={:.2}, price={:.2}, delta={:.2}, gamma={:.2}, vega={:.2}, theta={:.2}, ts_event={}, ts_init={})",
842 self.pnl,
843 self.price,
844 self.greeks.delta,
845 self.greeks.gamma,
846 self.greeks.vega,
847 self.greeks.theta,
848 unix_nanos_to_iso8601(self.ts_event),
849 unix_nanos_to_iso8601(self.ts_init)
850 )
851 }
852}
853
854impl Add for PortfolioGreeks {
855 type Output = Self;
856
857 fn add(self, other: Self) -> Self {
858 Self {
859 pnl: self.pnl + other.pnl,
860 price: self.price + other.price,
861 greeks: self.greeks + other.greeks,
862 ts_event: self.ts_event,
863 ts_init: self.ts_init,
864 }
865 }
866}
867
868impl From<GreeksData> for PortfolioGreeks {
869 fn from(g: GreeksData) -> Self {
870 g.to_portfolio_greeks()
871 }
872}
873
874impl Mul<&PortfolioGreeks> for f64 {
875 type Output = PortfolioGreeks;
876
877 fn mul(self, g: &PortfolioGreeks) -> PortfolioGreeks {
878 PortfolioGreeks {
879 pnl: self * g.pnl,
880 price: self * g.price,
881 greeks: g.greeks * self,
882 ts_event: g.ts_event,
883 ts_init: g.ts_init,
884 }
885 }
886}
887
888impl HasTsInit for PortfolioGreeks {
889 fn ts_init(&self) -> UnixNanos {
890 self.ts_init
891 }
892}
893
894impl HasGreeks for PortfolioGreeks {
895 fn greeks(&self) -> OptionGreekValues {
896 self.greeks
897 }
898}
899
900impl HasGreeks for BlackScholesGreeksResult {
901 fn greeks(&self) -> OptionGreekValues {
902 OptionGreekValues {
903 delta: self.delta,
904 gamma: self.gamma,
905 vega: self.vega,
906 theta: self.theta,
907 rho: self.rho,
908 }
909 }
910}
911
912#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
913#[cfg_attr(
914 feature = "python",
915 pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
916)]
917#[cfg_attr(
918 feature = "python",
919 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
920)]
921pub struct YieldCurve {
922 pub curve_name: String,
923 pub tenors: Vec<f64>,
924 pub interest_rates: Vec<f64>,
925 pub ts_event: UnixNanos,
926 pub ts_init: UnixNanos,
927}
928
929impl YieldCurve {
930 #[must_use]
931 pub fn new(
932 curve_name: String,
933 tenors: Vec<f64>,
934 interest_rates: Vec<f64>,
935 ts_event: UnixNanos,
936 ts_init: UnixNanos,
937 ) -> Self {
938 Self {
939 curve_name,
940 tenors,
941 interest_rates,
942 ts_event,
943 ts_init,
944 }
945 }
946
947 #[must_use]
949 pub fn get_rate(&self, expiry_in_years: f64) -> f64 {
950 if self.interest_rates.len() == 1 {
951 return self.interest_rates[0];
952 }
953
954 quadratic_interpolation(expiry_in_years, &self.tenors, &self.interest_rates)
955 }
956
957 pub fn validate(&self) -> CorrectnessResult<()> {
965 check_equal_usize(
966 self.tenors.len(),
967 self.interest_rates.len(),
968 "tenors",
969 "interest_rates",
970 )?;
971
972 check_predicate_true(
974 self.interest_rates.len() == 1 || self.interest_rates.len() >= 3,
975 "`interest_rates` must hold one rate or at least three points",
976 )?;
977 check_predicate_true(
978 self.tenors
979 .iter()
980 .chain(&self.interest_rates)
981 .all(|value| value.is_finite()),
982 "`tenors` and `interest_rates` must be finite",
983 )?;
984 check_predicate_true(
985 self.tenors
986 .windows(2)
987 .all(|pair| pair[0] < pair[1] && !too_close_for_interpolation(pair[0], pair[1])),
988 "`tenors` must be strictly increasing, with each step wide enough to interpolate",
989 )
990 }
991}
992
993impl Display for YieldCurve {
994 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
995 write!(
996 f,
997 "YieldCurve(curve_name={}, ts_event={}, ts_init={})",
998 self.curve_name,
999 unix_nanos_to_iso8601(self.ts_event),
1000 unix_nanos_to_iso8601(self.ts_init)
1001 )
1002 }
1003}
1004
1005impl HasTsInit for YieldCurve {
1006 fn ts_init(&self) -> UnixNanos {
1007 self.ts_init
1008 }
1009}
1010
1011impl CustomDataTrait for YieldCurve {
1012 fn type_name(&self) -> &'static str {
1013 Self::type_name_static()
1014 }
1015
1016 fn as_any(&self) -> &dyn Any {
1017 self
1018 }
1019
1020 fn ts_event(&self) -> UnixNanos {
1021 self.ts_event
1022 }
1023
1024 fn to_json(&self) -> anyhow::Result<String> {
1025 Ok(serde_json::to_string(self)?)
1026 }
1027
1028 fn clone_arc(&self) -> Arc<dyn CustomDataTrait> {
1029 Arc::new(self.clone())
1030 }
1031
1032 fn eq_arc(&self, other: &dyn CustomDataTrait) -> bool {
1033 other.as_any().downcast_ref::<Self>() == Some(self)
1034 }
1035
1036 #[cfg(feature = "python")]
1037 fn to_pyobject(&self, py: pyo3::Python<'_>) -> pyo3::PyResult<pyo3::Py<pyo3::PyAny>> {
1038 crate::data::custom::clone_pyclass_to_pyobject(self, py)
1039 }
1040
1041 fn type_name_static() -> &'static str {
1042 "YieldCurve"
1043 }
1044
1045 fn from_json(value: serde_json::Value) -> anyhow::Result<Arc<dyn CustomDataTrait>> {
1046 let parsed: Self = serde_json::from_value(value)?;
1047 Ok(Arc::new(parsed))
1048 }
1049}
1050
1051impl Default for YieldCurve {
1052 fn default() -> Self {
1053 Self {
1054 curve_name: "USD".to_string(),
1055 tenors: vec![0.5, 1.0, 1.5, 2.0, 2.5],
1056 interest_rates: vec![0.04, 0.04, 0.04, 0.04, 0.04],
1057 ts_event: UnixNanos::default(),
1058 ts_init: UnixNanos::default(),
1059 }
1060 }
1061}
1062
1063#[cfg(test)]
1064mod tests {
1065 use rstest::rstest;
1066
1067 use super::*;
1068 use crate::identifiers::InstrumentId;
1069
1070 fn create_test_greeks_data() -> GreeksData {
1071 GreeksData::new(
1072 InstrumentId::from("SPY240315C00500000.OPRA"),
1073 true,
1074 500.0,
1075 20_240_315,
1076 91, 0.25,
1078 100.0,
1079 1.0,
1080 520.0,
1081 0.05,
1082 0.05,
1083 0.2,
1084 250.0,
1085 25.5,
1086 OptionGreekValues {
1087 delta: 0.65,
1088 gamma: 0.003,
1089 vega: 15.2,
1090 theta: -0.08,
1091 rho: 0.0,
1092 },
1093 0.75,
1094 UnixNanos::from(1_500_000_000),
1095 UnixNanos::from(1_000_000_000),
1096 )
1097 }
1098
1099 fn create_test_portfolio_greeks() -> PortfolioGreeks {
1100 PortfolioGreeks::new(
1101 1500.0,
1102 125.5,
1103 2.15,
1104 0.008,
1105 42.7,
1106 -2.3,
1107 UnixNanos::from(1_500_000_000),
1108 UnixNanos::from(1_000_000_000),
1109 )
1110 }
1111
1112 fn create_test_yield_curve() -> YieldCurve {
1113 YieldCurve::new(
1114 "USD".to_string(),
1115 vec![0.25, 0.5, 1.0, 2.0, 5.0],
1116 vec![0.025, 0.03, 0.035, 0.04, 0.045],
1117 UnixNanos::from(1_500_000_000),
1118 UnixNanos::from(1_000_000_000),
1119 )
1120 }
1121
1122 #[rstest]
1123 fn test_black_scholes_greeks_result_creation() {
1124 let result = BlackScholesGreeksResult {
1125 price: 25.5,
1126 vol: 0.2,
1127 delta: 0.65,
1128 gamma: 0.003,
1129 vega: 15.2,
1130 theta: -0.08,
1131 itm_prob: 0.55,
1132 rho: 1.75,
1133 };
1134
1135 assert_eq!(result.price, 25.5);
1136 assert_eq!(result.delta, 0.65);
1137 assert_eq!(result.gamma, 0.003);
1138 assert_eq!(result.vega, 15.2);
1139 assert_eq!(result.theta, -0.08);
1140 assert_eq!(result.itm_prob, 0.55);
1141 }
1142
1143 #[rstest]
1144 fn test_black_scholes_greeks_result_clone_and_copy() {
1145 let result1 = BlackScholesGreeksResult {
1146 price: 25.5,
1147 vol: 0.2,
1148 delta: 0.65,
1149 gamma: 0.003,
1150 vega: 15.2,
1151 theta: -0.08,
1152 itm_prob: 0.55,
1153 rho: 1.75,
1154 };
1155 let result2 = result1;
1156 let result3 = result1;
1157
1158 assert_eq!(result1, result2);
1159 assert_eq!(result1, result3);
1160 }
1161
1162 #[rstest]
1163 fn test_black_scholes_greeks_result_debug() {
1164 let result = BlackScholesGreeksResult {
1165 price: 25.5,
1166 vol: 0.2,
1167 delta: 0.65,
1168 gamma: 0.003,
1169 vega: 15.2,
1170 theta: -0.08,
1171 itm_prob: 0.55,
1172 rho: 1.75,
1173 };
1174 let debug_str = format!("{result:?}");
1175
1176 assert!(debug_str.contains("BlackScholesGreeksResult"));
1177 assert!(debug_str.contains("25.5"));
1178 assert!(debug_str.contains("0.65"));
1179 }
1180
1181 #[rstest]
1182 fn test_imply_vol_and_greeks_result_creation() {
1183 let result = BlackScholesGreeksResult {
1184 price: 25.5,
1185 vol: 0.2,
1186 delta: 0.65,
1187 gamma: 0.003,
1188 vega: 15.2,
1189 theta: -0.08,
1190 itm_prob: 0.55,
1191 rho: 1.75,
1192 };
1193
1194 assert_eq!(result.vol, 0.2);
1195 assert_eq!(result.price, 25.5);
1196 assert_eq!(result.delta, 0.65);
1197 assert_eq!(result.gamma, 0.003);
1198 assert_eq!(result.vega, 15.2);
1199 assert_eq!(result.theta, -0.08);
1200 }
1201
1202 #[rstest]
1203 fn test_black_scholes_greeks_basic_call() {
1204 let s = 100.0;
1205 let r = 0.05;
1206 let b = 0.05;
1207 let vol = 0.2;
1208 let is_call = true;
1209 let k = 100.0;
1210 let t = 1.0;
1211
1212 let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t);
1213
1214 assert!(greeks.price > 0.0);
1215 assert!(greeks.delta > 0.0 && greeks.delta < 1.0);
1216 assert!(greeks.gamma > 0.0);
1217 assert!(greeks.vega > 0.0);
1218 assert!(greeks.theta < 0.0); }
1220
1221 #[rstest]
1222 fn test_black_scholes_greeks_basic_put() {
1223 let s = 100.0;
1224 let r = 0.05;
1225 let b = 0.05;
1226 let vol = 0.2;
1227 let is_call = false;
1228 let k = 100.0;
1229 let t = 1.0;
1230
1231 let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t);
1232
1233 assert!(
1234 greeks.price > 0.0,
1235 "Put option price should be positive, was: {}",
1236 greeks.price
1237 );
1238 assert!(greeks.delta < 0.0 && greeks.delta > -1.0);
1239 assert!(greeks.gamma > 0.0);
1240 assert!(greeks.vega > 0.0);
1241 assert!(greeks.theta < 0.0); }
1243
1244 #[rstest]
1245 fn test_black_scholes_greeks_deep_itm_call() {
1246 let s = 150.0;
1247 let r = 0.05;
1248 let b = 0.05;
1249 let vol = 0.2;
1250 let is_call = true;
1251 let k = 100.0;
1252 let t = 1.0;
1253
1254 let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t);
1255
1256 assert!(greeks.delta > 0.9); assert!(greeks.gamma > 0.0 && greeks.gamma < 0.01); }
1259
1260 #[rstest]
1261 fn test_black_scholes_greeks_deep_otm_call() {
1262 let s = 50.0;
1263 let r = 0.05;
1264 let b = 0.05;
1265 let vol = 0.2;
1266 let is_call = true;
1267 let k = 100.0;
1268 let t = 1.0;
1269
1270 let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t);
1271
1272 assert!(greeks.delta < 0.1); assert!(greeks.gamma > 0.0 && greeks.gamma < 0.01); }
1275
1276 #[rstest]
1277 fn test_black_scholes_greeks_zero_time() {
1278 let s = 100.0;
1279 let r = 0.05;
1280 let b = 0.05;
1281 let vol = 0.2;
1282 let is_call = true;
1283 let k = 100.0;
1284 let t = 0.0001; let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t);
1287
1288 assert!(greeks.price >= 0.0);
1289 assert!(greeks.theta.is_finite());
1290 }
1291
1292 #[rstest]
1293 fn test_imply_vol_basic() {
1294 let s = 100.0;
1295 let r = 0.05;
1296 let b = 0.05;
1297 let vol = 0.2;
1298 let is_call = true;
1299 let k = 100.0;
1300 let t = 1.0;
1301
1302 let theoretical_price = black_scholes_greeks(s, r, b, vol, is_call, k, t).price;
1303 let implied_vol = imply_vol(s, r, b, is_call, k, t, theoretical_price);
1304
1305 let tolerance = 1e-4;
1307 assert!(
1308 (implied_vol - vol).abs() < tolerance,
1309 "Implied vol difference exceeds tolerance: {implied_vol} vs {vol}"
1310 );
1311 }
1312
1313 #[rstest]
1320 fn test_greeks_data_new() {
1321 let greeks = create_test_greeks_data();
1322
1323 assert_eq!(greeks.ts_init, UnixNanos::from(1_000_000_000));
1324 assert_eq!(greeks.ts_event, UnixNanos::from(1_500_000_000));
1325 assert_eq!(
1326 greeks.instrument_id,
1327 InstrumentId::from("SPY240315C00500000.OPRA")
1328 );
1329 assert!(greeks.is_call);
1330 assert_eq!(greeks.strike, 500.0);
1331 assert_eq!(greeks.expiry, 20_240_315);
1332 assert_eq!(greeks.expiry_in_years, 0.25);
1333 assert_eq!(greeks.multiplier, 100.0);
1334 assert_eq!(greeks.quantity, 1.0);
1335 assert_eq!(greeks.underlying_price, 520.0);
1336 assert_eq!(greeks.interest_rate, 0.05);
1337 assert_eq!(greeks.cost_of_carry, 0.05);
1338 assert_eq!(greeks.vol, 0.2);
1339 assert_eq!(greeks.pnl, 250.0);
1340 assert_eq!(greeks.price, 25.5);
1341 assert_eq!(greeks.delta, 0.65);
1342 assert_eq!(greeks.gamma, 0.003);
1343 assert_eq!(greeks.vega, 15.2);
1344 assert_eq!(greeks.theta, -0.08);
1345 assert_eq!(greeks.itm_prob, 0.75);
1346 }
1347
1348 #[rstest]
1349 fn test_greeks_data_from_delta() {
1350 let delta = 0.5;
1351 let multiplier = 100.0;
1352 let ts_event = UnixNanos::from(2_000_000_000);
1353 let instrument_id = InstrumentId::from("AAPL240315C00180000.OPRA");
1354
1355 let greeks = GreeksData::from_delta(instrument_id, delta, multiplier, ts_event);
1356
1357 assert_eq!(greeks.ts_init, ts_event);
1358 assert_eq!(greeks.ts_event, ts_event);
1359 assert_eq!(greeks.instrument_id, instrument_id);
1360 assert!(greeks.is_call);
1361 assert_eq!(greeks.delta, delta);
1362 assert_eq!(greeks.multiplier, multiplier);
1363 assert_eq!(greeks.quantity, 1.0);
1364
1365 assert_eq!(greeks.strike, 0.0);
1367 assert_eq!(greeks.expiry, 0);
1368 assert_eq!(greeks.price, 0.0);
1369 assert_eq!(greeks.gamma, 0.0);
1370 assert_eq!(greeks.vega, 0.0);
1371 assert_eq!(greeks.theta, 0.0);
1372 }
1373
1374 #[rstest]
1375 fn test_greeks_data_default() {
1376 let greeks = GreeksData::default();
1377
1378 assert_eq!(greeks.ts_init, UnixNanos::default());
1379 assert_eq!(greeks.ts_event, UnixNanos::default());
1380 assert_eq!(greeks.instrument_id, InstrumentId::from("ES.GLBX"));
1381 assert!(greeks.is_call);
1382 assert_eq!(greeks.strike, 0.0);
1383 assert_eq!(greeks.expiry, 0);
1384 assert_eq!(greeks.multiplier, 0.0);
1385 assert_eq!(greeks.quantity, 0.0);
1386 assert_eq!(greeks.delta, 0.0);
1387 assert_eq!(greeks.gamma, 0.0);
1388 assert_eq!(greeks.vega, 0.0);
1389 assert_eq!(greeks.theta, 0.0);
1390 }
1391
1392 #[rstest]
1393 fn test_greeks_data_display() {
1394 let greeks = create_test_greeks_data();
1395 let display_str = format!("{greeks}");
1396
1397 assert_eq!(
1398 display_str,
1399 "GreeksData(instrument_id=SPY240315C00500000.OPRA, expiry=20240315, itm_prob=75.00%, vol=20.00%, pnl=250.00, price=25.50, delta=0.65, gamma=0.00, vega=15.20, theta=-0.08, quantity=1, ts_event=1970-01-01T00:00:01.500000000Z, ts_init=1970-01-01T00:00:01.000000000Z)"
1400 );
1401 }
1402
1403 #[rstest]
1404 fn test_greeks_data_multiplication() {
1405 let greeks = create_test_greeks_data();
1406 let quantity = 5.0;
1407 let scaled_greeks = quantity * &greeks;
1408
1409 assert_eq!(scaled_greeks.ts_init, greeks.ts_init);
1410 assert_eq!(scaled_greeks.ts_event, greeks.ts_event);
1411 assert_eq!(scaled_greeks.instrument_id, greeks.instrument_id);
1412 assert_eq!(scaled_greeks.is_call, greeks.is_call);
1413 assert_eq!(scaled_greeks.strike, greeks.strike);
1414 assert_eq!(scaled_greeks.expiry, greeks.expiry);
1415 assert_eq!(scaled_greeks.multiplier, greeks.multiplier);
1416 assert_eq!(scaled_greeks.quantity, greeks.quantity);
1417 assert_eq!(scaled_greeks.vol, greeks.vol);
1418 assert_eq!(scaled_greeks.itm_prob, greeks.itm_prob);
1419
1420 assert_eq!(scaled_greeks.pnl, quantity * greeks.pnl);
1422 assert_eq!(scaled_greeks.price, quantity * greeks.price);
1423 assert_eq!(scaled_greeks.delta, quantity * greeks.delta);
1424 assert_eq!(scaled_greeks.gamma, quantity * greeks.gamma);
1425 assert_eq!(scaled_greeks.vega, quantity * greeks.vega);
1426 assert_eq!(scaled_greeks.theta, quantity * greeks.theta);
1427 }
1428
1429 #[rstest]
1430 fn test_greeks_data_has_ts_init() {
1431 let greeks = create_test_greeks_data();
1432 assert_eq!(greeks.ts_init(), UnixNanos::from(1_000_000_000));
1433 }
1434
1435 #[rstest]
1436 fn test_greeks_data_clone() {
1437 let greeks1 = create_test_greeks_data();
1438 let greeks2 = greeks1.clone();
1439
1440 assert_eq!(greeks1.ts_init, greeks2.ts_init);
1441 assert_eq!(greeks1.instrument_id, greeks2.instrument_id);
1442 assert_eq!(greeks1.delta, greeks2.delta);
1443 assert_eq!(greeks1.gamma, greeks2.gamma);
1444 }
1445
1446 #[rstest]
1447 fn test_portfolio_greeks_new() {
1448 let portfolio_greeks = create_test_portfolio_greeks();
1449
1450 assert_eq!(portfolio_greeks.ts_init, UnixNanos::from(1_000_000_000));
1451 assert_eq!(portfolio_greeks.ts_event, UnixNanos::from(1_500_000_000));
1452 assert_eq!(portfolio_greeks.pnl, 1500.0);
1453 assert_eq!(portfolio_greeks.price, 125.5);
1454 assert_eq!(portfolio_greeks.delta, 2.15);
1455 assert_eq!(portfolio_greeks.gamma, 0.008);
1456 assert_eq!(portfolio_greeks.vega, 42.7);
1457 assert_eq!(portfolio_greeks.theta, -2.3);
1458 }
1459
1460 #[rstest]
1461 fn test_portfolio_greeks_default() {
1462 let portfolio_greeks = PortfolioGreeks::default();
1463
1464 assert_eq!(portfolio_greeks.ts_init, UnixNanos::default());
1465 assert_eq!(portfolio_greeks.ts_event, UnixNanos::default());
1466 assert_eq!(portfolio_greeks.pnl, 0.0);
1467 assert_eq!(portfolio_greeks.price, 0.0);
1468 assert_eq!(portfolio_greeks.delta, 0.0);
1469 assert_eq!(portfolio_greeks.gamma, 0.0);
1470 assert_eq!(portfolio_greeks.vega, 0.0);
1471 assert_eq!(portfolio_greeks.theta, 0.0);
1472 }
1473
1474 #[rstest]
1475 fn test_portfolio_greeks_display() {
1476 let portfolio_greeks = create_test_portfolio_greeks();
1477 let display_str = format!("{portfolio_greeks}");
1478
1479 assert_eq!(
1480 display_str,
1481 "PortfolioGreeks(pnl=1500.00, price=125.50, delta=2.15, gamma=0.01, vega=42.70, theta=-2.30, ts_event=1970-01-01T00:00:01.500000000Z, ts_init=1970-01-01T00:00:01.000000000Z)"
1482 );
1483 }
1484
1485 #[rstest]
1486 fn test_portfolio_greeks_addition() {
1487 let mut greeks1 = PortfolioGreeks::new(
1488 100.0,
1489 50.0,
1490 1.0,
1491 0.005,
1492 20.0,
1493 -1.0,
1494 UnixNanos::from(1_500_000_000),
1495 UnixNanos::from(1_000_000_000),
1496 );
1497
1498 let mut greeks2 = PortfolioGreeks::new(
1499 200.0,
1500 75.0,
1501 1.5,
1502 0.003,
1503 25.0,
1504 -1.5,
1505 UnixNanos::from(2_500_000_000),
1506 UnixNanos::from(2_000_000_000),
1507 );
1508
1509 greeks1.greeks.rho = 12.0;
1510 greeks2.greeks.rho = 17.0;
1511
1512 let result = greeks1 + greeks2;
1513
1514 assert_eq!(result.ts_init, UnixNanos::from(1_000_000_000)); assert_eq!(result.ts_event, UnixNanos::from(1_500_000_000)); assert_eq!(result.pnl, 300.0);
1517 assert_eq!(result.price, 125.0);
1518 assert_eq!(result.delta, 2.5);
1519 assert_eq!(result.gamma, 0.008);
1520 assert_eq!(result.vega, 45.0);
1521 assert_eq!(result.theta, -2.5);
1522 assert_eq!(result.rho, 29.0);
1523 }
1524
1525 #[rstest]
1526 #[case(1.0, [20.0, 30.0, 40.0, 50.0, 60.0, -70.0, 80.0])]
1527 #[case(2.0, [40.0, 60.0, 80.0, 100.0, 120.0, -140.0, 160.0])]
1528 #[case(-3.0, [-60.0, -90.0, -120.0, -150.0, -180.0, 210.0, -240.0])]
1529 #[case(0.0, [0.0; 7])]
1530 fn test_greeks_data_portfolio_scaling(#[case] quantity: f64, #[case] expected: [f64; 7]) {
1531 let greeks = GreeksData {
1532 pnl: 2.0,
1533 price: 3.0,
1534 multiplier: 10.0,
1535 quantity: 9.0,
1536 greeks: OptionGreekValues {
1537 delta: 4.0,
1538 gamma: 5.0,
1539 vega: 6.0,
1540 theta: -7.0,
1541 rho: 8.0,
1542 },
1543 ..create_test_greeks_data()
1544 };
1545
1546 let portfolio = quantity * &greeks.to_portfolio_greeks();
1547
1548 assert_eq!(portfolio.ts_init, UnixNanos::from(1_000_000_000));
1549 assert_eq!(portfolio.ts_event, UnixNanos::from(1_500_000_000));
1550 assert_eq!(
1551 [
1552 portfolio.pnl,
1553 portfolio.price,
1554 portfolio.delta,
1555 portfolio.gamma,
1556 portfolio.vega,
1557 portfolio.theta,
1558 portfolio.rho
1559 ],
1560 expected,
1561 );
1562 assert_eq!(greeks.pnl, 2.0);
1563 assert_eq!(greeks.price, 3.0);
1564 assert_eq!(greeks.multiplier, 10.0);
1565 assert_eq!(greeks.quantity, 9.0);
1566 assert_eq!(
1567 greeks.greeks,
1568 OptionGreekValues {
1569 delta: 4.0,
1570 gamma: 5.0,
1571 vega: 6.0,
1572 theta: -7.0,
1573 rho: 8.0,
1574 }
1575 );
1576 }
1577
1578 #[rstest]
1579 fn test_portfolio_greeks_from_greeks_data() {
1580 let greeks_data = create_test_greeks_data();
1581 let portfolio_greeks: PortfolioGreeks = greeks_data.clone().into();
1582
1583 assert_eq!(portfolio_greeks.ts_init, greeks_data.ts_init);
1584 assert_eq!(portfolio_greeks.ts_event, greeks_data.ts_event);
1585 assert_eq!(portfolio_greeks.pnl, 25_000.0);
1586 assert_eq!(portfolio_greeks.price, 2_550.0);
1587 assert_eq!(portfolio_greeks.delta, 65.0);
1588 assert_eq!(portfolio_greeks.gamma, 0.3);
1589 assert_eq!(portfolio_greeks.vega, 1_520.0);
1590 assert_eq!(portfolio_greeks.theta, -8.0);
1591 assert_eq!(portfolio_greeks.rho, 0.0);
1592 }
1593
1594 #[rstest]
1595 fn test_portfolio_greeks_has_ts_init() {
1596 let portfolio_greeks = create_test_portfolio_greeks();
1597 assert_eq!(portfolio_greeks.ts_init(), UnixNanos::from(1_000_000_000));
1598 }
1599
1600 #[rstest]
1601 fn test_yield_curve_new() {
1602 let curve = create_test_yield_curve();
1603
1604 assert_eq!(curve.ts_init, UnixNanos::from(1_000_000_000));
1605 assert_eq!(curve.ts_event, UnixNanos::from(1_500_000_000));
1606 assert_eq!(curve.curve_name, "USD");
1607 assert_eq!(curve.tenors, vec![0.25, 0.5, 1.0, 2.0, 5.0]);
1608 assert_eq!(curve.interest_rates, vec![0.025, 0.03, 0.035, 0.04, 0.045]);
1609 }
1610
1611 #[rstest]
1612 fn test_yield_curve_default() {
1613 let curve = YieldCurve::default();
1614
1615 assert_eq!(curve.ts_init, UnixNanos::default());
1616 assert_eq!(curve.ts_event, UnixNanos::default());
1617 assert_eq!(curve.curve_name, "USD");
1618 assert_eq!(curve.tenors, vec![0.5, 1.0, 1.5, 2.0, 2.5]);
1619 assert_eq!(curve.interest_rates, vec![0.04, 0.04, 0.04, 0.04, 0.04]);
1620 }
1621
1622 #[rstest]
1623 fn test_yield_curve_get_rate_single_point() {
1624 let curve = YieldCurve::new(
1625 "USD".to_string(),
1626 vec![1.0],
1627 vec![0.05],
1628 UnixNanos::default(),
1629 UnixNanos::default(),
1630 );
1631
1632 assert_eq!(curve.get_rate(0.5), 0.05);
1633 assert_eq!(curve.get_rate(1.0), 0.05);
1634 assert_eq!(curve.get_rate(2.0), 0.05);
1635 }
1636
1637 #[rstest]
1638 fn test_yield_curve_get_rate_interpolation() {
1639 let curve = create_test_yield_curve();
1640
1641 assert_eq!(curve.get_rate(0.25), 0.025);
1643 assert_eq!(curve.get_rate(1.0), 0.035);
1644 assert_eq!(curve.get_rate(5.0), 0.045);
1645
1646 let rate_0_75 = curve.get_rate(0.75);
1648 assert!(rate_0_75 > 0.025 && rate_0_75 < 0.045);
1649 }
1650
1651 #[rstest]
1652 fn test_yield_curve_display() {
1653 let curve = create_test_yield_curve();
1654 let display_str = format!("{curve}");
1655
1656 assert_eq!(
1657 display_str,
1658 "YieldCurve(curve_name=USD, ts_event=1970-01-01T00:00:01.500000000Z, ts_init=1970-01-01T00:00:01.000000000Z)"
1659 );
1660 }
1661
1662 #[rstest]
1663 fn test_yield_curve_has_ts_init() {
1664 let curve = create_test_yield_curve();
1665 assert_eq!(curve.ts_init(), UnixNanos::from(1_000_000_000));
1666 }
1667
1668 #[rstest]
1669 fn test_yield_curve_clone() {
1670 let curve1 = create_test_yield_curve();
1671 let curve2 = curve1.clone();
1672
1673 assert_eq!(curve1.curve_name, curve2.curve_name);
1674 assert_eq!(curve1.tenors, curve2.tenors);
1675 assert_eq!(curve1.interest_rates, curve2.interest_rates);
1676 }
1677
1678 #[rstest]
1679 #[case(vec![0.0], vec![0.04])]
1680 #[case(vec![0.25, 1.0, 5.0], vec![0.043, 0.041, 0.038])]
1681 fn test_yield_curve_validate_accepts_evaluable_curves(
1682 #[case] tenors: Vec<f64>,
1683 #[case] interest_rates: Vec<f64>,
1684 ) {
1685 let curve = YieldCurve::new(
1686 "USD".to_string(),
1687 tenors,
1688 interest_rates,
1689 UnixNanos::default(),
1690 UnixNanos::default(),
1691 );
1692
1693 assert!(curve.validate().is_ok());
1694 }
1695
1696 #[rstest]
1697 #[case(vec![0.5, 1.0, 2.0], vec![0.04, 0.05], "'tenors' usize of 3 was not equal")]
1698 #[case(vec![0.5, 1.0], vec![0.04, 0.05], "one rate or at least three points")]
1699 #[case(vec![], vec![], "one rate or at least three points")]
1700 #[case(vec![0.25, 1.0, 5.0], vec![0.04, f64::NAN, 0.05], "must be finite")]
1701 #[case(vec![0.25, f64::INFINITY, 5.0], vec![0.04, 0.05, 0.06], "must be finite")]
1702 #[case(vec![0.05, 0.05, 1.0], vec![0.04, 0.05, 0.06], "strictly increasing")]
1703 #[case(vec![1.0, 0.5, 2.0], vec![0.04, 0.05, 0.06], "strictly increasing")]
1704 #[case(vec![1.0, 1.0 + f64::EPSILON, 2.0], vec![0.04, 0.05, 0.06], "wide enough")]
1705 fn test_yield_curve_validate_rejects_curves_get_rate_cannot_evaluate(
1706 #[case] tenors: Vec<f64>,
1707 #[case] interest_rates: Vec<f64>,
1708 #[case] message: &str,
1709 ) {
1710 let curve = YieldCurve::new(
1711 "USD".to_string(),
1712 tenors,
1713 interest_rates,
1714 UnixNanos::default(),
1715 UnixNanos::default(),
1716 );
1717
1718 let error = curve.validate().unwrap_err();
1719
1720 assert!(
1721 error.to_string().contains(message),
1722 "error was {error}, expected to contain {message}"
1723 );
1724 }
1725
1726 #[rstest]
1727 fn test_yield_curve_custom_data_json_round_trip() {
1728 let curve = create_test_yield_curve();
1729 let mut other = curve.clone();
1730 other.interest_rates[0] = 0.02;
1731
1732 let json = curve.to_json().unwrap();
1733 let restored = YieldCurve::from_json(serde_json::from_str(&json).unwrap()).unwrap();
1734
1735 assert_eq!(curve.type_name(), "YieldCurve");
1736 assert_eq!(YieldCurve::type_name_static(), "YieldCurve");
1737 assert_eq!(
1738 CustomDataTrait::ts_event(&curve),
1739 UnixNanos::from(1_500_000_000)
1740 );
1741 assert!(restored.eq_arc(&curve));
1742 assert!(!restored.eq_arc(&other));
1743 assert_eq!(restored.as_any().downcast_ref::<YieldCurve>(), Some(&curve));
1744 }
1745
1746 #[rstest]
1747 fn test_black_scholes_greeks_extreme_values() {
1748 let s = 1000.0;
1749 let r = 0.1;
1750 let b = 0.1;
1751 let vol = 0.5;
1752 let is_call = true;
1753 let k = 10.0; let t = 0.1;
1755
1756 let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t);
1757
1758 assert!(greeks.price.is_finite());
1759 assert!(greeks.delta.is_finite());
1760 assert!(greeks.gamma.is_finite());
1761 assert!(greeks.vega.is_finite());
1762 assert!(greeks.theta.is_finite());
1763 assert!(greeks.price > 0.0);
1764 assert!(greeks.delta > 0.99); }
1766
1767 #[rstest]
1768 fn test_black_scholes_greeks_high_volatility() {
1769 let s = 100.0;
1770 let r = 0.05;
1771 let b = 0.05;
1772 let vol = 2.0; let is_call = true;
1774 let k = 100.0;
1775 let t = 1.0;
1776
1777 let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t);
1778
1779 assert!(greeks.price.is_finite());
1780 assert!(greeks.delta.is_finite());
1781 assert!(greeks.gamma.is_finite());
1782 assert!(greeks.vega.is_finite());
1783 assert!(greeks.theta.is_finite());
1784 assert!(greeks.price > 0.0);
1785 }
1786
1787 #[rstest]
1788 fn test_greeks_data_put_option() {
1789 let greeks = GreeksData::new(
1790 InstrumentId::from("SPY240315P00480000.OPRA"),
1791 false, 480.0,
1793 20_240_315,
1794 91, 0.25,
1796 100.0,
1797 1.0,
1798 500.0,
1799 0.05,
1800 0.05,
1801 0.25,
1802 -150.0, 8.5,
1804 OptionGreekValues {
1805 delta: -0.35,
1806 gamma: 0.002,
1807 vega: 12.8,
1808 theta: -0.06,
1809 rho: 0.0,
1810 },
1811 0.25,
1812 UnixNanos::from(1_500_000_000),
1813 UnixNanos::from(1_000_000_000),
1814 );
1815
1816 assert!(!greeks.is_call);
1817 assert!(greeks.delta < 0.0);
1818 assert_eq!(greeks.pnl, -150.0);
1819 }
1820
1821 #[rstest]
1823 fn test_greeks_accuracy_call() {
1824 let s = 100.0;
1825 let k = 100.1;
1826 let t = 1.0;
1827 let r = 0.01;
1828 let b = 0.005;
1829 let vol = 0.2;
1830 let is_call = true;
1831 let eps = 1e-3;
1832
1833 let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t);
1834
1835 let price0 = |s: f64| black_scholes_greeks_exact(s, r, b, vol, is_call, k, t).price;
1837
1838 let delta_bnr = (price0(s + eps) - price0(s - eps)) / (2.0 * eps);
1839 let gamma_bnr = (price0(s + eps) + price0(s - eps) - 2.0 * price0(s)) / (eps * eps);
1840 let vega_bnr = (black_scholes_greeks_exact(s, r, b, vol + eps, is_call, k, t).price
1841 - black_scholes_greeks_exact(s, r, b, vol - eps, is_call, k, t).price)
1842 / (2.0 * eps)
1843 / 100.0;
1844 let theta_bnr = (black_scholes_greeks_exact(s, r, b, vol, is_call, k, t - eps).price
1845 - black_scholes_greeks_exact(s, r, b, vol, is_call, k, t + eps).price)
1846 / (2.0 * eps)
1847 / 365.25;
1848
1849 let tolerance = 5e-3;
1852 assert!(
1853 (greeks.delta - delta_bnr).abs() < tolerance,
1854 "Delta difference exceeds tolerance: {} vs {}",
1855 greeks.delta,
1856 delta_bnr
1857 );
1858 let gamma_tolerance = 0.1;
1860 assert!(
1861 (greeks.gamma - gamma_bnr).abs() < gamma_tolerance,
1862 "Gamma difference exceeds tolerance: {} vs {}",
1863 greeks.gamma,
1864 gamma_bnr
1865 );
1866 assert!(
1868 (greeks.vega - vega_bnr).abs() < tolerance,
1869 "Vega difference exceeds tolerance: {} vs {}",
1870 greeks.vega,
1871 vega_bnr
1872 );
1873 assert!(
1874 (greeks.theta - theta_bnr).abs() < tolerance,
1875 "Theta difference exceeds tolerance: {} vs {}",
1876 greeks.theta,
1877 theta_bnr
1878 );
1879 }
1880
1881 #[rstest]
1882 fn test_greeks_accuracy_put() {
1883 let s = 100.0;
1884 let k = 100.1;
1885 let t = 1.0;
1886 let r = 0.01;
1887 let b = 0.005;
1888 let vol = 0.2;
1889 let is_call = false;
1890 let eps = 1e-3;
1891
1892 let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t);
1893
1894 let price0 = |s: f64| black_scholes_greeks_exact(s, r, b, vol, is_call, k, t).price;
1896
1897 let delta_bnr = (price0(s + eps) - price0(s - eps)) / (2.0 * eps);
1898 let gamma_bnr = (price0(s + eps) + price0(s - eps) - 2.0 * price0(s)) / (eps * eps);
1899 let vega_bnr = (black_scholes_greeks_exact(s, r, b, vol + eps, is_call, k, t).price
1900 - black_scholes_greeks_exact(s, r, b, vol - eps, is_call, k, t).price)
1901 / (2.0 * eps)
1902 / 100.0;
1903 let theta_bnr = (black_scholes_greeks_exact(s, r, b, vol, is_call, k, t - eps).price
1904 - black_scholes_greeks_exact(s, r, b, vol, is_call, k, t + eps).price)
1905 / (2.0 * eps)
1906 / 365.25;
1907
1908 let tolerance = 5e-3;
1911 assert!(
1912 (greeks.delta - delta_bnr).abs() < tolerance,
1913 "Delta difference exceeds tolerance: {} vs {}",
1914 greeks.delta,
1915 delta_bnr
1916 );
1917 let gamma_tolerance = 0.1;
1919 assert!(
1920 (greeks.gamma - gamma_bnr).abs() < gamma_tolerance,
1921 "Gamma difference exceeds tolerance: {} vs {}",
1922 greeks.gamma,
1923 gamma_bnr
1924 );
1925 assert!(
1927 (greeks.vega - vega_bnr).abs() < tolerance,
1928 "Vega difference exceeds tolerance: {} vs {}",
1929 greeks.vega,
1930 vega_bnr
1931 );
1932 assert!(
1933 (greeks.theta - theta_bnr).abs() < tolerance,
1934 "Theta difference exceeds tolerance: {} vs {}",
1935 greeks.theta,
1936 theta_bnr
1937 );
1938 }
1939
1940 #[rstest]
1941 fn test_imply_vol_and_greeks_accuracy_call() {
1942 let s = 100.0;
1943 let k = 100.1;
1944 let t = 1.0;
1945 let r = 0.01;
1946 let b = 0.005;
1947 let vol = 0.2;
1948 let is_call = true;
1949
1950 let base_greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t);
1951 let price = base_greeks.price;
1952
1953 let implied_result = imply_vol_and_greeks(s, r, b, is_call, k, t, price);
1954
1955 let tolerance = 2e-4;
1957 assert!(
1958 (implied_result.vol - vol).abs() < tolerance,
1959 "Vol difference exceeds tolerance: {} vs {}",
1960 implied_result.vol,
1961 vol
1962 );
1963 assert!(
1964 (implied_result.price - base_greeks.price).abs() < tolerance,
1965 "Price difference exceeds tolerance: {} vs {}",
1966 implied_result.price,
1967 base_greeks.price
1968 );
1969 assert!(
1970 (implied_result.delta - base_greeks.delta).abs() < tolerance,
1971 "Delta difference exceeds tolerance: {} vs {}",
1972 implied_result.delta,
1973 base_greeks.delta
1974 );
1975 assert!(
1976 (implied_result.gamma - base_greeks.gamma).abs() < tolerance,
1977 "Gamma difference exceeds tolerance: {} vs {}",
1978 implied_result.gamma,
1979 base_greeks.gamma
1980 );
1981 assert!(
1982 (implied_result.vega - base_greeks.vega).abs() < tolerance,
1983 "Vega difference exceeds tolerance: {} vs {}",
1984 implied_result.vega,
1985 base_greeks.vega
1986 );
1987 assert!(
1988 (implied_result.theta - base_greeks.theta).abs() < tolerance,
1989 "Theta difference exceeds tolerance: {} vs {}",
1990 implied_result.theta,
1991 base_greeks.theta
1992 );
1993 }
1994
1995 #[rstest]
1996 fn test_black_scholes_greeks_target_price_refinement() {
1997 let s = 100.0;
1998 let r = 0.05;
1999 let b = 0.05;
2000 let initial_vol = 0.2;
2001 let is_call = true;
2002 let k = 100.0;
2003 let t = 1.0;
2004
2005 let initial_greeks = black_scholes_greeks(s, r, b, initial_vol, is_call, k, t);
2007 let target_price = initial_greeks.price;
2008
2009 let refined_vol = initial_vol * 1.1; let refined_greeks =
2012 refine_vol_and_greeks(s, r, b, is_call, k, t, target_price, refined_vol);
2013
2014 let price_tolerance = (s * 5e-5).max(1e-4) * 2.0;
2017 assert!(
2018 (refined_greeks.price - target_price).abs() < price_tolerance,
2019 "Refined price should match target: {} vs {}",
2020 refined_greeks.price,
2021 target_price
2022 );
2023
2024 assert!(
2026 refined_vol > refined_greeks.vol && refined_greeks.vol > initial_vol * 0.9,
2027 "Refined vol should converge towards initial: {} (initial: {}, refined: {})",
2028 refined_greeks.vol,
2029 initial_vol,
2030 refined_vol
2031 );
2032 }
2033
2034 #[rstest]
2035 fn test_black_scholes_greeks_target_price_refinement_put() {
2036 let s = 100.0;
2037 let r = 0.05;
2038 let b = 0.05;
2039 let initial_vol = 0.25;
2040 let is_call = false;
2041 let k = 105.0;
2042 let t = 0.5;
2043
2044 let initial_greeks = black_scholes_greeks(s, r, b, initial_vol, is_call, k, t);
2046 let target_price = initial_greeks.price;
2047
2048 let refined_vol = initial_vol * 0.8; let refined_greeks =
2051 refine_vol_and_greeks(s, r, b, is_call, k, t, target_price, refined_vol);
2052
2053 let price_tolerance = (s * 5e-5).max(1e-4) * 2.0;
2056 assert!(
2057 (refined_greeks.price - target_price).abs() < price_tolerance,
2058 "Refined price should match target: {} vs {}",
2059 refined_greeks.price,
2060 target_price
2061 );
2062
2063 assert!(
2065 refined_vol < refined_greeks.vol && refined_greeks.vol < initial_vol * 1.1,
2066 "Refined vol should converge towards initial: {} (initial: {}, refined: {})",
2067 refined_greeks.vol,
2068 initial_vol,
2069 refined_vol
2070 );
2071 }
2072
2073 #[rstest]
2074 fn test_imply_vol_and_greeks_accuracy_put() {
2075 let s = 100.0;
2076 let k = 100.1;
2077 let t = 1.0;
2078 let r = 0.01;
2079 let b = 0.005;
2080 let vol = 0.2;
2081 let is_call = false;
2082
2083 let base_greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t);
2084 let price = base_greeks.price;
2085
2086 let implied_result = imply_vol_and_greeks(s, r, b, is_call, k, t, price);
2087
2088 let tolerance = 2e-4;
2090 assert!(
2091 (implied_result.vol - vol).abs() < tolerance,
2092 "Vol difference exceeds tolerance: {} vs {}",
2093 implied_result.vol,
2094 vol
2095 );
2096 assert!(
2097 (implied_result.price - base_greeks.price).abs() < tolerance,
2098 "Price difference exceeds tolerance: {} vs {}",
2099 implied_result.price,
2100 base_greeks.price
2101 );
2102 assert!(
2103 (implied_result.delta - base_greeks.delta).abs() < tolerance,
2104 "Delta difference exceeds tolerance: {} vs {}",
2105 implied_result.delta,
2106 base_greeks.delta
2107 );
2108 assert!(
2109 (implied_result.gamma - base_greeks.gamma).abs() < tolerance,
2110 "Gamma difference exceeds tolerance: {} vs {}",
2111 implied_result.gamma,
2112 base_greeks.gamma
2113 );
2114 assert!(
2115 (implied_result.vega - base_greeks.vega).abs() < tolerance,
2116 "Vega difference exceeds tolerance: {} vs {}",
2117 implied_result.vega,
2118 base_greeks.vega
2119 );
2120 assert!(
2121 (implied_result.theta - base_greeks.theta).abs() < tolerance,
2122 "Theta difference exceeds tolerance: {} vs {}",
2123 implied_result.theta,
2124 base_greeks.theta
2125 );
2126 }
2127
2128 #[rstest]
2131 fn test_black_scholes_greeks_vs_exact(
2132 #[values(90.0, 100.0, 110.0)] spot: f64,
2133 #[values(true, false)] is_call: bool,
2134 #[values(0.15, 0.25, 0.5)] vol: f64,
2135 #[values(0.01, 0.25, 2.0)] t: f64,
2136 ) {
2137 let r = 0.05;
2138 let b = 0.05;
2139 let k = 100.0;
2140
2141 let greeks_fast = black_scholes_greeks(spot, r, b, vol, is_call, k, t);
2142 let greeks_exact = black_scholes_greeks_exact(spot, r, b, vol, is_call, k, t);
2143
2144 let rel_tolerance = if t < 0.1 {
2149 1e-4 } else {
2151 8e-6 };
2153 let abs_tolerance = 1e-10; let check_7_sig_figs = |fast: f64, exact: f64, name: &str| {
2157 let abs_diff = (fast - exact).abs();
2158 let small_value_threshold = 1e-4;
2162 let max_allowed = if exact.abs() < small_value_threshold {
2163 if t < 0.1 {
2165 1e-5 } else {
2167 1e-6 }
2169 } else {
2170 exact.abs().max(abs_tolerance) * rel_tolerance
2172 };
2173 let rel_diff = if exact.abs() > abs_tolerance {
2174 abs_diff / exact.abs()
2175 } else {
2176 0.0 };
2178
2179 assert!(
2180 abs_diff < max_allowed,
2181 "{name} mismatch for spot={spot}, is_call={is_call}, vol={vol}, t={t}: fast={fast:.10}, exact={exact:.10}, abs_diff={abs_diff:.2e}, rel_diff={rel_diff:.2e}, max_allowed={max_allowed:.2e}"
2182 );
2183 };
2184
2185 check_7_sig_figs(greeks_fast.price, greeks_exact.price, "Price");
2186 check_7_sig_figs(greeks_fast.delta, greeks_exact.delta, "Delta");
2187 check_7_sig_figs(greeks_fast.gamma, greeks_exact.gamma, "Gamma");
2188 check_7_sig_figs(greeks_fast.vega, greeks_exact.vega, "Vega");
2189 check_7_sig_figs(greeks_fast.theta, greeks_exact.theta, "Theta");
2190 }
2191
2192 #[rstest]
2195 fn test_refine_vol_and_greeks_vs_imply_vol_and_greeks(
2196 #[values(90.0, 100.0, 110.0)] spot: f64,
2197 #[values(true, false)] is_call: bool,
2198 #[values(0.15, 0.25, 0.5)] target_vol: f64,
2199 #[values(0.01, 0.25, 2.0)] t: f64,
2200 ) {
2201 let r = 0.05;
2202 let b = 0.05;
2203 let k = 100.0;
2204
2205 let base_greeks = black_scholes_greeks(spot, r, b, target_vol, is_call, k, t);
2207 let target_price = base_greeks.price;
2208
2209 let initial_guess = target_vol - 0.01;
2211
2212 let refined_result =
2214 refine_vol_and_greeks(spot, r, b, is_call, k, t, target_price, initial_guess);
2215
2216 let implied_result = imply_vol_and_greeks(spot, r, b, is_call, k, t, target_price);
2218
2219 let moneyness = (spot - k) / k;
2222 let is_deep_itm_otm = moneyness.abs() > 0.05;
2223 let is_deep_edge_case = t < 0.1 && is_deep_itm_otm;
2224
2225 let vol_abs_tolerance = 1e-6;
2231 let vol_rel_tolerance = if is_deep_edge_case {
2232 2.0 } else if t < 0.1 {
2235 0.10 } else if t > 1.5 {
2238 if target_vol <= 0.15 {
2240 0.05 } else {
2242 0.01 }
2244 } else {
2245 if target_vol <= 0.15 {
2247 0.05 } else {
2249 0.001 }
2251 };
2252
2253 let refined_vol_error = (refined_result.vol - target_vol).abs();
2254 let implied_vol_error = (implied_result.vol - target_vol).abs();
2255 let refined_vol_rel_error = refined_vol_error / target_vol.max(vol_abs_tolerance);
2256 let implied_vol_rel_error = implied_vol_error / target_vol.max(vol_abs_tolerance);
2257
2258 assert!(
2259 refined_vol_rel_error < vol_rel_tolerance,
2260 "Refined vol mismatch for spot={}, is_call={}, target_vol={}, t={}: refined={:.10}, target={:.10}, abs_error={:.2e}, rel_error={:.2e}",
2261 spot,
2262 is_call,
2263 target_vol,
2264 t,
2265 refined_result.vol,
2266 target_vol,
2267 refined_vol_error,
2268 refined_vol_rel_error
2269 );
2270
2271 let implied_vol_tolerance = if is_deep_edge_case {
2274 2.0 } else if implied_result.vol < 1e-6 {
2277 2.0 } else if t < 0.1 && (implied_result.vol - target_vol).abs() / target_vol.max(1e-6) > 0.5 {
2280 2.0 } else {
2283 vol_rel_tolerance
2284 };
2285
2286 assert!(
2287 implied_vol_rel_error < implied_vol_tolerance,
2288 "Implied vol mismatch for spot={}, is_call={}, target_vol={}, t={}: implied={:.10}, target={:.10}, abs_error={:.2e}, rel_error={:.2e}",
2289 spot,
2290 is_call,
2291 target_vol,
2292 t,
2293 implied_result.vol,
2294 target_vol,
2295 implied_vol_error,
2296 implied_vol_rel_error
2297 );
2298
2299 let greeks_abs_tolerance = 1e-10;
2303
2304 let moneyness = (spot - k) / k;
2306 let is_deep_itm_otm = moneyness.abs() > 0.05;
2307 let is_deep_edge_case = t < 0.1 && is_deep_itm_otm;
2308
2309 let greeks_rel_tolerance = if is_deep_edge_case {
2313 1.0 } else if t < 0.1 {
2316 if target_vol <= 0.15 {
2318 0.10 } else {
2320 0.05 }
2322 } else if t > 1.5 {
2323 if target_vol <= 0.15 {
2325 0.08 } else {
2327 0.01 }
2329 } else {
2330 if target_vol <= 0.15 {
2332 0.05 } else {
2334 2e-3 }
2336 };
2337
2338 let imply_vol_failed = implied_result.vol < 1e-6
2343 || (t < 0.1 && (implied_result.vol - target_vol).abs() / target_vol.max(1e-6) > 0.5)
2344 || is_deep_edge_case;
2345 let effective_greeks_tolerance = if imply_vol_failed || is_deep_edge_case {
2346 1.0 } else {
2348 greeks_rel_tolerance
2349 };
2350
2351 let check_6_sig_figs = |refined: f64, implied: f64, name: &str, is_gamma: bool| {
2352 if (imply_vol_failed || is_deep_edge_case)
2355 && (!implied.is_finite() || implied.abs() < 1e-4 || refined.abs() < 1e-4)
2356 {
2357 return; }
2359
2360 let abs_diff = (refined - implied).abs();
2361 let small_value_threshold = if is_deep_edge_case { 1e-3 } else { 1e-6 };
2364 let rel_diff =
2365 if implied.abs() < small_value_threshold && refined.abs() < small_value_threshold {
2366 0.0 } else {
2368 abs_diff / implied.abs().max(greeks_abs_tolerance)
2369 };
2370 let gamma_multiplier = if (0.1..=1.5).contains(&t) {
2372 if target_vol <= 0.15 { 5.0 } else { 3.0 }
2374 } else {
2375 if target_vol <= 0.15 { 10.0 } else { 5.0 }
2377 };
2378 let tolerance = if is_gamma {
2379 effective_greeks_tolerance * gamma_multiplier
2380 } else {
2381 effective_greeks_tolerance
2382 };
2383 let max_allowed = if is_deep_edge_case && implied.abs() < 1e-3 {
2385 2e-5 } else {
2387 implied.abs().max(greeks_abs_tolerance) * tolerance
2388 };
2389
2390 assert!(
2391 abs_diff < max_allowed,
2392 "{name} mismatch between refine and imply for spot={spot}, is_call={is_call}, target_vol={target_vol}, t={t}: refined={refined:.10}, implied={implied:.10}, abs_diff={abs_diff:.2e}, rel_diff={rel_diff:.2e}, max_allowed={max_allowed:.2e}"
2393 );
2394 };
2395
2396 check_6_sig_figs(refined_result.price, implied_result.price, "Price", false);
2397 check_6_sig_figs(refined_result.delta, implied_result.delta, "Delta", false);
2398 check_6_sig_figs(refined_result.gamma, implied_result.gamma, "Gamma", true);
2399 check_6_sig_figs(refined_result.vega, implied_result.vega, "Vega", false);
2400 check_6_sig_figs(refined_result.theta, implied_result.theta, "Theta", false);
2401 }
2402
2403 #[rstest]
2404 #[case(true, 1.0, 0.437_729_301_518)]
2405 #[case(false, 1.0, -0.513_500_122_983)]
2406 #[case(true, 0.0, -0.075_770_821_464)]
2407 #[case(false, 0.0, -0.075_770_821_464)]
2408 fn test_checked_greeks_rho_at_zero_carry(
2409 #[case] is_call: bool,
2410 #[case] db_dr: f64,
2411 #[case] expected: f64,
2412 ) {
2413 let result =
2414 black_scholes_greeks_checked(100.0, 0.05, 0.0, 0.2, is_call, 100.0, 1.0, db_dr)
2415 .unwrap();
2416 assert!((result.rho - expected).abs() < 1e-6);
2417 assert_eq!(result.greeks().rho, result.rho);
2418 let raw = black_scholes_greeks(100.0, 0.05, 0.0, 0.2, is_call, 100.0, 1.0);
2419 assert_eq!(
2420 result,
2421 BlackScholesGreeksResult {
2422 rho: result.rho,
2423 ..raw
2424 }
2425 );
2426 }
2427
2428 #[rstest]
2429 #[case(0, 0.0, "underlying price")]
2430 #[case(0, -1.0, "underlying price")]
2431 #[case(0, f64::INFINITY, "underlying price")]
2432 #[case(0, f64::MIN_POSITIVE, "underlying price")]
2433 #[case(0, f64::MAX, "underlying price")]
2434 #[case(1, f64::NAN, "interest rate")]
2435 #[case(2, f64::INFINITY, "cost of carry")]
2436 #[case(3, 0.0, "volatility")]
2437 #[case(3, -0.2, "volatility")]
2438 #[case(3, f64::NAN, "volatility")]
2439 #[case(4, 0.0, "strike")]
2440 #[case(4, -100.0, "strike")]
2441 #[case(5, 0.0, "time to expiry")]
2442 #[case(5, -1.0, "time to expiry")]
2443 fn test_checked_greeks_reject_invalid_inputs(
2444 #[case] index: usize,
2445 #[case] value: f64,
2446 #[case] expected: &'static str,
2447 ) {
2448 let mut inputs = [100.0, 0.05, 0.03, 0.2, 110.0, 0.5];
2449 inputs[index] = value;
2450 let [s, r, b, vol, k, t] = inputs;
2451 assert_eq!(
2452 black_scholes_greeks_checked(s, r, b, vol, true, k, t, 1.0),
2453 Err(OptionValuationError::InvalidInput(expected))
2454 );
2455 }
2456
2457 #[rstest]
2458 #[case(true, 0.0)]
2459 #[case(true, 100.0)]
2460 #[case(true, 200.0)]
2461 #[case(false, 0.0)]
2462 #[case(false, 100.0)]
2463 #[case(false, 200.0)]
2464 fn test_checked_iv_rejects_zero_and_unattainable_premiums(
2465 #[case] is_call: bool,
2466 #[case] price: f64,
2467 ) {
2468 let expected = if price == 0.0 {
2469 OptionValuationError::InvalidInput("option price")
2470 } else {
2471 OptionValuationError::ImpliedVolatilityUnavailable
2472 };
2473
2474 assert_eq!(
2475 imply_vol_and_greeks_checked(100.0, 0.0, 0.0, is_call, 100.0, 1.0, price, 1.0, None),
2476 Err(expected)
2477 );
2478 }
2479
2480 #[rstest]
2481 #[case(1e-8)]
2482 #[case(0.21)]
2483 #[case(10.0)]
2484 fn test_checked_iv_refinement_falls_back_without_fabricated_volatility(#[case] seed: f64) {
2485 let raw = black_scholes_greeks(100.0, 0.05, 0.0, 0.2, true, 100.0, 1.0);
2486 let result = imply_vol_and_greeks_checked(
2487 100.0,
2488 0.05,
2489 0.0,
2490 true,
2491 100.0,
2492 1.0,
2493 raw.price,
2494 1.0,
2495 Some(seed),
2496 )
2497 .unwrap();
2498 assert!((result.vol - 0.2).abs() < 1e-5);
2499 assert!((result.price - raw.price).abs() < 1e-4);
2500 assert!((result.rho - 0.437_729_301_518).abs() < 1e-6);
2501 }
2502
2503 #[rstest]
2504 fn test_checked_greeks_rejects_nonfinite_kernel_results() {
2505 assert_eq!(
2506 black_scholes_greeks_checked(100.0, 0.05, 1000.0, 0.2, true, 100.0, 1.0, 1.0),
2507 Err(OptionValuationError::InvalidResult)
2508 );
2509 }
2510
2511 #[rstest]
2512 fn test_checked_iv_accepts_a_premium_the_fast_kernel_cannot_reprice() {
2513 let result =
2514 imply_vol_and_greeks_checked(100.0, 0.05, 0.05, true, 200.0, 1.0, 1e-10, 0.0, None)
2515 .unwrap();
2516 assert!((result.price - 1e-10).abs() < 1e-14);
2517 assert_eq!(
2518 result,
2519 black_scholes_greeks_exact(100.0, 0.05, 0.05, result.vol, true, 200.0, 1.0)
2520 );
2521 }
2522
2523 #[rstest]
2524 #[case(f64::NAN)]
2525 #[case(f64::INFINITY)]
2526 fn test_checked_greeks_rejects_nonfinite_carry_sensitivity(#[case] db_dr: f64) {
2527 assert_eq!(
2528 black_scholes_greeks_checked(100.0, 0.05, 0.0, 0.2, true, 100.0, 1.0, db_dr),
2529 Err(OptionValuationError::InvalidInput("carry rate sensitivity"))
2530 );
2531 }
2532
2533 #[rstest]
2534 fn test_checked_iv_does_not_accept_a_clamped_deep_itm_refinement() {
2535 let result = imply_vol_and_greeks_checked(
2536 200.0,
2537 0.0,
2538 0.0,
2539 true,
2540 100.0,
2541 1.0,
2542 100.000_001,
2543 1.0,
2544 Some(1e-8),
2545 )
2546 .unwrap();
2547 assert!((result.vol - 0.138_406_809_897_693_3).abs() < 1e-12);
2548 }
2549
2550 #[rstest]
2551 #[case(true, 0.460_172_162_723)]
2552 #[case(false, -0.539_827_837_277)]
2553 fn test_checked_greeks_rho_at_zero_rates(#[case] is_call: bool, #[case] expected: f64) {
2554 let result =
2555 black_scholes_greeks_checked(100.0, 0.0, 0.0, 0.2, is_call, 100.0, 1.0, 1.0).unwrap();
2556 assert!((result.rho - expected).abs() < 1e-6);
2557 }
2558
2559 #[rstest]
2560 #[case(0.0)]
2561 #[case(0.4)]
2562 #[case(1.0)]
2563 fn test_checked_greeks_rho_matches_independent_rate_bumps(#[case] db_dr: f64) {
2564 let result =
2565 black_scholes_greeks_checked(110.0, 0.05, 0.03, 0.25, true, 105.0, 0.75, db_dr)
2566 .unwrap();
2567 let epsilon = 1e-5;
2568 let high = black_scholes_greeks_exact(
2569 110.0,
2570 0.05 + epsilon,
2571 0.03 + db_dr * epsilon,
2572 0.25,
2573 true,
2574 105.0,
2575 0.75,
2576 );
2577 let low = black_scholes_greeks_exact(
2578 110.0,
2579 0.05 - epsilon,
2580 0.03 - db_dr * epsilon,
2581 0.25,
2582 true,
2583 105.0,
2584 0.75,
2585 );
2586 let reference = (high.price - low.price) / (2.0 * epsilon) * 0.01;
2587 assert!((result.rho - reference).abs() < 1e-6);
2588 }
2589
2590 #[rstest]
2591 fn test_checked_greeks_rho_is_stable_for_deep_itm_calls() {
2592 let result =
2593 black_scholes_greeks_checked(1e20, 0.05, 0.0, 0.2, true, 100.0, 1.0, 1.0).unwrap();
2594 assert!((result.rho - 0.951_229_424_500_714).abs() < 1e-12);
2595 }
2596
2597 #[rstest]
2598 fn test_checked_implied_volatility_preserves_a_tiny_premium_solution() {
2599 let vol = imply_vol_checked(100.0, 0.05, 0.05, true, 200.0, 1.0, 1e-10).unwrap();
2600 assert!((vol - 0.099_420_823_233_839_27).abs() < 1e-12);
2601 }
2602
2603 #[rstest]
2604 fn test_checked_iv_does_not_accept_the_upper_refinement_clamp() {
2605 let result = imply_vol_and_greeks_checked(
2606 100.0,
2607 0.0,
2608 0.0,
2609 true,
2610 100.0,
2611 1.0,
2612 99.99998,
2613 1.0,
2614 Some(10.0),
2615 )
2616 .unwrap();
2617 assert!((result.vol - 10.398_675_164_168_564).abs() < 1e-12);
2618 }
2619
2620 #[rstest]
2621 #[case(100.0, 0.0)]
2622 #[case(100.0, 100.0)]
2623 #[case(100.0, 200.0)]
2624 #[case(0.0, 10.0)]
2625 fn test_checked_implied_volatility_rejects_invalid_or_unavailable_solutions(
2626 #[case] spot: f64,
2627 #[case] price: f64,
2628 ) {
2629 assert_eq!(
2630 imply_vol_checked(spot, 0.0, 0.0, true, 100.0, 1.0, price),
2631 Err(OptionValuationError::ImpliedVolatilityUnavailable)
2632 );
2633 }
2634
2635 #[rstest]
2636 fn test_checked_iv_accepts_valid_index_option_premium() {
2637 let strike = 5085.0;
2638 let time = 1.0 / 365.25;
2639 let vol = 0.141;
2640 let target = black_scholes_greeks_exact(5000.0, 0.0425, 0.0, vol, true, strike, time);
2641 let result = imply_vol_and_greeks_checked(
2642 5000.0,
2643 0.0425,
2644 0.0,
2645 true,
2646 strike,
2647 time,
2648 target.price,
2649 0.0,
2650 None,
2651 )
2652 .unwrap();
2653 assert!((result.vol - vol).abs() < 1e-10);
2654 assert_eq!(
2655 result,
2656 black_scholes_greeks_exact(5000.0, 0.0425, 0.0, result.vol, true, strike, time)
2657 );
2658 }
2659
2660 #[rstest]
2661 fn test_checked_greeks_accepts_valid_tail_after_spot_shock() {
2662 let result = black_scholes_greeks_checked(
2663 4593.0,
2664 0.0425,
2665 0.0,
2666 0.14,
2667 true,
2668 5100.0,
2669 7.0 / 365.25,
2670 0.0,
2671 )
2672 .unwrap();
2673 assert_eq!(
2674 result,
2675 black_scholes_greeks_exact(4593.0, 0.0425, 0.0, 0.14, true, 5100.0, 7.0 / 365.25)
2676 );
2677 }
2678
2679 #[rstest]
2680 fn test_checked_iv_rejects_a_premium_neither_kernel_can_reprice() {
2681 let vol = imply_vol_checked(100.0, 0.0, 0.0, true, 100.0, 1.0, 1e-12).unwrap();
2682 assert!((vol - 2.506_628_274_631_000_4e-14).abs() < 1e-27);
2683 assert_eq!(
2684 imply_vol_and_greeks_checked(100.0, 0.0, 0.0, true, 100.0, 1.0, 1e-12, 0.0, None),
2685 Err(OptionValuationError::InvalidResult)
2686 );
2687 }
2688}