Skip to main content

nautilus_model/python/data/
greeks.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.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 nautilus_core::{UnixNanos, python::to_pyvalue_err};
17use pyo3::{prelude::*, types::PyType};
18
19use crate::data::greeks::{
20    BlackScholesGreeksResult, GreeksData, OptionGreekValues, PortfolioGreeks, YieldCurve,
21    black_scholes_greeks_checked, imply_vol_and_greeks_checked, imply_vol_checked,
22};
23
24#[cfg(feature = "python")]
25#[pymethods]
26#[pyo3_stub_gen::derive::gen_stub_pymethods]
27impl OptionGreekValues {
28    #[getter]
29    fn delta(&self) -> f64 {
30        self.delta
31    }
32
33    #[getter]
34    fn gamma(&self) -> f64 {
35        self.gamma
36    }
37
38    #[getter]
39    fn vega(&self) -> f64 {
40        self.vega
41    }
42
43    #[getter]
44    fn theta(&self) -> f64 {
45        self.theta
46    }
47
48    #[getter]
49    fn rho(&self) -> f64 {
50        self.rho
51    }
52}
53
54#[cfg(feature = "python")]
55#[pymethods]
56#[pyo3_stub_gen::derive::gen_stub_pymethods]
57impl GreeksData {
58    #[classmethod]
59    #[pyo3(name = "from_delta", signature = (instrument_id, delta, multiplier, ts_event=0))]
60    fn py_from_delta(
61        _cls: &Bound<'_, PyType>,
62        instrument_id: crate::identifiers::InstrumentId,
63        delta: f64,
64        multiplier: f64,
65        ts_event: u64,
66    ) -> Self {
67        Self::from_delta(instrument_id, delta, multiplier, UnixNanos::from(ts_event))
68    }
69
70    /// Returns portfolio values with `pnl`, `price`, and all Greeks scaled by the contract multiplier.
71    ///
72    /// Preserves both timestamps and leaves position quantity scaling to the caller.
73    #[pyo3(name = "to_portfolio_greeks")]
74    fn py_to_portfolio_greeks(&self) -> PortfolioGreeks {
75        self.to_portfolio_greeks()
76    }
77
78    fn __rmul__(&self, scalar: f64) -> PortfolioGreeks {
79        scalar * &self.to_portfolio_greeks()
80    }
81
82    #[getter]
83    fn instrument_id(&self) -> crate::identifiers::InstrumentId {
84        self.instrument_id
85    }
86
87    #[getter]
88    fn is_call(&self) -> bool {
89        self.is_call
90    }
91
92    #[getter]
93    fn strike(&self) -> f64 {
94        self.strike
95    }
96
97    #[getter]
98    fn expiry(&self) -> i32 {
99        self.expiry
100    }
101
102    #[getter]
103    fn expiry_in_days(&self) -> i32 {
104        self.expiry_in_days
105    }
106
107    #[getter]
108    fn expiry_in_years(&self) -> f64 {
109        self.expiry_in_years
110    }
111
112    #[getter]
113    fn multiplier(&self) -> f64 {
114        self.multiplier
115    }
116
117    #[getter]
118    fn quantity(&self) -> f64 {
119        self.quantity
120    }
121
122    #[getter]
123    fn underlying_price(&self) -> f64 {
124        self.underlying_price
125    }
126
127    #[getter]
128    fn interest_rate(&self) -> f64 {
129        self.interest_rate
130    }
131
132    #[getter]
133    fn cost_of_carry(&self) -> f64 {
134        self.cost_of_carry
135    }
136
137    #[getter]
138    fn vol(&self) -> f64 {
139        self.vol
140    }
141
142    #[getter]
143    fn pnl(&self) -> f64 {
144        self.pnl
145    }
146
147    #[getter]
148    fn price(&self) -> f64 {
149        self.price
150    }
151
152    #[getter]
153    fn delta(&self) -> f64 {
154        self.greeks.delta
155    }
156
157    #[getter]
158    fn gamma(&self) -> f64 {
159        self.greeks.gamma
160    }
161
162    #[getter]
163    fn vega(&self) -> f64 {
164        self.greeks.vega
165    }
166
167    #[getter]
168    fn theta(&self) -> f64 {
169        self.greeks.theta
170    }
171
172    #[getter]
173    fn rho(&self) -> f64 {
174        self.greeks.rho
175    }
176
177    #[getter]
178    fn itm_prob(&self) -> f64 {
179        self.itm_prob
180    }
181
182    #[getter]
183    fn ts_event(&self) -> u64 {
184        self.ts_event.as_u64()
185    }
186
187    #[getter]
188    fn ts_init(&self) -> u64 {
189        self.ts_init.as_u64()
190    }
191}
192
193#[cfg(feature = "python")]
194#[pymethods]
195#[pyo3_stub_gen::derive::gen_stub_pymethods]
196impl PortfolioGreeks {
197    fn __rmul__(&self, scalar: f64) -> Self {
198        scalar * self
199    }
200
201    fn __add__(&self, other: &Self) -> Self {
202        self.clone() + other.clone()
203    }
204
205    #[getter]
206    fn pnl(&self) -> f64 {
207        self.pnl
208    }
209
210    #[getter]
211    fn price(&self) -> f64 {
212        self.price
213    }
214
215    #[getter]
216    fn delta(&self) -> f64 {
217        self.greeks.delta
218    }
219
220    #[getter]
221    fn gamma(&self) -> f64 {
222        self.greeks.gamma
223    }
224
225    #[getter]
226    fn vega(&self) -> f64 {
227        self.greeks.vega
228    }
229
230    #[getter]
231    fn theta(&self) -> f64 {
232        self.greeks.theta
233    }
234
235    #[getter]
236    fn rho(&self) -> f64 {
237        self.greeks.rho
238    }
239
240    #[getter]
241    fn ts_event(&self) -> u64 {
242        self.ts_event.as_u64()
243    }
244
245    #[getter]
246    fn ts_init(&self) -> u64 {
247        self.ts_init.as_u64()
248    }
249}
250
251#[cfg(feature = "python")]
252#[pymethods]
253#[pyo3_stub_gen::derive::gen_stub_pymethods]
254impl YieldCurve {
255    #[new]
256    fn py_new(
257        curve_name: String,
258        tenors: Vec<f64>,
259        interest_rates: Vec<f64>,
260        ts_event: u64,
261        ts_init: u64,
262    ) -> PyResult<Self> {
263        let curve = Self::new(
264            curve_name,
265            tenors,
266            interest_rates,
267            ts_event.into(),
268            ts_init.into(),
269        );
270        curve.validate().map_err(to_pyvalue_err)?;
271        Ok(curve)
272    }
273
274    fn __repr__(&self) -> String {
275        self.to_string()
276    }
277
278    #[getter]
279    fn curve_name(&self) -> &str {
280        &self.curve_name
281    }
282
283    #[getter]
284    fn tenors(&self) -> Vec<f64> {
285        self.tenors.clone()
286    }
287
288    #[getter]
289    fn interest_rates(&self) -> Vec<f64> {
290        self.interest_rates.clone()
291    }
292
293    #[getter]
294    fn ts_event(&self) -> u64 {
295        self.ts_event.as_u64()
296    }
297
298    #[getter]
299    fn ts_init(&self) -> u64 {
300        self.ts_init.as_u64()
301    }
302}
303
304#[cfg(feature = "python")]
305#[pymethods]
306#[pyo3_stub_gen::derive::gen_stub_pymethods]
307impl BlackScholesGreeksResult {
308    #[getter]
309    fn price(&self) -> f64 {
310        self.price
311    }
312
313    #[getter]
314    fn vol(&self) -> f64 {
315        self.vol
316    }
317
318    #[getter]
319    fn delta(&self) -> f64 {
320        self.delta
321    }
322
323    #[getter]
324    fn gamma(&self) -> f64 {
325        self.gamma
326    }
327
328    #[getter]
329    fn vega(&self) -> f64 {
330        self.vega
331    }
332
333    #[getter]
334    fn theta(&self) -> f64 {
335        self.theta
336    }
337
338    #[getter]
339    fn itm_prob(&self) -> f64 {
340        self.itm_prob
341    }
342
343    #[getter]
344    fn rho(&self) -> f64 {
345        self.rho
346    }
347}
348
349/// Computes Black-Scholes greeks using the fast `compute_greeks` implementation.
350/// This function uses `compute_greeks` from `black_scholes.rs` which is optimized for performance.
351#[doc = "
352
353`db_dr` is zero for fixed carry and one for carry defined as `r - q`, including `r == q`.
354Rho is per percentage point.
355The exact implementation is used when the fast result is nonfinite or inconsistent.
356
357# Errors
358
359Returns an error if:
360- Model inputs are invalid.
361- Valuation results are nonfinite or inconsistent."]
362#[pyfunction]
363#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.model")]
364#[pyo3(name = "black_scholes_greeks", signature = (s, r, b, vol, is_call, k, t, db_dr=0.0))]
365#[expect(clippy::too_many_arguments)]
366pub fn py_black_scholes_greeks(
367    s: f64,
368    r: f64,
369    b: f64,
370    vol: f64,
371    is_call: bool,
372    k: f64,
373    t: f64,
374    db_dr: f64,
375) -> PyResult<BlackScholesGreeksResult> {
376    black_scholes_greeks_checked(s, r, b, vol, is_call, k, t, db_dr).map_err(to_pyvalue_err)
377}
378
379/// Computes the implied volatility for an option given its parameters and market price.
380///
381/// # Errors
382///
383/// Returns an error if implied volatility calculation fails.
384#[pyfunction]
385#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.model")]
386#[pyo3(name = "imply_vol")]
387pub fn py_imply_vol(
388    s: f64,
389    r: f64,
390    b: f64,
391    is_call: bool,
392    k: f64,
393    t: f64,
394    price: f64,
395) -> PyResult<f64> {
396    imply_vol_checked(s, r, b, is_call, k, t, price).map_err(to_pyvalue_err)
397}
398
399/// Computes implied volatility and greeks using the fast implementations.
400/// This function uses `compute_greeks` after implying volatility.
401#[doc = "
402
403`db_dr` is zero for fixed carry and one for carry defined as `r - q`, including `r == q`.
404Rho is per percentage point.
405The exact implementation is used when the fast kernel cannot reprice the solved premium
406within 0.01 percent.
407
408# Errors
409
410Returns an error if:
411- Model inputs or the premium are invalid.
412- The premium has no finite positive implied volatility.
413- Results are nonfinite, inconsistent, or fail the 0.01 percent repricing tolerance."]
414#[pyfunction]
415#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.model")]
416#[pyo3(name = "imply_vol_and_greeks", signature = (s, r, b, is_call, k, t, price, db_dr=0.0))]
417#[expect(clippy::too_many_arguments)]
418pub fn py_imply_vol_and_greeks(
419    s: f64,
420    r: f64,
421    b: f64,
422    is_call: bool,
423    k: f64,
424    t: f64,
425    price: f64,
426    db_dr: f64,
427) -> PyResult<BlackScholesGreeksResult> {
428    imply_vol_and_greeks_checked(s, r, b, is_call, k, t, price, db_dr, None).map_err(to_pyvalue_err)
429}
430
431/// Refines implied volatility using an initial guess and computes greeks.
432/// This function uses `compute_iv_and_greeks` which performs a Halley iteration
433/// to refine the volatility estimate from an initial guess.
434#[doc = "
435
436`db_dr` is zero for fixed carry and one for carry defined as `r - q`, including `r == q`.
437Rho is per percentage point.
438
439Refinement falls back to the full implied-volatility solver when its volatility reaches a
440clamp bound or it fails to reprice the premium within 0.01 percent.
441The exact implementation is used when the fast kernel cannot reprice the solved premium
442within that tolerance.
443
444# Errors
445
446Returns an error if:
447- Model inputs or the premium are invalid.
448- The premium has no finite positive implied volatility.
449- Results are nonfinite, inconsistent, or fail the 0.01 percent repricing tolerance."]
450#[pyfunction]
451#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.model")]
452#[pyo3(name = "refine_vol_and_greeks", signature = (s, r, b, is_call, k, t, target_price, initial_vol, db_dr=0.0))]
453#[expect(clippy::too_many_arguments)]
454pub fn py_refine_vol_and_greeks(
455    s: f64,
456    r: f64,
457    b: f64,
458    is_call: bool,
459    k: f64,
460    t: f64,
461    target_price: f64,
462    initial_vol: f64,
463    db_dr: f64,
464) -> PyResult<BlackScholesGreeksResult> {
465    imply_vol_and_greeks_checked(
466        s,
467        r,
468        b,
469        is_call,
470        k,
471        t,
472        target_price,
473        db_dr,
474        Some(initial_vol),
475    )
476    .map_err(to_pyvalue_err)
477}