1use std::{
19 fmt::Display,
20 ops::{Add, Deref, Mul},
21};
22
23use implied_vol::{DefaultSpecialFn, ImpliedBlackVolatility, SpecialFn};
24use nautilus_core::{UnixNanos, datetime::unix_nanos_to_iso8601, math::quadratic_interpolation};
25
26use crate::{
27 data::{
28 HasTsInit,
29 black_scholes::{compute_greeks, compute_iv_and_greeks},
30 },
31 identifiers::InstrumentId,
32};
33
34const FRAC_SQRT_2_PI: f64 = f64::from_bits(0x3fd9_8845_33d4_3651);
35const THETA_DAILY_FACTOR: f64 = 1.0 / 365.25;
37const VEGA_PERCENT_FACTOR: f64 = 0.01;
39
40#[repr(C)]
43#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Default)]
44#[cfg_attr(
45 feature = "python",
46 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
47)]
48#[cfg_attr(
49 feature = "python",
50 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
51)]
52pub struct OptionGreekValues {
53 pub delta: f64,
54 pub gamma: f64,
55 pub vega: f64,
56 pub theta: f64,
57 pub rho: f64,
58}
59
60impl Add for OptionGreekValues {
61 type Output = Self;
62
63 fn add(self, rhs: Self) -> Self {
64 Self {
65 delta: self.delta + rhs.delta,
66 gamma: self.gamma + rhs.gamma,
67 vega: self.vega + rhs.vega,
68 theta: self.theta + rhs.theta,
69 rho: self.rho + rhs.rho,
70 }
71 }
72}
73
74impl Mul<f64> for OptionGreekValues {
75 type Output = Self;
76
77 fn mul(self, scalar: f64) -> Self {
78 Self {
79 delta: self.delta * scalar,
80 gamma: self.gamma * scalar,
81 vega: self.vega * scalar,
82 theta: self.theta * scalar,
83 rho: self.rho * scalar,
84 }
85 }
86}
87
88impl Mul<OptionGreekValues> for f64 {
89 type Output = OptionGreekValues;
90
91 fn mul(self, greeks: OptionGreekValues) -> OptionGreekValues {
92 greeks * self
93 }
94}
95
96impl Display for OptionGreekValues {
97 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98 write!(
99 f,
100 "OptionGreekValues(delta={:.4}, gamma={:.4}, vega={:.4}, theta={:.4}, rho={:.4})",
101 self.delta, self.gamma, self.vega, self.theta, self.rho
102 )
103 }
104}
105
106pub trait HasGreeks {
108 fn greeks(&self) -> OptionGreekValues;
109}
110
111#[inline(always)]
112fn norm_pdf(x: f64) -> f64 {
113 FRAC_SQRT_2_PI * (-0.5 * x * x).exp()
114}
115
116#[repr(C)]
119#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
120#[cfg_attr(
121 feature = "python",
122 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
123)]
124#[cfg_attr(
125 feature = "python",
126 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
127)]
128pub struct BlackScholesGreeksResult {
129 pub price: f64,
130 pub vol: f64,
131 pub delta: f64,
132 pub gamma: f64,
133 pub vega: f64,
134 pub theta: f64,
135 pub itm_prob: f64,
136}
137
138#[must_use]
142pub fn black_scholes_greeks_exact(
143 s: f64,
144 r: f64,
145 b: f64,
146 vol: f64,
147 is_call: bool,
148 k: f64,
149 t: f64,
150) -> BlackScholesGreeksResult {
151 let phi = if is_call { 1.0 } else { -1.0 };
152 let sqrt_t = t.sqrt();
153 let scaled_vol = vol * sqrt_t;
154
155 let d1 = ((s / k).ln() + (b + 0.5 * vol.powi(2)) * t) / scaled_vol;
157 let d2 = d1 - scaled_vol;
158
159 let cdf_phi_d1 = DefaultSpecialFn::norm_cdf(phi * d1);
161 let cdf_phi_d2 = DefaultSpecialFn::norm_cdf(phi * d2);
162 let pdf_d1 = norm_pdf(d1);
163
164 let df_b = ((b - r) * t).exp();
166 let df_r = (-r * t).exp();
167
168 let price = phi * (s * df_b * cdf_phi_d1 - k * df_r * cdf_phi_d2);
170 let delta = phi * df_b * cdf_phi_d1;
171 let gamma = (df_b * pdf_d1) / (s * scaled_vol);
172 let vega = s * df_b * sqrt_t * pdf_d1 * VEGA_PERCENT_FACTOR;
173
174 let theta_v = -(s * df_b * pdf_d1 * vol) / (2.0 * sqrt_t);
176 let theta_b = -phi * (b - r) * s * df_b * cdf_phi_d1;
177 let theta_r = -phi * r * k * df_r * cdf_phi_d2;
178 let theta = (theta_v + theta_b + theta_r) * THETA_DAILY_FACTOR;
179
180 BlackScholesGreeksResult {
181 price,
182 vol,
183 delta,
184 gamma,
185 vega,
186 theta,
187 itm_prob: cdf_phi_d2,
188 }
189}
190
191#[must_use]
192pub fn imply_vol(s: f64, r: f64, b: f64, is_call: bool, k: f64, t: f64, price: f64) -> f64 {
193 let forward = s * (b * t).exp();
194 let forward_price = price * (r * t).exp();
195
196 ImpliedBlackVolatility::builder()
197 .option_price(forward_price)
198 .forward(forward)
199 .strike(k)
200 .expiry(t)
201 .is_call(is_call)
202 .build_unchecked()
203 .calculate::<DefaultSpecialFn>()
204 .unwrap_or(0.0)
205}
206
207#[must_use]
210pub fn black_scholes_greeks(
211 s: f64,
212 r: f64,
213 b: f64,
214 vol: f64,
215 is_call: bool,
216 k: f64,
217 t: f64,
218) -> BlackScholesGreeksResult {
219 let greeks = compute_greeks::<f32>(
221 s as f32, k as f32, t as f32, r as f32, b as f32, vol as f32, is_call,
222 );
223
224 BlackScholesGreeksResult {
225 price: f64::from(greeks.price),
226 vol,
227 delta: f64::from(greeks.delta),
228 gamma: f64::from(greeks.gamma),
229 vega: f64::from(greeks.vega) * VEGA_PERCENT_FACTOR,
230 theta: f64::from(greeks.theta) * THETA_DAILY_FACTOR,
231 itm_prob: f64::from(greeks.itm_prob),
232 }
233}
234
235#[must_use]
238pub fn imply_vol_and_greeks(
239 s: f64,
240 r: f64,
241 b: f64,
242 is_call: bool,
243 k: f64,
244 t: f64,
245 price: f64,
246) -> BlackScholesGreeksResult {
247 let vol = imply_vol(s, r, b, is_call, k, t, price);
248 let safe_vol = if vol < 1e-8 { 1e-8 } else { vol };
252 black_scholes_greeks(s, r, b, safe_vol, is_call, k, t)
253}
254
255#[expect(clippy::too_many_arguments)]
259#[must_use]
260pub fn refine_vol_and_greeks(
261 s: f64,
262 r: f64,
263 b: f64,
264 is_call: bool,
265 k: f64,
266 t: f64,
267 target_price: f64,
268 initial_vol: f64,
269) -> BlackScholesGreeksResult {
270 let greeks = compute_iv_and_greeks::<f32>(
272 target_price as f32,
273 s as f32,
274 k as f32,
275 t as f32,
276 r as f32,
277 b as f32,
278 is_call,
279 initial_vol as f32,
280 );
281
282 BlackScholesGreeksResult {
283 price: f64::from(greeks.price),
284 vol: f64::from(greeks.vol),
285 delta: f64::from(greeks.delta),
286 gamma: f64::from(greeks.gamma),
287 vega: f64::from(greeks.vega) * VEGA_PERCENT_FACTOR,
288 theta: f64::from(greeks.theta) * THETA_DAILY_FACTOR,
289 itm_prob: f64::from(greeks.itm_prob),
290 }
291}
292
293#[derive(Debug, Clone)]
294#[cfg_attr(
295 feature = "python",
296 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
297)]
298#[cfg_attr(
299 feature = "python",
300 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
301)]
302pub struct GreeksData {
303 pub ts_init: UnixNanos,
304 pub ts_event: UnixNanos,
305 pub instrument_id: InstrumentId,
306 pub is_call: bool,
307 pub strike: f64,
308 pub expiry: i32,
309 pub expiry_in_days: i32,
310 pub expiry_in_years: f64,
311 pub multiplier: f64,
312 pub quantity: f64,
313 pub underlying_price: f64,
314 pub interest_rate: f64,
315 pub cost_of_carry: f64,
316 pub vol: f64,
317 pub pnl: f64,
318 pub price: f64,
319 pub greeks: OptionGreekValues,
321 pub itm_prob: f64,
323}
324
325impl GreeksData {
326 #[expect(clippy::too_many_arguments)]
327 #[must_use]
328 pub fn new(
329 ts_init: UnixNanos,
330 ts_event: UnixNanos,
331 instrument_id: InstrumentId,
332 is_call: bool,
333 strike: f64,
334 expiry: i32,
335 expiry_in_days: i32,
336 expiry_in_years: f64,
337 multiplier: f64,
338 quantity: f64,
339 underlying_price: f64,
340 interest_rate: f64,
341 cost_of_carry: f64,
342 vol: f64,
343 pnl: f64,
344 price: f64,
345 greeks: OptionGreekValues,
346 itm_prob: f64,
347 ) -> Self {
348 Self {
349 ts_init,
350 ts_event,
351 instrument_id,
352 is_call,
353 strike,
354 expiry,
355 expiry_in_days,
356 expiry_in_years,
357 multiplier,
358 quantity,
359 underlying_price,
360 interest_rate,
361 cost_of_carry,
362 vol,
363 pnl,
364 price,
365 greeks,
366 itm_prob,
367 }
368 }
369
370 #[must_use]
371 pub fn from_delta(
372 instrument_id: InstrumentId,
373 delta: f64,
374 multiplier: f64,
375 ts_event: UnixNanos,
376 ) -> Self {
377 Self {
378 ts_init: ts_event,
379 ts_event,
380 instrument_id,
381 is_call: true,
382 strike: 0.0,
383 expiry: 0,
384 expiry_in_days: 0,
385 expiry_in_years: 0.0,
386 multiplier,
387 quantity: 1.0,
388 underlying_price: 0.0,
389 interest_rate: 0.0,
390 cost_of_carry: 0.0,
391 vol: 0.0,
392 pnl: 0.0,
393 price: 0.0,
394 greeks: OptionGreekValues {
395 delta,
396 ..Default::default()
397 },
398 itm_prob: 0.0,
399 }
400 }
401}
402
403impl Deref for GreeksData {
404 type Target = OptionGreekValues;
405 fn deref(&self) -> &Self::Target {
406 &self.greeks
407 }
408}
409
410impl HasGreeks for GreeksData {
411 fn greeks(&self) -> OptionGreekValues {
412 self.greeks
413 }
414}
415
416impl Default for GreeksData {
417 fn default() -> Self {
418 Self {
419 ts_init: UnixNanos::default(),
420 ts_event: UnixNanos::default(),
421 instrument_id: InstrumentId::from("ES.GLBX"),
422 is_call: true,
423 strike: 0.0,
424 expiry: 0,
425 expiry_in_days: 0,
426 expiry_in_years: 0.0,
427 multiplier: 0.0,
428 quantity: 0.0,
429 underlying_price: 0.0,
430 interest_rate: 0.0,
431 cost_of_carry: 0.0,
432 vol: 0.0,
433 pnl: 0.0,
434 price: 0.0,
435 greeks: OptionGreekValues::default(),
436 itm_prob: 0.0,
437 }
438 }
439}
440
441impl Display for GreeksData {
442 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
443 write!(
444 f,
445 "GreeksData(instrument_id={}, expiry={}, itm_prob={:.2}%, vol={:.2}%, pnl={:.2}, price={:.2}, delta={:.2}, gamma={:.2}, vega={:.2}, theta={:.2}, quantity={}, ts_init={})",
446 self.instrument_id,
447 self.expiry,
448 self.itm_prob * 100.0,
449 self.vol * 100.0,
450 self.pnl,
451 self.price,
452 self.greeks.delta,
453 self.greeks.gamma,
454 self.greeks.vega,
455 self.greeks.theta,
456 self.quantity,
457 unix_nanos_to_iso8601(self.ts_init)
458 )
459 }
460}
461
462impl Mul<&GreeksData> for f64 {
464 type Output = GreeksData;
465
466 fn mul(self, g: &GreeksData) -> GreeksData {
467 GreeksData {
468 ts_init: g.ts_init,
469 ts_event: g.ts_event,
470 instrument_id: g.instrument_id,
471 is_call: g.is_call,
472 strike: g.strike,
473 expiry: g.expiry,
474 expiry_in_days: g.expiry_in_days,
475 expiry_in_years: g.expiry_in_years,
476 multiplier: g.multiplier,
477 quantity: g.quantity,
478 underlying_price: g.underlying_price,
479 interest_rate: g.interest_rate,
480 cost_of_carry: g.cost_of_carry,
481 vol: g.vol,
482 pnl: self * g.pnl,
483 price: self * g.price,
484 greeks: g.greeks * self,
485 itm_prob: g.itm_prob,
486 }
487 }
488}
489
490impl HasTsInit for GreeksData {
491 fn ts_init(&self) -> UnixNanos {
492 self.ts_init
493 }
494}
495
496#[derive(Debug, Clone)]
497#[cfg_attr(
498 feature = "python",
499 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
500)]
501#[cfg_attr(
502 feature = "python",
503 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
504)]
505pub struct PortfolioGreeks {
506 pub ts_init: UnixNanos,
507 pub ts_event: UnixNanos,
508 pub pnl: f64,
509 pub price: f64,
510 pub greeks: OptionGreekValues,
511}
512
513impl PortfolioGreeks {
514 #[expect(clippy::too_many_arguments)]
515 #[must_use]
516 pub fn new(
517 ts_init: UnixNanos,
518 ts_event: UnixNanos,
519 pnl: f64,
520 price: f64,
521 delta: f64,
522 gamma: f64,
523 vega: f64,
524 theta: f64,
525 ) -> Self {
526 Self {
527 ts_init,
528 ts_event,
529 pnl,
530 price,
531 greeks: OptionGreekValues {
532 delta,
533 gamma,
534 vega,
535 theta,
536 rho: 0.0,
537 },
538 }
539 }
540}
541
542impl Deref for PortfolioGreeks {
543 type Target = OptionGreekValues;
544 fn deref(&self) -> &Self::Target {
545 &self.greeks
546 }
547}
548
549impl Default for PortfolioGreeks {
550 fn default() -> Self {
551 Self {
552 ts_init: UnixNanos::default(),
553 ts_event: UnixNanos::default(),
554 pnl: 0.0,
555 price: 0.0,
556 greeks: OptionGreekValues::default(),
557 }
558 }
559}
560
561impl Display for PortfolioGreeks {
562 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
563 write!(
564 f,
565 "PortfolioGreeks(pnl={:.2}, price={:.2}, delta={:.2}, gamma={:.2}, vega={:.2}, theta={:.2}, ts_event={}, ts_init={})",
566 self.pnl,
567 self.price,
568 self.greeks.delta,
569 self.greeks.gamma,
570 self.greeks.vega,
571 self.greeks.theta,
572 unix_nanos_to_iso8601(self.ts_event),
573 unix_nanos_to_iso8601(self.ts_init)
574 )
575 }
576}
577
578impl Add for PortfolioGreeks {
579 type Output = Self;
580
581 fn add(self, other: Self) -> Self {
582 Self {
583 ts_init: self.ts_init,
584 ts_event: self.ts_event,
585 pnl: self.pnl + other.pnl,
586 price: self.price + other.price,
587 greeks: self.greeks + other.greeks,
588 }
589 }
590}
591
592impl From<GreeksData> for PortfolioGreeks {
593 fn from(g: GreeksData) -> Self {
594 Self {
595 ts_init: g.ts_init,
596 ts_event: g.ts_event,
597 pnl: g.pnl,
598 price: g.price,
599 greeks: g.greeks,
600 }
601 }
602}
603
604impl HasTsInit for PortfolioGreeks {
605 fn ts_init(&self) -> UnixNanos {
606 self.ts_init
607 }
608}
609
610impl HasGreeks for PortfolioGreeks {
611 fn greeks(&self) -> OptionGreekValues {
612 self.greeks
613 }
614}
615
616impl HasGreeks for BlackScholesGreeksResult {
617 fn greeks(&self) -> OptionGreekValues {
618 OptionGreekValues {
619 delta: self.delta,
620 gamma: self.gamma,
621 vega: self.vega,
622 theta: self.theta,
623 rho: 0.0,
624 }
625 }
626}
627
628#[derive(Debug, Clone)]
629pub struct YieldCurveData {
630 pub ts_init: UnixNanos,
631 pub ts_event: UnixNanos,
632 pub curve_name: String,
633 pub tenors: Vec<f64>,
634 pub interest_rates: Vec<f64>,
635}
636
637impl YieldCurveData {
638 #[must_use]
639 pub fn new(
640 ts_init: UnixNanos,
641 ts_event: UnixNanos,
642 curve_name: String,
643 tenors: Vec<f64>,
644 interest_rates: Vec<f64>,
645 ) -> Self {
646 Self {
647 ts_init,
648 ts_event,
649 curve_name,
650 tenors,
651 interest_rates,
652 }
653 }
654
655 #[must_use]
657 pub fn get_rate(&self, expiry_in_years: f64) -> f64 {
658 if self.interest_rates.len() == 1 {
659 return self.interest_rates[0];
660 }
661
662 quadratic_interpolation(expiry_in_years, &self.tenors, &self.interest_rates)
663 }
664}
665
666impl Display for YieldCurveData {
667 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
668 write!(
669 f,
670 "InterestRateCurve(curve_name={}, ts_event={}, ts_init={})",
671 self.curve_name,
672 unix_nanos_to_iso8601(self.ts_event),
673 unix_nanos_to_iso8601(self.ts_init)
674 )
675 }
676}
677
678impl HasTsInit for YieldCurveData {
679 fn ts_init(&self) -> UnixNanos {
680 self.ts_init
681 }
682}
683
684impl Default for YieldCurveData {
685 fn default() -> Self {
686 Self {
687 ts_init: UnixNanos::default(),
688 ts_event: UnixNanos::default(),
689 curve_name: "USD".to_string(),
690 tenors: vec![0.5, 1.0, 1.5, 2.0, 2.5],
691 interest_rates: vec![0.04, 0.04, 0.04, 0.04, 0.04],
692 }
693 }
694}
695
696#[cfg(test)]
697mod tests {
698 use rstest::rstest;
699
700 use super::*;
701 use crate::identifiers::InstrumentId;
702
703 fn create_test_greeks_data() -> GreeksData {
704 GreeksData::new(
705 UnixNanos::from(1_000_000_000),
706 UnixNanos::from(1_500_000_000),
707 InstrumentId::from("SPY240315C00500000.OPRA"),
708 true,
709 500.0,
710 20_240_315,
711 91, 0.25,
713 100.0,
714 1.0,
715 520.0,
716 0.05,
717 0.05,
718 0.2,
719 250.0,
720 25.5,
721 OptionGreekValues {
722 delta: 0.65,
723 gamma: 0.003,
724 vega: 15.2,
725 theta: -0.08,
726 rho: 0.0,
727 },
728 0.75,
729 )
730 }
731
732 fn create_test_portfolio_greeks() -> PortfolioGreeks {
733 PortfolioGreeks::new(
734 UnixNanos::from(1_000_000_000),
735 UnixNanos::from(1_500_000_000),
736 1500.0,
737 125.5,
738 2.15,
739 0.008,
740 42.7,
741 -2.3,
742 )
743 }
744
745 fn create_test_yield_curve() -> YieldCurveData {
746 YieldCurveData::new(
747 UnixNanos::from(1_000_000_000),
748 UnixNanos::from(1_500_000_000),
749 "USD".to_string(),
750 vec![0.25, 0.5, 1.0, 2.0, 5.0],
751 vec![0.025, 0.03, 0.035, 0.04, 0.045],
752 )
753 }
754
755 #[rstest]
756 fn test_black_scholes_greeks_result_creation() {
757 let result = BlackScholesGreeksResult {
758 price: 25.5,
759 vol: 0.2,
760 delta: 0.65,
761 gamma: 0.003,
762 vega: 15.2,
763 theta: -0.08,
764 itm_prob: 0.55,
765 };
766
767 assert_eq!(result.price, 25.5);
768 assert_eq!(result.delta, 0.65);
769 assert_eq!(result.gamma, 0.003);
770 assert_eq!(result.vega, 15.2);
771 assert_eq!(result.theta, -0.08);
772 assert_eq!(result.itm_prob, 0.55);
773 }
774
775 #[rstest]
776 fn test_black_scholes_greeks_result_clone_and_copy() {
777 let result1 = BlackScholesGreeksResult {
778 price: 25.5,
779 vol: 0.2,
780 delta: 0.65,
781 gamma: 0.003,
782 vega: 15.2,
783 theta: -0.08,
784 itm_prob: 0.55,
785 };
786 let result2 = result1;
787 let result3 = result1;
788
789 assert_eq!(result1, result2);
790 assert_eq!(result1, result3);
791 }
792
793 #[rstest]
794 fn test_black_scholes_greeks_result_debug() {
795 let result = BlackScholesGreeksResult {
796 price: 25.5,
797 vol: 0.2,
798 delta: 0.65,
799 gamma: 0.003,
800 vega: 15.2,
801 theta: -0.08,
802 itm_prob: 0.55,
803 };
804 let debug_str = format!("{result:?}");
805
806 assert!(debug_str.contains("BlackScholesGreeksResult"));
807 assert!(debug_str.contains("25.5"));
808 assert!(debug_str.contains("0.65"));
809 }
810
811 #[rstest]
812 fn test_imply_vol_and_greeks_result_creation() {
813 let result = BlackScholesGreeksResult {
814 price: 25.5,
815 vol: 0.2,
816 delta: 0.65,
817 gamma: 0.003,
818 vega: 15.2,
819 theta: -0.08,
820 itm_prob: 0.55,
821 };
822
823 assert_eq!(result.vol, 0.2);
824 assert_eq!(result.price, 25.5);
825 assert_eq!(result.delta, 0.65);
826 assert_eq!(result.gamma, 0.003);
827 assert_eq!(result.vega, 15.2);
828 assert_eq!(result.theta, -0.08);
829 }
830
831 #[rstest]
832 fn test_black_scholes_greeks_basic_call() {
833 let s = 100.0;
834 let r = 0.05;
835 let b = 0.05;
836 let vol = 0.2;
837 let is_call = true;
838 let k = 100.0;
839 let t = 1.0;
840
841 let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t);
842
843 assert!(greeks.price > 0.0);
844 assert!(greeks.delta > 0.0 && greeks.delta < 1.0);
845 assert!(greeks.gamma > 0.0);
846 assert!(greeks.vega > 0.0);
847 assert!(greeks.theta < 0.0); }
849
850 #[rstest]
851 fn test_black_scholes_greeks_basic_put() {
852 let s = 100.0;
853 let r = 0.05;
854 let b = 0.05;
855 let vol = 0.2;
856 let is_call = false;
857 let k = 100.0;
858 let t = 1.0;
859
860 let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t);
861
862 assert!(
863 greeks.price > 0.0,
864 "Put option price should be positive, was: {}",
865 greeks.price
866 );
867 assert!(greeks.delta < 0.0 && greeks.delta > -1.0);
868 assert!(greeks.gamma > 0.0);
869 assert!(greeks.vega > 0.0);
870 assert!(greeks.theta < 0.0); }
872
873 #[rstest]
874 fn test_black_scholes_greeks_deep_itm_call() {
875 let s = 150.0;
876 let r = 0.05;
877 let b = 0.05;
878 let vol = 0.2;
879 let is_call = true;
880 let k = 100.0;
881 let t = 1.0;
882
883 let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t);
884
885 assert!(greeks.delta > 0.9); assert!(greeks.gamma > 0.0 && greeks.gamma < 0.01); }
888
889 #[rstest]
890 fn test_black_scholes_greeks_deep_otm_call() {
891 let s = 50.0;
892 let r = 0.05;
893 let b = 0.05;
894 let vol = 0.2;
895 let is_call = true;
896 let k = 100.0;
897 let t = 1.0;
898
899 let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t);
900
901 assert!(greeks.delta < 0.1); assert!(greeks.gamma > 0.0 && greeks.gamma < 0.01); }
904
905 #[rstest]
906 fn test_black_scholes_greeks_zero_time() {
907 let s = 100.0;
908 let r = 0.05;
909 let b = 0.05;
910 let vol = 0.2;
911 let is_call = true;
912 let k = 100.0;
913 let t = 0.0001; let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t);
916
917 assert!(greeks.price >= 0.0);
918 assert!(greeks.theta.is_finite());
919 }
920
921 #[rstest]
922 fn test_imply_vol_basic() {
923 let s = 100.0;
924 let r = 0.05;
925 let b = 0.05;
926 let vol = 0.2;
927 let is_call = true;
928 let k = 100.0;
929 let t = 1.0;
930
931 let theoretical_price = black_scholes_greeks(s, r, b, vol, is_call, k, t).price;
932 let implied_vol = imply_vol(s, r, b, is_call, k, t, theoretical_price);
933
934 let tolerance = 1e-4;
936 assert!(
937 (implied_vol - vol).abs() < tolerance,
938 "Implied vol difference exceeds tolerance: {implied_vol} vs {vol}"
939 );
940 }
941
942 #[rstest]
949 fn test_greeks_data_new() {
950 let greeks = create_test_greeks_data();
951
952 assert_eq!(greeks.ts_init, UnixNanos::from(1_000_000_000));
953 assert_eq!(greeks.ts_event, UnixNanos::from(1_500_000_000));
954 assert_eq!(
955 greeks.instrument_id,
956 InstrumentId::from("SPY240315C00500000.OPRA")
957 );
958 assert!(greeks.is_call);
959 assert_eq!(greeks.strike, 500.0);
960 assert_eq!(greeks.expiry, 20_240_315);
961 assert_eq!(greeks.expiry_in_years, 0.25);
962 assert_eq!(greeks.multiplier, 100.0);
963 assert_eq!(greeks.quantity, 1.0);
964 assert_eq!(greeks.underlying_price, 520.0);
965 assert_eq!(greeks.interest_rate, 0.05);
966 assert_eq!(greeks.cost_of_carry, 0.05);
967 assert_eq!(greeks.vol, 0.2);
968 assert_eq!(greeks.pnl, 250.0);
969 assert_eq!(greeks.price, 25.5);
970 assert_eq!(greeks.delta, 0.65);
971 assert_eq!(greeks.gamma, 0.003);
972 assert_eq!(greeks.vega, 15.2);
973 assert_eq!(greeks.theta, -0.08);
974 assert_eq!(greeks.itm_prob, 0.75);
975 }
976
977 #[rstest]
978 fn test_greeks_data_from_delta() {
979 let delta = 0.5;
980 let multiplier = 100.0;
981 let ts_event = UnixNanos::from(2_000_000_000);
982 let instrument_id = InstrumentId::from("AAPL240315C00180000.OPRA");
983
984 let greeks = GreeksData::from_delta(instrument_id, delta, multiplier, ts_event);
985
986 assert_eq!(greeks.ts_init, ts_event);
987 assert_eq!(greeks.ts_event, ts_event);
988 assert_eq!(greeks.instrument_id, instrument_id);
989 assert!(greeks.is_call);
990 assert_eq!(greeks.delta, delta);
991 assert_eq!(greeks.multiplier, multiplier);
992 assert_eq!(greeks.quantity, 1.0);
993
994 assert_eq!(greeks.strike, 0.0);
996 assert_eq!(greeks.expiry, 0);
997 assert_eq!(greeks.price, 0.0);
998 assert_eq!(greeks.gamma, 0.0);
999 assert_eq!(greeks.vega, 0.0);
1000 assert_eq!(greeks.theta, 0.0);
1001 }
1002
1003 #[rstest]
1004 fn test_greeks_data_default() {
1005 let greeks = GreeksData::default();
1006
1007 assert_eq!(greeks.ts_init, UnixNanos::default());
1008 assert_eq!(greeks.ts_event, UnixNanos::default());
1009 assert_eq!(greeks.instrument_id, InstrumentId::from("ES.GLBX"));
1010 assert!(greeks.is_call);
1011 assert_eq!(greeks.strike, 0.0);
1012 assert_eq!(greeks.expiry, 0);
1013 assert_eq!(greeks.multiplier, 0.0);
1014 assert_eq!(greeks.quantity, 0.0);
1015 assert_eq!(greeks.delta, 0.0);
1016 assert_eq!(greeks.gamma, 0.0);
1017 assert_eq!(greeks.vega, 0.0);
1018 assert_eq!(greeks.theta, 0.0);
1019 }
1020
1021 #[rstest]
1022 fn test_greeks_data_display() {
1023 let greeks = create_test_greeks_data();
1024 let display_str = format!("{greeks}");
1025
1026 assert!(display_str.contains("GreeksData"));
1027 assert!(display_str.contains("SPY240315C00500000.OPRA"));
1028 assert!(display_str.contains("20240315"));
1029 assert!(display_str.contains("75.00%")); assert!(display_str.contains("20.00%")); assert!(display_str.contains("250.00")); assert!(display_str.contains("25.50")); assert!(display_str.contains("0.65")); }
1035
1036 #[rstest]
1037 fn test_greeks_data_multiplication() {
1038 let greeks = create_test_greeks_data();
1039 let quantity = 5.0;
1040 let scaled_greeks = quantity * &greeks;
1041
1042 assert_eq!(scaled_greeks.ts_init, greeks.ts_init);
1043 assert_eq!(scaled_greeks.ts_event, greeks.ts_event);
1044 assert_eq!(scaled_greeks.instrument_id, greeks.instrument_id);
1045 assert_eq!(scaled_greeks.is_call, greeks.is_call);
1046 assert_eq!(scaled_greeks.strike, greeks.strike);
1047 assert_eq!(scaled_greeks.expiry, greeks.expiry);
1048 assert_eq!(scaled_greeks.multiplier, greeks.multiplier);
1049 assert_eq!(scaled_greeks.quantity, greeks.quantity);
1050 assert_eq!(scaled_greeks.vol, greeks.vol);
1051 assert_eq!(scaled_greeks.itm_prob, greeks.itm_prob);
1052
1053 assert_eq!(scaled_greeks.pnl, quantity * greeks.pnl);
1055 assert_eq!(scaled_greeks.price, quantity * greeks.price);
1056 assert_eq!(scaled_greeks.delta, quantity * greeks.delta);
1057 assert_eq!(scaled_greeks.gamma, quantity * greeks.gamma);
1058 assert_eq!(scaled_greeks.vega, quantity * greeks.vega);
1059 assert_eq!(scaled_greeks.theta, quantity * greeks.theta);
1060 }
1061
1062 #[rstest]
1063 fn test_greeks_data_has_ts_init() {
1064 let greeks = create_test_greeks_data();
1065 assert_eq!(greeks.ts_init(), UnixNanos::from(1_000_000_000));
1066 }
1067
1068 #[rstest]
1069 fn test_greeks_data_clone() {
1070 let greeks1 = create_test_greeks_data();
1071 let greeks2 = greeks1.clone();
1072
1073 assert_eq!(greeks1.ts_init, greeks2.ts_init);
1074 assert_eq!(greeks1.instrument_id, greeks2.instrument_id);
1075 assert_eq!(greeks1.delta, greeks2.delta);
1076 assert_eq!(greeks1.gamma, greeks2.gamma);
1077 }
1078
1079 #[rstest]
1080 fn test_portfolio_greeks_new() {
1081 let portfolio_greeks = create_test_portfolio_greeks();
1082
1083 assert_eq!(portfolio_greeks.ts_init, UnixNanos::from(1_000_000_000));
1084 assert_eq!(portfolio_greeks.ts_event, UnixNanos::from(1_500_000_000));
1085 assert_eq!(portfolio_greeks.pnl, 1500.0);
1086 assert_eq!(portfolio_greeks.price, 125.5);
1087 assert_eq!(portfolio_greeks.delta, 2.15);
1088 assert_eq!(portfolio_greeks.gamma, 0.008);
1089 assert_eq!(portfolio_greeks.vega, 42.7);
1090 assert_eq!(portfolio_greeks.theta, -2.3);
1091 }
1092
1093 #[rstest]
1094 fn test_portfolio_greeks_default() {
1095 let portfolio_greeks = PortfolioGreeks::default();
1096
1097 assert_eq!(portfolio_greeks.ts_init, UnixNanos::default());
1098 assert_eq!(portfolio_greeks.ts_event, UnixNanos::default());
1099 assert_eq!(portfolio_greeks.pnl, 0.0);
1100 assert_eq!(portfolio_greeks.price, 0.0);
1101 assert_eq!(portfolio_greeks.delta, 0.0);
1102 assert_eq!(portfolio_greeks.gamma, 0.0);
1103 assert_eq!(portfolio_greeks.vega, 0.0);
1104 assert_eq!(portfolio_greeks.theta, 0.0);
1105 }
1106
1107 #[rstest]
1108 fn test_portfolio_greeks_display() {
1109 let portfolio_greeks = create_test_portfolio_greeks();
1110 let display_str = format!("{portfolio_greeks}");
1111
1112 assert!(display_str.contains("PortfolioGreeks"));
1113 assert!(display_str.contains("1500.00")); assert!(display_str.contains("125.50")); assert!(display_str.contains("2.15")); assert!(display_str.contains("0.01")); assert!(display_str.contains("42.70")); assert!(display_str.contains("-2.30")); }
1120
1121 #[rstest]
1122 fn test_portfolio_greeks_addition() {
1123 let greeks1 = PortfolioGreeks::new(
1124 UnixNanos::from(1_000_000_000),
1125 UnixNanos::from(1_500_000_000),
1126 100.0,
1127 50.0,
1128 1.0,
1129 0.005,
1130 20.0,
1131 -1.0,
1132 );
1133 let greeks2 = PortfolioGreeks::new(
1134 UnixNanos::from(2_000_000_000),
1135 UnixNanos::from(2_500_000_000),
1136 200.0,
1137 75.0,
1138 1.5,
1139 0.003,
1140 25.0,
1141 -1.5,
1142 );
1143
1144 let result = greeks1 + greeks2;
1145
1146 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);
1149 assert_eq!(result.price, 125.0);
1150 assert_eq!(result.delta, 2.5);
1151 assert_eq!(result.gamma, 0.008);
1152 assert_eq!(result.vega, 45.0);
1153 assert_eq!(result.theta, -2.5);
1154 }
1155
1156 #[rstest]
1157 fn test_portfolio_greeks_from_greeks_data() {
1158 let greeks_data = create_test_greeks_data();
1159 let portfolio_greeks: PortfolioGreeks = greeks_data.clone().into();
1160
1161 assert_eq!(portfolio_greeks.ts_init, greeks_data.ts_init);
1162 assert_eq!(portfolio_greeks.ts_event, greeks_data.ts_event);
1163 assert_eq!(portfolio_greeks.pnl, greeks_data.pnl);
1164 assert_eq!(portfolio_greeks.price, greeks_data.price);
1165 assert_eq!(portfolio_greeks.delta, greeks_data.delta);
1166 assert_eq!(portfolio_greeks.gamma, greeks_data.gamma);
1167 assert_eq!(portfolio_greeks.vega, greeks_data.vega);
1168 assert_eq!(portfolio_greeks.theta, greeks_data.theta);
1169 }
1170
1171 #[rstest]
1172 fn test_portfolio_greeks_has_ts_init() {
1173 let portfolio_greeks = create_test_portfolio_greeks();
1174 assert_eq!(portfolio_greeks.ts_init(), UnixNanos::from(1_000_000_000));
1175 }
1176
1177 #[rstest]
1178 fn test_yield_curve_data_new() {
1179 let curve = create_test_yield_curve();
1180
1181 assert_eq!(curve.ts_init, UnixNanos::from(1_000_000_000));
1182 assert_eq!(curve.ts_event, UnixNanos::from(1_500_000_000));
1183 assert_eq!(curve.curve_name, "USD");
1184 assert_eq!(curve.tenors, vec![0.25, 0.5, 1.0, 2.0, 5.0]);
1185 assert_eq!(curve.interest_rates, vec![0.025, 0.03, 0.035, 0.04, 0.045]);
1186 }
1187
1188 #[rstest]
1189 fn test_yield_curve_data_default() {
1190 let curve = YieldCurveData::default();
1191
1192 assert_eq!(curve.ts_init, UnixNanos::default());
1193 assert_eq!(curve.ts_event, UnixNanos::default());
1194 assert_eq!(curve.curve_name, "USD");
1195 assert_eq!(curve.tenors, vec![0.5, 1.0, 1.5, 2.0, 2.5]);
1196 assert_eq!(curve.interest_rates, vec![0.04, 0.04, 0.04, 0.04, 0.04]);
1197 }
1198
1199 #[rstest]
1200 fn test_yield_curve_data_get_rate_single_point() {
1201 let curve = YieldCurveData::new(
1202 UnixNanos::default(),
1203 UnixNanos::default(),
1204 "USD".to_string(),
1205 vec![1.0],
1206 vec![0.05],
1207 );
1208
1209 assert_eq!(curve.get_rate(0.5), 0.05);
1210 assert_eq!(curve.get_rate(1.0), 0.05);
1211 assert_eq!(curve.get_rate(2.0), 0.05);
1212 }
1213
1214 #[rstest]
1215 fn test_yield_curve_data_get_rate_interpolation() {
1216 let curve = create_test_yield_curve();
1217
1218 assert_eq!(curve.get_rate(0.25), 0.025);
1220 assert_eq!(curve.get_rate(1.0), 0.035);
1221 assert_eq!(curve.get_rate(5.0), 0.045);
1222
1223 let rate_0_75 = curve.get_rate(0.75);
1225 assert!(rate_0_75 > 0.025 && rate_0_75 < 0.045);
1226 }
1227
1228 #[rstest]
1229 fn test_yield_curve_data_display() {
1230 let curve = create_test_yield_curve();
1231 let display_str = format!("{curve}");
1232
1233 assert!(display_str.contains("InterestRateCurve"));
1234 assert!(display_str.contains("USD"));
1235 }
1236
1237 #[rstest]
1238 fn test_yield_curve_data_has_ts_init() {
1239 let curve = create_test_yield_curve();
1240 assert_eq!(curve.ts_init(), UnixNanos::from(1_000_000_000));
1241 }
1242
1243 #[rstest]
1244 fn test_yield_curve_data_clone() {
1245 let curve1 = create_test_yield_curve();
1246 let curve2 = curve1.clone();
1247
1248 assert_eq!(curve1.curve_name, curve2.curve_name);
1249 assert_eq!(curve1.tenors, curve2.tenors);
1250 assert_eq!(curve1.interest_rates, curve2.interest_rates);
1251 }
1252
1253 #[rstest]
1254 fn test_black_scholes_greeks_extreme_values() {
1255 let s = 1000.0;
1256 let r = 0.1;
1257 let b = 0.1;
1258 let vol = 0.5;
1259 let is_call = true;
1260 let k = 10.0; let t = 0.1;
1262
1263 let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t);
1264
1265 assert!(greeks.price.is_finite());
1266 assert!(greeks.delta.is_finite());
1267 assert!(greeks.gamma.is_finite());
1268 assert!(greeks.vega.is_finite());
1269 assert!(greeks.theta.is_finite());
1270 assert!(greeks.price > 0.0);
1271 assert!(greeks.delta > 0.99); }
1273
1274 #[rstest]
1275 fn test_black_scholes_greeks_high_volatility() {
1276 let s = 100.0;
1277 let r = 0.05;
1278 let b = 0.05;
1279 let vol = 2.0; let is_call = true;
1281 let k = 100.0;
1282 let t = 1.0;
1283
1284 let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t);
1285
1286 assert!(greeks.price.is_finite());
1287 assert!(greeks.delta.is_finite());
1288 assert!(greeks.gamma.is_finite());
1289 assert!(greeks.vega.is_finite());
1290 assert!(greeks.theta.is_finite());
1291 assert!(greeks.price > 0.0);
1292 }
1293
1294 #[rstest]
1295 fn test_greeks_data_put_option() {
1296 let greeks = GreeksData::new(
1297 UnixNanos::from(1_000_000_000),
1298 UnixNanos::from(1_500_000_000),
1299 InstrumentId::from("SPY240315P00480000.OPRA"),
1300 false, 480.0,
1302 20_240_315,
1303 91, 0.25,
1305 100.0,
1306 1.0,
1307 500.0,
1308 0.05,
1309 0.05,
1310 0.25,
1311 -150.0, 8.5,
1313 OptionGreekValues {
1314 delta: -0.35,
1315 gamma: 0.002,
1316 vega: 12.8,
1317 theta: -0.06,
1318 rho: 0.0,
1319 },
1320 0.25,
1321 );
1322
1323 assert!(!greeks.is_call);
1324 assert!(greeks.delta < 0.0);
1325 assert_eq!(greeks.pnl, -150.0);
1326 }
1327
1328 #[rstest]
1330 fn test_greeks_accuracy_call() {
1331 let s = 100.0;
1332 let k = 100.1;
1333 let t = 1.0;
1334 let r = 0.01;
1335 let b = 0.005;
1336 let vol = 0.2;
1337 let is_call = true;
1338 let eps = 1e-3;
1339
1340 let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t);
1341
1342 let price0 = |s: f64| black_scholes_greeks_exact(s, r, b, vol, is_call, k, t).price;
1344
1345 let delta_bnr = (price0(s + eps) - price0(s - eps)) / (2.0 * eps);
1346 let gamma_bnr = (price0(s + eps) + price0(s - eps) - 2.0 * price0(s)) / (eps * eps);
1347 let vega_bnr = (black_scholes_greeks_exact(s, r, b, vol + eps, is_call, k, t).price
1348 - black_scholes_greeks_exact(s, r, b, vol - eps, is_call, k, t).price)
1349 / (2.0 * eps)
1350 / 100.0;
1351 let theta_bnr = (black_scholes_greeks_exact(s, r, b, vol, is_call, k, t - eps).price
1352 - black_scholes_greeks_exact(s, r, b, vol, is_call, k, t + eps).price)
1353 / (2.0 * eps)
1354 / 365.25;
1355
1356 let tolerance = 5e-3;
1359 assert!(
1360 (greeks.delta - delta_bnr).abs() < tolerance,
1361 "Delta difference exceeds tolerance: {} vs {}",
1362 greeks.delta,
1363 delta_bnr
1364 );
1365 let gamma_tolerance = 0.1;
1367 assert!(
1368 (greeks.gamma - gamma_bnr).abs() < gamma_tolerance,
1369 "Gamma difference exceeds tolerance: {} vs {}",
1370 greeks.gamma,
1371 gamma_bnr
1372 );
1373 assert!(
1375 (greeks.vega - vega_bnr).abs() < tolerance,
1376 "Vega difference exceeds tolerance: {} vs {}",
1377 greeks.vega,
1378 vega_bnr
1379 );
1380 assert!(
1381 (greeks.theta - theta_bnr).abs() < tolerance,
1382 "Theta difference exceeds tolerance: {} vs {}",
1383 greeks.theta,
1384 theta_bnr
1385 );
1386 }
1387
1388 #[rstest]
1389 fn test_greeks_accuracy_put() {
1390 let s = 100.0;
1391 let k = 100.1;
1392 let t = 1.0;
1393 let r = 0.01;
1394 let b = 0.005;
1395 let vol = 0.2;
1396 let is_call = false;
1397 let eps = 1e-3;
1398
1399 let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t);
1400
1401 let price0 = |s: f64| black_scholes_greeks_exact(s, r, b, vol, is_call, k, t).price;
1403
1404 let delta_bnr = (price0(s + eps) - price0(s - eps)) / (2.0 * eps);
1405 let gamma_bnr = (price0(s + eps) + price0(s - eps) - 2.0 * price0(s)) / (eps * eps);
1406 let vega_bnr = (black_scholes_greeks_exact(s, r, b, vol + eps, is_call, k, t).price
1407 - black_scholes_greeks_exact(s, r, b, vol - eps, is_call, k, t).price)
1408 / (2.0 * eps)
1409 / 100.0;
1410 let theta_bnr = (black_scholes_greeks_exact(s, r, b, vol, is_call, k, t - eps).price
1411 - black_scholes_greeks_exact(s, r, b, vol, is_call, k, t + eps).price)
1412 / (2.0 * eps)
1413 / 365.25;
1414
1415 let tolerance = 5e-3;
1418 assert!(
1419 (greeks.delta - delta_bnr).abs() < tolerance,
1420 "Delta difference exceeds tolerance: {} vs {}",
1421 greeks.delta,
1422 delta_bnr
1423 );
1424 let gamma_tolerance = 0.1;
1426 assert!(
1427 (greeks.gamma - gamma_bnr).abs() < gamma_tolerance,
1428 "Gamma difference exceeds tolerance: {} vs {}",
1429 greeks.gamma,
1430 gamma_bnr
1431 );
1432 assert!(
1434 (greeks.vega - vega_bnr).abs() < tolerance,
1435 "Vega difference exceeds tolerance: {} vs {}",
1436 greeks.vega,
1437 vega_bnr
1438 );
1439 assert!(
1440 (greeks.theta - theta_bnr).abs() < tolerance,
1441 "Theta difference exceeds tolerance: {} vs {}",
1442 greeks.theta,
1443 theta_bnr
1444 );
1445 }
1446
1447 #[rstest]
1448 fn test_imply_vol_and_greeks_accuracy_call() {
1449 let s = 100.0;
1450 let k = 100.1;
1451 let t = 1.0;
1452 let r = 0.01;
1453 let b = 0.005;
1454 let vol = 0.2;
1455 let is_call = true;
1456
1457 let base_greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t);
1458 let price = base_greeks.price;
1459
1460 let implied_result = imply_vol_and_greeks(s, r, b, is_call, k, t, price);
1461
1462 let tolerance = 2e-4;
1464 assert!(
1465 (implied_result.vol - vol).abs() < tolerance,
1466 "Vol difference exceeds tolerance: {} vs {}",
1467 implied_result.vol,
1468 vol
1469 );
1470 assert!(
1471 (implied_result.price - base_greeks.price).abs() < tolerance,
1472 "Price difference exceeds tolerance: {} vs {}",
1473 implied_result.price,
1474 base_greeks.price
1475 );
1476 assert!(
1477 (implied_result.delta - base_greeks.delta).abs() < tolerance,
1478 "Delta difference exceeds tolerance: {} vs {}",
1479 implied_result.delta,
1480 base_greeks.delta
1481 );
1482 assert!(
1483 (implied_result.gamma - base_greeks.gamma).abs() < tolerance,
1484 "Gamma difference exceeds tolerance: {} vs {}",
1485 implied_result.gamma,
1486 base_greeks.gamma
1487 );
1488 assert!(
1489 (implied_result.vega - base_greeks.vega).abs() < tolerance,
1490 "Vega difference exceeds tolerance: {} vs {}",
1491 implied_result.vega,
1492 base_greeks.vega
1493 );
1494 assert!(
1495 (implied_result.theta - base_greeks.theta).abs() < tolerance,
1496 "Theta difference exceeds tolerance: {} vs {}",
1497 implied_result.theta,
1498 base_greeks.theta
1499 );
1500 }
1501
1502 #[rstest]
1503 fn test_black_scholes_greeks_target_price_refinement() {
1504 let s = 100.0;
1505 let r = 0.05;
1506 let b = 0.05;
1507 let initial_vol = 0.2;
1508 let is_call = true;
1509 let k = 100.0;
1510 let t = 1.0;
1511
1512 let initial_greeks = black_scholes_greeks(s, r, b, initial_vol, is_call, k, t);
1514 let target_price = initial_greeks.price;
1515
1516 let refined_vol = initial_vol * 1.1; let refined_greeks =
1519 refine_vol_and_greeks(s, r, b, is_call, k, t, target_price, refined_vol);
1520
1521 let price_tolerance = (s * 5e-5).max(1e-4) * 2.0;
1524 assert!(
1525 (refined_greeks.price - target_price).abs() < price_tolerance,
1526 "Refined price should match target: {} vs {}",
1527 refined_greeks.price,
1528 target_price
1529 );
1530
1531 assert!(
1533 refined_vol > refined_greeks.vol && refined_greeks.vol > initial_vol * 0.9,
1534 "Refined vol should converge towards initial: {} (initial: {}, refined: {})",
1535 refined_greeks.vol,
1536 initial_vol,
1537 refined_vol
1538 );
1539 }
1540
1541 #[rstest]
1542 fn test_black_scholes_greeks_target_price_refinement_put() {
1543 let s = 100.0;
1544 let r = 0.05;
1545 let b = 0.05;
1546 let initial_vol = 0.25;
1547 let is_call = false;
1548 let k = 105.0;
1549 let t = 0.5;
1550
1551 let initial_greeks = black_scholes_greeks(s, r, b, initial_vol, is_call, k, t);
1553 let target_price = initial_greeks.price;
1554
1555 let refined_vol = initial_vol * 0.8; let refined_greeks =
1558 refine_vol_and_greeks(s, r, b, is_call, k, t, target_price, refined_vol);
1559
1560 let price_tolerance = (s * 5e-5).max(1e-4) * 2.0;
1563 assert!(
1564 (refined_greeks.price - target_price).abs() < price_tolerance,
1565 "Refined price should match target: {} vs {}",
1566 refined_greeks.price,
1567 target_price
1568 );
1569
1570 assert!(
1572 refined_vol < refined_greeks.vol && refined_greeks.vol < initial_vol * 1.1,
1573 "Refined vol should converge towards initial: {} (initial: {}, refined: {})",
1574 refined_greeks.vol,
1575 initial_vol,
1576 refined_vol
1577 );
1578 }
1579
1580 #[rstest]
1581 fn test_imply_vol_and_greeks_accuracy_put() {
1582 let s = 100.0;
1583 let k = 100.1;
1584 let t = 1.0;
1585 let r = 0.01;
1586 let b = 0.005;
1587 let vol = 0.2;
1588 let is_call = false;
1589
1590 let base_greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t);
1591 let price = base_greeks.price;
1592
1593 let implied_result = imply_vol_and_greeks(s, r, b, is_call, k, t, price);
1594
1595 let tolerance = 2e-4;
1597 assert!(
1598 (implied_result.vol - vol).abs() < tolerance,
1599 "Vol difference exceeds tolerance: {} vs {}",
1600 implied_result.vol,
1601 vol
1602 );
1603 assert!(
1604 (implied_result.price - base_greeks.price).abs() < tolerance,
1605 "Price difference exceeds tolerance: {} vs {}",
1606 implied_result.price,
1607 base_greeks.price
1608 );
1609 assert!(
1610 (implied_result.delta - base_greeks.delta).abs() < tolerance,
1611 "Delta difference exceeds tolerance: {} vs {}",
1612 implied_result.delta,
1613 base_greeks.delta
1614 );
1615 assert!(
1616 (implied_result.gamma - base_greeks.gamma).abs() < tolerance,
1617 "Gamma difference exceeds tolerance: {} vs {}",
1618 implied_result.gamma,
1619 base_greeks.gamma
1620 );
1621 assert!(
1622 (implied_result.vega - base_greeks.vega).abs() < tolerance,
1623 "Vega difference exceeds tolerance: {} vs {}",
1624 implied_result.vega,
1625 base_greeks.vega
1626 );
1627 assert!(
1628 (implied_result.theta - base_greeks.theta).abs() < tolerance,
1629 "Theta difference exceeds tolerance: {} vs {}",
1630 implied_result.theta,
1631 base_greeks.theta
1632 );
1633 }
1634
1635 #[rstest]
1638 fn test_black_scholes_greeks_vs_exact(
1639 #[values(90.0, 100.0, 110.0)] spot: f64,
1640 #[values(true, false)] is_call: bool,
1641 #[values(0.15, 0.25, 0.5)] vol: f64,
1642 #[values(0.01, 0.25, 2.0)] t: f64,
1643 ) {
1644 let r = 0.05;
1645 let b = 0.05;
1646 let k = 100.0;
1647
1648 let greeks_fast = black_scholes_greeks(spot, r, b, vol, is_call, k, t);
1649 let greeks_exact = black_scholes_greeks_exact(spot, r, b, vol, is_call, k, t);
1650
1651 let rel_tolerance = if t < 0.1 {
1656 1e-4 } else {
1658 8e-6 };
1660 let abs_tolerance = 1e-10; let check_7_sig_figs = |fast: f64, exact: f64, name: &str| {
1664 let abs_diff = (fast - exact).abs();
1665 let small_value_threshold = 1e-4;
1669 let max_allowed = if exact.abs() < small_value_threshold {
1670 if t < 0.1 {
1672 1e-5 } else {
1674 1e-6 }
1676 } else {
1677 exact.abs().max(abs_tolerance) * rel_tolerance
1679 };
1680 let rel_diff = if exact.abs() > abs_tolerance {
1681 abs_diff / exact.abs()
1682 } else {
1683 0.0 };
1685
1686 assert!(
1687 abs_diff < max_allowed,
1688 "{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}"
1689 );
1690 };
1691
1692 check_7_sig_figs(greeks_fast.price, greeks_exact.price, "Price");
1693 check_7_sig_figs(greeks_fast.delta, greeks_exact.delta, "Delta");
1694 check_7_sig_figs(greeks_fast.gamma, greeks_exact.gamma, "Gamma");
1695 check_7_sig_figs(greeks_fast.vega, greeks_exact.vega, "Vega");
1696 check_7_sig_figs(greeks_fast.theta, greeks_exact.theta, "Theta");
1697 }
1698
1699 #[rstest]
1702 fn test_refine_vol_and_greeks_vs_imply_vol_and_greeks(
1703 #[values(90.0, 100.0, 110.0)] spot: f64,
1704 #[values(true, false)] is_call: bool,
1705 #[values(0.15, 0.25, 0.5)] target_vol: f64,
1706 #[values(0.01, 0.25, 2.0)] t: f64,
1707 ) {
1708 let r = 0.05;
1709 let b = 0.05;
1710 let k = 100.0;
1711
1712 let base_greeks = black_scholes_greeks(spot, r, b, target_vol, is_call, k, t);
1714 let target_price = base_greeks.price;
1715
1716 let initial_guess = target_vol - 0.01;
1718
1719 let refined_result =
1721 refine_vol_and_greeks(spot, r, b, is_call, k, t, target_price, initial_guess);
1722
1723 let implied_result = imply_vol_and_greeks(spot, r, b, is_call, k, t, target_price);
1725
1726 let moneyness = (spot - k) / k;
1729 let is_deep_itm_otm = moneyness.abs() > 0.05;
1730 let is_deep_edge_case = t < 0.1 && is_deep_itm_otm;
1731
1732 let vol_abs_tolerance = 1e-6;
1738 let vol_rel_tolerance = if is_deep_edge_case {
1739 2.0 } else if t < 0.1 {
1742 0.10 } else if t > 1.5 {
1745 if target_vol <= 0.15 {
1747 0.05 } else {
1749 0.01 }
1751 } else {
1752 if target_vol <= 0.15 {
1754 0.05 } else {
1756 0.001 }
1758 };
1759
1760 let refined_vol_error = (refined_result.vol - target_vol).abs();
1761 let implied_vol_error = (implied_result.vol - target_vol).abs();
1762 let refined_vol_rel_error = refined_vol_error / target_vol.max(vol_abs_tolerance);
1763 let implied_vol_rel_error = implied_vol_error / target_vol.max(vol_abs_tolerance);
1764
1765 assert!(
1766 refined_vol_rel_error < vol_rel_tolerance,
1767 "Refined vol mismatch for spot={}, is_call={}, target_vol={}, t={}: refined={:.10}, target={:.10}, abs_error={:.2e}, rel_error={:.2e}",
1768 spot,
1769 is_call,
1770 target_vol,
1771 t,
1772 refined_result.vol,
1773 target_vol,
1774 refined_vol_error,
1775 refined_vol_rel_error
1776 );
1777
1778 let implied_vol_tolerance = if is_deep_edge_case {
1781 2.0 } else if implied_result.vol < 1e-6 {
1784 2.0 } else if t < 0.1 && (implied_result.vol - target_vol).abs() / target_vol.max(1e-6) > 0.5 {
1787 2.0 } else {
1790 vol_rel_tolerance
1791 };
1792
1793 assert!(
1794 implied_vol_rel_error < implied_vol_tolerance,
1795 "Implied vol mismatch for spot={}, is_call={}, target_vol={}, t={}: implied={:.10}, target={:.10}, abs_error={:.2e}, rel_error={:.2e}",
1796 spot,
1797 is_call,
1798 target_vol,
1799 t,
1800 implied_result.vol,
1801 target_vol,
1802 implied_vol_error,
1803 implied_vol_rel_error
1804 );
1805
1806 let greeks_abs_tolerance = 1e-10;
1810
1811 let moneyness = (spot - k) / k;
1813 let is_deep_itm_otm = moneyness.abs() > 0.05;
1814 let is_deep_edge_case = t < 0.1 && is_deep_itm_otm;
1815
1816 let greeks_rel_tolerance = if is_deep_edge_case {
1820 1.0 } else if t < 0.1 {
1823 if target_vol <= 0.15 {
1825 0.10 } else {
1827 0.05 }
1829 } else if t > 1.5 {
1830 if target_vol <= 0.15 {
1832 0.08 } else {
1834 0.01 }
1836 } else {
1837 if target_vol <= 0.15 {
1839 0.05 } else {
1841 2e-3 }
1843 };
1844
1845 let imply_vol_failed = implied_result.vol < 1e-6
1850 || (t < 0.1 && (implied_result.vol - target_vol).abs() / target_vol.max(1e-6) > 0.5)
1851 || is_deep_edge_case;
1852 let effective_greeks_tolerance = if imply_vol_failed || is_deep_edge_case {
1853 1.0 } else {
1855 greeks_rel_tolerance
1856 };
1857
1858 let check_6_sig_figs = |refined: f64, implied: f64, name: &str, is_gamma: bool| {
1859 if (imply_vol_failed || is_deep_edge_case)
1862 && (!implied.is_finite() || implied.abs() < 1e-4 || refined.abs() < 1e-4)
1863 {
1864 return; }
1866
1867 let abs_diff = (refined - implied).abs();
1868 let small_value_threshold = if is_deep_edge_case { 1e-3 } else { 1e-6 };
1871 let rel_diff =
1872 if implied.abs() < small_value_threshold && refined.abs() < small_value_threshold {
1873 0.0 } else {
1875 abs_diff / implied.abs().max(greeks_abs_tolerance)
1876 };
1877 let gamma_multiplier = if (0.1..=1.5).contains(&t) {
1879 if target_vol <= 0.15 { 5.0 } else { 3.0 }
1881 } else {
1882 if target_vol <= 0.15 { 10.0 } else { 5.0 }
1884 };
1885 let tolerance = if is_gamma {
1886 effective_greeks_tolerance * gamma_multiplier
1887 } else {
1888 effective_greeks_tolerance
1889 };
1890 let max_allowed = if is_deep_edge_case && implied.abs() < 1e-3 {
1892 2e-5 } else {
1894 implied.abs().max(greeks_abs_tolerance) * tolerance
1895 };
1896
1897 assert!(
1898 abs_diff < max_allowed,
1899 "{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}"
1900 );
1901 };
1902
1903 check_6_sig_figs(refined_result.price, implied_result.price, "Price", false);
1904 check_6_sig_figs(refined_result.delta, implied_result.delta, "Delta", false);
1905 check_6_sig_figs(refined_result.gamma, implied_result.gamma, "Gamma", true);
1906 check_6_sig_figs(refined_result.vega, implied_result.vega, "Vega", false);
1907 check_6_sig_figs(refined_result.theta, implied_result.theta, "Theta", false);
1908 }
1909}