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