nautilus_model/data/
greeks.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Option *Greeks* data structures (delta, gamma, theta, vega, rho) used throughout the platform.
17
18use 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// dS_t = S_t * (b * dt + sigma * dW_t) (stock)
43// dC_t = r * C_t * dt (cash numeraire)
44#[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; // in absolute percent change
70    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; // 1 / 365.25 in change per calendar day
74
75    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    // in the money probability, P(phi * S_T > phi * K), phi = 1 if is_call else -1
152    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
282// Implement multiplication for quantity * greeks
283impl 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    // Interpolate the yield curve for a given expiry time
452    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}