nautilus_model/data/
greeks.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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 implied_vol::{implied_black_volatility, norm_cdf, norm_pdf};
17
18#[repr(C)]
19#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
20#[cfg_attr(
21    feature = "python",
22    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
23)]
24pub struct BlackScholesGreeksResult {
25    pub price: f64,
26    pub delta: f64,
27    pub gamma: f64,
28    pub vega: f64,
29    pub theta: f64,
30}
31
32// dS_t = S_t * (b * dt + sigma * dW_t) (stock)
33// dC_t = r * C_t * dt (cash numeraire)
34#[allow(clippy::too_many_arguments)]
35pub fn black_scholes_greeks(
36    s: f64,
37    r: f64,
38    b: f64,
39    sigma: f64,
40    is_call: bool,
41    k: f64,
42    t: f64,
43    multiplier: f64,
44) -> BlackScholesGreeksResult {
45    let phi = if is_call { 1.0 } else { -1.0 };
46    let scaled_vol = sigma * t.sqrt();
47    let d1 = ((s / k).ln() + (b + 0.5 * sigma.powi(2)) * t) / scaled_vol;
48    let d2 = d1 - scaled_vol;
49    let cdf_phi_d1 = norm_cdf(phi * d1);
50    let cdf_phi_d2 = norm_cdf(phi * d2);
51    let dist_d1 = norm_pdf(d1);
52    let df = ((b - r) * t).exp();
53    let s_t = s * df;
54    let k_t = k * (-r * t).exp();
55
56    let price = multiplier * phi * (s_t * cdf_phi_d1 - k_t * cdf_phi_d2);
57    let delta = multiplier * phi * df * cdf_phi_d1;
58    let gamma = multiplier * df * dist_d1 / (s * scaled_vol);
59    let vega = multiplier * s_t * t.sqrt() * dist_d1 * 0.01; // in absolute percent change
60    let theta = multiplier
61        * (s_t * (-dist_d1 * sigma / (2.0 * t.sqrt()) - phi * (b - r) * cdf_phi_d1)
62            - phi * r * k_t * cdf_phi_d2)
63        * 0.0027378507871321013; // 1 / 365.25 in change per calendar day
64
65    BlackScholesGreeksResult {
66        price,
67        delta,
68        gamma,
69        vega,
70        theta,
71    }
72}
73
74pub fn imply_vol(s: f64, r: f64, b: f64, is_call: bool, k: f64, t: f64, price: f64) -> f64 {
75    let forward = s * b.exp();
76    let forward_price = price * (r * t).exp();
77
78    implied_black_volatility(forward_price, forward, k, t, is_call)
79}
80
81#[repr(C)]
82#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
83#[cfg_attr(
84    feature = "python",
85    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
86)]
87pub struct ImplyVolAndGreeksResult {
88    pub vol: f64,
89    pub price: f64,
90    pub delta: f64,
91    pub gamma: f64,
92    pub vega: f64,
93    pub theta: f64,
94}
95
96#[allow(clippy::too_many_arguments)]
97pub fn imply_vol_and_greeks(
98    s: f64,
99    r: f64,
100    b: f64,
101    is_call: bool,
102    k: f64,
103    t: f64,
104    price: f64,
105    multiplier: f64,
106) -> ImplyVolAndGreeksResult {
107    let vol = imply_vol(s, r, b, is_call, k, t, price);
108    let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t, multiplier);
109
110    ImplyVolAndGreeksResult {
111        vol,
112        price: greeks.price,
113        delta: greeks.delta,
114        gamma: greeks.gamma,
115        vega: greeks.vega,
116        theta: greeks.theta,
117    }
118}
119
120////////////////////////////////////////////////////////////////////////////////
121// Tests
122////////////////////////////////////////////////////////////////////////////////
123
124#[cfg(test)]
125mod tests {
126    use rstest::rstest;
127
128    use super::*;
129
130    #[rstest]
131    fn test_greeks_accuracy_call() {
132        let s = 100.0;
133        let k = 100.1;
134        let t = 1.0;
135        let r = 0.01;
136        let b = 0.005;
137        let sigma = 0.2;
138        let is_call = true;
139        let eps = 1e-3;
140
141        let greeks = black_scholes_greeks(s, r, b, sigma, is_call, k, t, 1.0);
142
143        let price0 = |s: f64| black_scholes_greeks(s, r, b, sigma, is_call, k, t, 1.0).price;
144
145        let delta_bnr = (price0(s + eps) - price0(s - eps)) / (2.0 * eps);
146        let gamma_bnr = (price0(s + eps) + price0(s - eps) - 2.0 * price0(s)) / (eps * eps);
147        let vega_bnr = (black_scholes_greeks(s, r, b, sigma + eps, is_call, k, t, 1.0).price
148            - black_scholes_greeks(s, r, b, sigma - eps, is_call, k, t, 1.0).price)
149            / (2.0 * eps)
150            / 100.0;
151        let theta_bnr = (black_scholes_greeks(s, r, b, sigma, is_call, k, t - eps, 1.0).price
152            - black_scholes_greeks(s, r, b, sigma, is_call, k, t + eps, 1.0).price)
153            / (2.0 * eps)
154            / 365.25;
155
156        let tolerance = 1e-5;
157        assert!(
158            (greeks.delta - delta_bnr).abs() < tolerance,
159            "Delta difference exceeds tolerance"
160        );
161        assert!(
162            (greeks.gamma - gamma_bnr).abs() < tolerance,
163            "Gamma difference exceeds tolerance"
164        );
165        assert!(
166            (greeks.vega - vega_bnr).abs() < tolerance,
167            "Vega difference exceeds tolerance"
168        );
169        assert!(
170            (greeks.theta - theta_bnr).abs() < tolerance,
171            "Theta difference exceeds tolerance"
172        );
173    }
174
175    #[rstest]
176    fn test_greeks_accuracy_put() {
177        let s = 100.0;
178        let k = 100.1;
179        let t = 1.0;
180        let r = 0.01;
181        let b = 0.005;
182        let sigma = 0.2;
183        let is_call = false;
184        let eps = 1e-3;
185
186        let greeks = black_scholes_greeks(s, r, b, sigma, is_call, k, t, 1.0);
187
188        let price0 = |s: f64| black_scholes_greeks(s, r, b, sigma, is_call, k, t, 1.0).price;
189
190        let delta_bnr = (price0(s + eps) - price0(s - eps)) / (2.0 * eps);
191        let gamma_bnr = (price0(s + eps) + price0(s - eps) - 2.0 * price0(s)) / (eps * eps);
192        let vega_bnr = (black_scholes_greeks(s, r, b, sigma + eps, is_call, k, t, 1.0).price
193            - black_scholes_greeks(s, r, b, sigma - eps, is_call, k, t, 1.0).price)
194            / (2.0 * eps)
195            / 100.0;
196        let theta_bnr = (black_scholes_greeks(s, r, b, sigma, is_call, k, t - eps, 1.0).price
197            - black_scholes_greeks(s, r, b, sigma, is_call, k, t + eps, 1.0).price)
198            / (2.0 * eps)
199            / 365.25;
200
201        let tolerance = 1e-5;
202        assert!(
203            (greeks.delta - delta_bnr).abs() < tolerance,
204            "Delta difference exceeds tolerance"
205        );
206        assert!(
207            (greeks.gamma - gamma_bnr).abs() < tolerance,
208            "Gamma difference exceeds tolerance"
209        );
210        assert!(
211            (greeks.vega - vega_bnr).abs() < tolerance,
212            "Vega difference exceeds tolerance"
213        );
214        assert!(
215            (greeks.theta - theta_bnr).abs() < tolerance,
216            "Theta difference exceeds tolerance"
217        );
218    }
219
220    #[rstest]
221    fn test_imply_vol_and_greeks_accuracy_call() {
222        let s = 100.0;
223        let k = 100.1;
224        let t = 1.0;
225        let r = 0.01;
226        let b = 0.005;
227        let sigma = 0.2;
228        let is_call = true;
229
230        let base_greeks = black_scholes_greeks(s, r, b, sigma, is_call, k, t, 1.0);
231        let price = base_greeks.price;
232
233        let implied_result = imply_vol_and_greeks(s, r, b, is_call, k, t, price, 1.0);
234
235        let tolerance = 1e-5;
236        assert!(
237            (implied_result.vol - sigma).abs() < tolerance,
238            "Vol difference exceeds tolerance"
239        );
240        assert!(
241            (implied_result.price - base_greeks.price).abs() < tolerance,
242            "Price difference exceeds tolerance"
243        );
244        assert!(
245            (implied_result.delta - base_greeks.delta).abs() < tolerance,
246            "Delta difference exceeds tolerance"
247        );
248        assert!(
249            (implied_result.gamma - base_greeks.gamma).abs() < tolerance,
250            "Gamma difference exceeds tolerance"
251        );
252        assert!(
253            (implied_result.vega - base_greeks.vega).abs() < tolerance,
254            "Vega difference exceeds tolerance"
255        );
256        assert!(
257            (implied_result.theta - base_greeks.theta).abs() < tolerance,
258            "Theta difference exceeds tolerance"
259        );
260    }
261
262    #[rstest]
263    fn test_imply_vol_and_greeks_accuracy_put() {
264        let s = 100.0;
265        let k = 100.1;
266        let t = 1.0;
267        let r = 0.01;
268        let b = 0.005;
269        let sigma = 0.2;
270        let is_call = false;
271
272        let base_greeks = black_scholes_greeks(s, r, b, sigma, is_call, k, t, 1.0);
273        let price = base_greeks.price;
274
275        let implied_result = imply_vol_and_greeks(s, r, b, is_call, k, t, price, 1.0);
276
277        let tolerance = 1e-5;
278        assert!(
279            (implied_result.vol - sigma).abs() < tolerance,
280            "Vol difference exceeds tolerance"
281        );
282        assert!(
283            (implied_result.price - base_greeks.price).abs() < tolerance,
284            "Price difference exceeds tolerance"
285        );
286        assert!(
287            (implied_result.delta - base_greeks.delta).abs() < tolerance,
288            "Delta difference exceeds tolerance"
289        );
290        assert!(
291            (implied_result.gamma - base_greeks.gamma).abs() < tolerance,
292            "Gamma difference exceeds tolerance"
293        );
294        assert!(
295            (implied_result.vega - base_greeks.vega).abs() < tolerance,
296            "Vega difference exceeds tolerance"
297        );
298        assert!(
299            (implied_result.theta - base_greeks.theta).abs() < tolerance,
300            "Theta difference exceeds tolerance"
301        );
302    }
303}