1use std::{
17 fmt,
18 ops::{Add, Mul},
19};
20
21use implied_vol::{implied_black_volatility, norm_cdf, norm_pdf};
22use nautilus_core::{UnixNanos, datetime::unix_nanos_to_iso8601, math::quadratic_interpolation};
23
24use crate::{data::GetTsInit, identifiers::InstrumentId};
25
26#[repr(C)]
27#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
28#[cfg_attr(
29 feature = "python",
30 pyo3::pyclass(module = "posei_trader.core.nautilus_pyo3.model")
31)]
32pub struct BlackScholesGreeksResult {
33 pub price: f64,
34 pub delta: f64,
35 pub gamma: f64,
36 pub vega: f64,
37 pub theta: f64,
38}
39
40#[allow(clippy::too_many_arguments)]
43pub fn black_scholes_greeks(
44 s: f64,
45 r: f64,
46 b: f64,
47 sigma: f64,
48 is_call: bool,
49 k: f64,
50 t: f64,
51 multiplier: f64,
52) -> BlackScholesGreeksResult {
53 let phi = if is_call { 1.0 } else { -1.0 };
54 let scaled_vol = sigma * t.sqrt();
55 let d1 = ((s / k).ln() + (b + 0.5 * sigma.powi(2)) * t) / scaled_vol;
56 let d2 = d1 - scaled_vol;
57 let cdf_phi_d1 = norm_cdf(phi * d1);
58 let cdf_phi_d2 = norm_cdf(phi * d2);
59 let dist_d1 = norm_pdf(d1);
60 let df = ((b - r) * t).exp();
61 let s_t = s * df;
62 let k_t = k * (-r * t).exp();
63
64 let price = multiplier * phi * (s_t * cdf_phi_d1 - k_t * cdf_phi_d2);
65 let delta = multiplier * phi * df * cdf_phi_d1;
66 let gamma = multiplier * df * dist_d1 / (s * scaled_vol);
67 let vega = multiplier * s_t * t.sqrt() * dist_d1 * 0.01; let theta = multiplier
69 * (s_t * (-dist_d1 * sigma / (2.0 * t.sqrt()) - phi * (b - r) * cdf_phi_d1)
70 - phi * r * k_t * cdf_phi_d2)
71 * 0.0027378507871321013; BlackScholesGreeksResult {
74 price,
75 delta,
76 gamma,
77 vega,
78 theta,
79 }
80}
81
82pub fn imply_vol(s: f64, r: f64, b: f64, is_call: bool, k: f64, t: f64, price: f64) -> f64 {
83 let forward = s * b.exp();
84 let forward_price = price * (r * t).exp();
85
86 implied_black_volatility(forward_price, forward, k, t, is_call)
87}
88
89#[repr(C)]
90#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
91#[cfg_attr(
92 feature = "python",
93 pyo3::pyclass(module = "posei_trader.core.nautilus_pyo3.model")
94)]
95pub struct ImplyVolAndGreeksResult {
96 pub vol: f64,
97 pub price: f64,
98 pub delta: f64,
99 pub gamma: f64,
100 pub vega: f64,
101 pub theta: f64,
102}
103
104#[allow(clippy::too_many_arguments)]
105pub fn imply_vol_and_greeks(
106 s: f64,
107 r: f64,
108 b: f64,
109 is_call: bool,
110 k: f64,
111 t: f64,
112 price: f64,
113 multiplier: f64,
114) -> ImplyVolAndGreeksResult {
115 let vol = imply_vol(s, r, b, is_call, k, t, price);
116 let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t, multiplier);
117
118 ImplyVolAndGreeksResult {
119 vol,
120 price: greeks.price,
121 delta: greeks.delta,
122 gamma: greeks.gamma,
123 vega: greeks.vega,
124 theta: greeks.theta,
125 }
126}
127
128#[derive(Debug, Clone)]
129pub struct GreeksData {
130 pub ts_init: UnixNanos,
131 pub ts_event: UnixNanos,
132 pub instrument_id: InstrumentId,
133 pub is_call: bool,
134 pub strike: f64,
135 pub expiry: i32,
136 pub expiry_in_years: f64,
137 pub multiplier: f64,
138 pub quantity: f64,
139 pub underlying_price: f64,
140 pub interest_rate: f64,
141 pub cost_of_carry: f64,
142 pub vol: f64,
143 pub pnl: f64,
144 pub price: f64,
145 pub delta: f64,
146 pub gamma: f64,
147 pub vega: f64,
148 pub theta: f64,
149 pub itm_prob: f64,
151}
152
153impl GreeksData {
154 #[allow(clippy::too_many_arguments)]
155 pub fn new(
156 ts_init: UnixNanos,
157 ts_event: UnixNanos,
158 instrument_id: InstrumentId,
159 is_call: bool,
160 strike: f64,
161 expiry: i32,
162 expiry_in_years: f64,
163 multiplier: f64,
164 quantity: f64,
165 underlying_price: f64,
166 interest_rate: f64,
167 cost_of_carry: f64,
168 vol: f64,
169 pnl: f64,
170 price: f64,
171 delta: f64,
172 gamma: f64,
173 vega: f64,
174 theta: f64,
175 itm_prob: f64,
176 ) -> Self {
177 Self {
178 ts_init,
179 ts_event,
180 instrument_id,
181 is_call,
182 strike,
183 expiry,
184 expiry_in_years,
185 multiplier,
186 quantity,
187 underlying_price,
188 interest_rate,
189 cost_of_carry,
190 vol,
191 pnl,
192 price,
193 delta,
194 gamma,
195 vega,
196 theta,
197 itm_prob,
198 }
199 }
200
201 pub fn from_delta(
202 instrument_id: InstrumentId,
203 delta: f64,
204 multiplier: f64,
205 ts_event: UnixNanos,
206 ) -> Self {
207 Self {
208 ts_init: ts_event,
209 ts_event,
210 instrument_id,
211 is_call: true,
212 strike: 0.0,
213 expiry: 0,
214 expiry_in_years: 0.0,
215 multiplier,
216 quantity: 1.0,
217 underlying_price: 0.0,
218 interest_rate: 0.0,
219 cost_of_carry: 0.0,
220 vol: 0.0,
221 pnl: 0.0,
222 price: 0.0,
223 delta,
224 gamma: 0.0,
225 vega: 0.0,
226 theta: 0.0,
227 itm_prob: 0.0,
228 }
229 }
230}
231
232impl Default for GreeksData {
233 fn default() -> Self {
234 Self {
235 ts_init: UnixNanos::default(),
236 ts_event: UnixNanos::default(),
237 instrument_id: InstrumentId::from("ES.GLBX"),
238 is_call: true,
239 strike: 0.0,
240 expiry: 0,
241 expiry_in_years: 0.0,
242 multiplier: 0.0,
243 quantity: 0.0,
244 underlying_price: 0.0,
245 interest_rate: 0.0,
246 cost_of_carry: 0.0,
247 vol: 0.0,
248 pnl: 0.0,
249 price: 0.0,
250 delta: 0.0,
251 gamma: 0.0,
252 vega: 0.0,
253 theta: 0.0,
254 itm_prob: 0.0,
255 }
256 }
257}
258
259impl fmt::Display for GreeksData {
260 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261 write!(
262 f,
263 "GreeksData(instrument_id={}, expiry={}, itm_prob={:.2}%, vol={:.2}%, pnl={:.2}, price={:.2}, delta={:.2}, gamma={:.2}, vega={:.2}, theta={:.2}, quantity={}, ts_init={})",
264 self.instrument_id,
265 self.expiry,
266 self.itm_prob * 100.0,
267 self.vol * 100.0,
268 self.pnl,
269 self.price,
270 self.delta,
271 self.gamma,
272 self.vega,
273 self.theta,
274 self.quantity,
275 unix_nanos_to_iso8601(self.ts_init)
276 )
277 }
278}
279
280impl Mul<&GreeksData> for f64 {
282 type Output = GreeksData;
283
284 fn mul(self, greeks: &GreeksData) -> GreeksData {
285 GreeksData {
286 ts_init: greeks.ts_init,
287 ts_event: greeks.ts_event,
288 instrument_id: greeks.instrument_id,
289 is_call: greeks.is_call,
290 strike: greeks.strike,
291 expiry: greeks.expiry,
292 expiry_in_years: greeks.expiry_in_years,
293 multiplier: greeks.multiplier,
294 quantity: greeks.quantity,
295 underlying_price: greeks.underlying_price,
296 interest_rate: greeks.interest_rate,
297 cost_of_carry: greeks.cost_of_carry,
298 vol: greeks.vol,
299 pnl: self * greeks.pnl,
300 price: self * greeks.price,
301 delta: self * greeks.delta,
302 gamma: self * greeks.gamma,
303 vega: self * greeks.vega,
304 theta: self * greeks.theta,
305 itm_prob: greeks.itm_prob,
306 }
307 }
308}
309
310impl GetTsInit for GreeksData {
311 fn ts_init(&self) -> UnixNanos {
312 self.ts_init
313 }
314}
315
316#[derive(Debug, Clone)]
317pub struct PortfolioGreeks {
318 pub ts_init: UnixNanos,
319 pub ts_event: UnixNanos,
320 pub pnl: f64,
321 pub price: f64,
322 pub delta: f64,
323 pub gamma: f64,
324 pub vega: f64,
325 pub theta: f64,
326}
327
328impl PortfolioGreeks {
329 #[allow(clippy::too_many_arguments)]
330 pub fn new(
331 ts_init: UnixNanos,
332 ts_event: UnixNanos,
333 pnl: f64,
334 price: f64,
335 delta: f64,
336 gamma: f64,
337 vega: f64,
338 theta: f64,
339 ) -> Self {
340 Self {
341 ts_init,
342 ts_event,
343 pnl,
344 price,
345 delta,
346 gamma,
347 vega,
348 theta,
349 }
350 }
351}
352
353impl Default for PortfolioGreeks {
354 fn default() -> Self {
355 Self {
356 ts_init: UnixNanos::default(),
357 ts_event: UnixNanos::default(),
358 pnl: 0.0,
359 price: 0.0,
360 delta: 0.0,
361 gamma: 0.0,
362 vega: 0.0,
363 theta: 0.0,
364 }
365 }
366}
367
368impl fmt::Display for PortfolioGreeks {
369 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
370 write!(
371 f,
372 "PortfolioGreeks(pnl={:.2}, price={:.2}, delta={:.2}, gamma={:.2}, vega={:.2}, theta={:.2}, ts_event={}, ts_init={})",
373 self.pnl,
374 self.price,
375 self.delta,
376 self.gamma,
377 self.vega,
378 self.theta,
379 unix_nanos_to_iso8601(self.ts_event),
380 unix_nanos_to_iso8601(self.ts_init)
381 )
382 }
383}
384
385impl Add for PortfolioGreeks {
386 type Output = Self;
387
388 fn add(self, other: Self) -> Self {
389 Self {
390 ts_init: self.ts_init,
391 ts_event: self.ts_event,
392 pnl: self.pnl + other.pnl,
393 price: self.price + other.price,
394 delta: self.delta + other.delta,
395 gamma: self.gamma + other.gamma,
396 vega: self.vega + other.vega,
397 theta: self.theta + other.theta,
398 }
399 }
400}
401
402impl From<GreeksData> for PortfolioGreeks {
403 fn from(greeks: GreeksData) -> Self {
404 Self {
405 ts_init: greeks.ts_init,
406 ts_event: greeks.ts_event,
407 pnl: greeks.pnl,
408 price: greeks.price,
409 delta: greeks.delta,
410 gamma: greeks.gamma,
411 vega: greeks.vega,
412 theta: greeks.theta,
413 }
414 }
415}
416
417impl GetTsInit for PortfolioGreeks {
418 fn ts_init(&self) -> UnixNanos {
419 self.ts_init
420 }
421}
422
423#[derive(Debug, Clone)]
424pub struct YieldCurveData {
425 pub ts_init: UnixNanos,
426 pub ts_event: UnixNanos,
427 pub curve_name: String,
428 pub tenors: Vec<f64>,
429 pub interest_rates: Vec<f64>,
430}
431
432impl YieldCurveData {
433 pub fn new(
434 ts_init: UnixNanos,
435 ts_event: UnixNanos,
436 curve_name: String,
437 tenors: Vec<f64>,
438 interest_rates: Vec<f64>,
439 ) -> Self {
440 Self {
441 ts_init,
442 ts_event,
443 curve_name,
444 tenors,
445 interest_rates,
446 }
447 }
448
449 pub fn get_rate(&self, expiry_in_years: f64) -> f64 {
451 if self.interest_rates.len() == 1 {
452 return self.interest_rates[0];
453 }
454
455 quadratic_interpolation(expiry_in_years, &self.tenors, &self.interest_rates)
456 }
457}
458
459impl fmt::Display for YieldCurveData {
460 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
461 write!(
462 f,
463 "InterestRateCurve(curve_name={}, ts_event={}, ts_init={})",
464 self.curve_name,
465 unix_nanos_to_iso8601(self.ts_event),
466 unix_nanos_to_iso8601(self.ts_init)
467 )
468 }
469}
470
471impl GetTsInit for YieldCurveData {
472 fn ts_init(&self) -> UnixNanos {
473 self.ts_init
474 }
475}
476
477impl Default for YieldCurveData {
478 fn default() -> Self {
479 Self {
480 ts_init: UnixNanos::default(),
481 ts_event: UnixNanos::default(),
482 curve_name: "USD".to_string(),
483 tenors: vec![0.5, 1.0, 1.5, 2.0, 2.5],
484 interest_rates: vec![0.04, 0.04, 0.04, 0.04, 0.04],
485 }
486 }
487}
488
489#[cfg(test)]
490mod tests {
491 use rstest::rstest;
492
493 use super::*;
494
495 #[rstest]
496 fn test_greeks_accuracy_call() {
497 let s = 100.0;
498 let k = 100.1;
499 let t = 1.0;
500 let r = 0.01;
501 let b = 0.005;
502 let sigma = 0.2;
503 let is_call = true;
504 let eps = 1e-3;
505
506 let greeks = black_scholes_greeks(s, r, b, sigma, is_call, k, t, 1.0);
507
508 let price0 = |s: f64| black_scholes_greeks(s, r, b, sigma, is_call, k, t, 1.0).price;
509
510 let delta_bnr = (price0(s + eps) - price0(s - eps)) / (2.0 * eps);
511 let gamma_bnr = (price0(s + eps) + price0(s - eps) - 2.0 * price0(s)) / (eps * eps);
512 let vega_bnr = (black_scholes_greeks(s, r, b, sigma + eps, is_call, k, t, 1.0).price
513 - black_scholes_greeks(s, r, b, sigma - eps, is_call, k, t, 1.0).price)
514 / (2.0 * eps)
515 / 100.0;
516 let theta_bnr = (black_scholes_greeks(s, r, b, sigma, is_call, k, t - eps, 1.0).price
517 - black_scholes_greeks(s, r, b, sigma, is_call, k, t + eps, 1.0).price)
518 / (2.0 * eps)
519 / 365.25;
520
521 let tolerance = 1e-5;
522 assert!(
523 (greeks.delta - delta_bnr).abs() < tolerance,
524 "Delta difference exceeds tolerance"
525 );
526 assert!(
527 (greeks.gamma - gamma_bnr).abs() < tolerance,
528 "Gamma difference exceeds tolerance"
529 );
530 assert!(
531 (greeks.vega - vega_bnr).abs() < tolerance,
532 "Vega difference exceeds tolerance"
533 );
534 assert!(
535 (greeks.theta - theta_bnr).abs() < tolerance,
536 "Theta difference exceeds tolerance"
537 );
538 }
539
540 #[rstest]
541 fn test_greeks_accuracy_put() {
542 let s = 100.0;
543 let k = 100.1;
544 let t = 1.0;
545 let r = 0.01;
546 let b = 0.005;
547 let sigma = 0.2;
548 let is_call = false;
549 let eps = 1e-3;
550
551 let greeks = black_scholes_greeks(s, r, b, sigma, is_call, k, t, 1.0);
552
553 let price0 = |s: f64| black_scholes_greeks(s, r, b, sigma, is_call, k, t, 1.0).price;
554
555 let delta_bnr = (price0(s + eps) - price0(s - eps)) / (2.0 * eps);
556 let gamma_bnr = (price0(s + eps) + price0(s - eps) - 2.0 * price0(s)) / (eps * eps);
557 let vega_bnr = (black_scholes_greeks(s, r, b, sigma + eps, is_call, k, t, 1.0).price
558 - black_scholes_greeks(s, r, b, sigma - eps, is_call, k, t, 1.0).price)
559 / (2.0 * eps)
560 / 100.0;
561 let theta_bnr = (black_scholes_greeks(s, r, b, sigma, is_call, k, t - eps, 1.0).price
562 - black_scholes_greeks(s, r, b, sigma, is_call, k, t + eps, 1.0).price)
563 / (2.0 * eps)
564 / 365.25;
565
566 let tolerance = 1e-5;
567 assert!(
568 (greeks.delta - delta_bnr).abs() < tolerance,
569 "Delta difference exceeds tolerance"
570 );
571 assert!(
572 (greeks.gamma - gamma_bnr).abs() < tolerance,
573 "Gamma difference exceeds tolerance"
574 );
575 assert!(
576 (greeks.vega - vega_bnr).abs() < tolerance,
577 "Vega difference exceeds tolerance"
578 );
579 assert!(
580 (greeks.theta - theta_bnr).abs() < tolerance,
581 "Theta difference exceeds tolerance"
582 );
583 }
584
585 #[rstest]
586 fn test_imply_vol_and_greeks_accuracy_call() {
587 let s = 100.0;
588 let k = 100.1;
589 let t = 1.0;
590 let r = 0.01;
591 let b = 0.005;
592 let sigma = 0.2;
593 let is_call = true;
594
595 let base_greeks = black_scholes_greeks(s, r, b, sigma, is_call, k, t, 1.0);
596 let price = base_greeks.price;
597
598 let implied_result = imply_vol_and_greeks(s, r, b, is_call, k, t, price, 1.0);
599
600 let tolerance = 1e-5;
601 assert!(
602 (implied_result.vol - sigma).abs() < tolerance,
603 "Vol difference exceeds tolerance"
604 );
605 assert!(
606 (implied_result.price - base_greeks.price).abs() < tolerance,
607 "Price difference exceeds tolerance"
608 );
609 assert!(
610 (implied_result.delta - base_greeks.delta).abs() < tolerance,
611 "Delta difference exceeds tolerance"
612 );
613 assert!(
614 (implied_result.gamma - base_greeks.gamma).abs() < tolerance,
615 "Gamma difference exceeds tolerance"
616 );
617 assert!(
618 (implied_result.vega - base_greeks.vega).abs() < tolerance,
619 "Vega difference exceeds tolerance"
620 );
621 assert!(
622 (implied_result.theta - base_greeks.theta).abs() < tolerance,
623 "Theta difference exceeds tolerance"
624 );
625 }
626
627 #[rstest]
628 fn test_imply_vol_and_greeks_accuracy_put() {
629 let s = 100.0;
630 let k = 100.1;
631 let t = 1.0;
632 let r = 0.01;
633 let b = 0.005;
634 let sigma = 0.2;
635 let is_call = false;
636
637 let base_greeks = black_scholes_greeks(s, r, b, sigma, is_call, k, t, 1.0);
638 let price = base_greeks.price;
639
640 let implied_result = imply_vol_and_greeks(s, r, b, is_call, k, t, price, 1.0);
641
642 let tolerance = 1e-5;
643 assert!(
644 (implied_result.vol - sigma).abs() < tolerance,
645 "Vol difference exceeds tolerance"
646 );
647 assert!(
648 (implied_result.price - base_greeks.price).abs() < tolerance,
649 "Price difference exceeds tolerance"
650 );
651 assert!(
652 (implied_result.delta - base_greeks.delta).abs() < tolerance,
653 "Delta difference exceeds tolerance"
654 );
655 assert!(
656 (implied_result.gamma - base_greeks.gamma).abs() < tolerance,
657 "Gamma difference exceeds tolerance"
658 );
659 assert!(
660 (implied_result.vega - base_greeks.vega).abs() < tolerance,
661 "Vega difference exceeds tolerance"
662 );
663 assert!(
664 (implied_result.theta - base_greeks.theta).abs() < tolerance,
665 "Theta difference exceeds tolerance"
666 );
667 }
668}