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
16use 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// dS_t = S_t * (b * dt + sigma * dW_t) (stock)
41// dC_t = r * C_t * dt (cash numeraire)
42#[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; // in absolute percent change
68    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; // 1 / 365.25 in change per calendar day
72
73    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    // in the money probability, P(phi * S_T > phi * K), phi = 1 if is_call else -1
150    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
280// Implement multiplication for quantity * greeks
281impl 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    // Interpolate the yield curve for a given expiry time
450    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}