blackscholes_python/lib.rs
1//! # blackscholes_python
2//! This library provides an simple, lightweight, and efficient (though not heavily optimized) implementation of the Black-Scholes-Merton model for pricing European options.
3//!
4//! ## Usage
5//! Simply create an instance of the `Inputs` struct and call the desired method.
6//!
7//! Example:
8//! ```
9//! let inputs: blackscholes::Inputs = blackscholes::Inputs.new(blackscholes::OptionType::Call, 100.0, 100.0, None, 0.05, 0.02, 20.0 / 365.25, Some(0.2));
10//! let price: f64 = inputs.calc_price();
11//! ```
12//!
13//! See the [Github Repo](https://github.com/hayden4r4/blackscholes-rust/tree/python_package) for full source code. Other implementations such as a [npm WASM package](https://www.npmjs.com/package/@haydenr4/blackscholes_wasm) and a [pure Rust Crate](https://crates.io/crates/blackscholes) are also available.
14
15use statrs::distribution::{Continuous, ContinuousCDF, Normal};
16use std::f64::consts::{E, PI};
17use std::fmt::{Display, Formatter, Result};
18
19use pyo3::prelude::*;
20
21#[pymodule]
22fn blackscholes_python(_py: Python, m: &PyModule) -> PyResult<()> {
23 m.add_class::<OptionType>()?;
24 m.add_class::<Inputs>()?;
25 Ok(())
26}
27
28/// The type of option to be priced. Call or Put.
29#[derive(Debug, Clone, Eq, PartialEq)]
30#[pyclass(text_signature = "(Call, Put, /)")]
31pub enum OptionType {
32 Call,
33 Put,
34}
35
36impl Display for OptionType {
37 fn fmt(&self, f: &mut Formatter) -> Result {
38 match self {
39 OptionType::Call => write!(f, "Call"),
40 OptionType::Put => write!(f, "Put"),
41 }
42 }
43}
44
45#[pymethods]
46impl OptionType {
47 /// Creates an instance of the `OptionType` enum.
48 /// # Arguments
49 /// * `option_type` - The type of option to be priced. Call or Put.
50 /// # Example
51 /// ```
52 /// use blackscholes::OptionType;
53 /// let option_type: OptionType = OptionType::Call;
54 /// ```
55 /// # Returns
56 /// An instance of the `OptionType` enum.
57 #[new]
58 pub fn new(option_type: &str) -> Self {
59 match option_type {
60 "Call" => OptionType::Call,
61 "Put" => OptionType::Put,
62 _ => panic!("Option type must be either Call or Put"),
63 }
64 }
65 /// # Returns
66 /// A string representation of the `OptionType` enum.
67 pub fn __str__(&self) -> String {
68 format!("{}", self)
69 }
70}
71
72/// The inputs to the Black-Scholes-Merton model.
73#[derive(Debug, Clone)]
74#[pyclass(text_signature = "(option_type, s, k, p, r, q, t, sigma, /)")]
75pub struct Inputs {
76 /// The type of the option (call or put)
77 pub option_type: OptionType,
78 /// Stock price
79 pub s: f64,
80 /// Strike price
81 pub k: f64,
82 /// Option price
83 pub p: Option<f64>,
84 /// Risk-free rate
85 pub r: f64,
86 /// Dividend yield
87 pub q: f64,
88 /// Time to maturity in years
89 pub t: f64,
90 /// Volatility
91 pub sigma: Option<f64>,
92}
93
94impl Display for Inputs {
95 fn fmt(&self, f: &mut Formatter) -> Result {
96 writeln!(f, "Option type: {}", self.option_type)?;
97 writeln!(f, "Stock price: {:.2}", self.s)?;
98 writeln!(f, "Strike price: {:.2}", self.k)?;
99 match self.p {
100 Some(p) => writeln!(f, "Option price: {:.2}", p)?,
101 None => writeln!(f, "Option price: None")?,
102 }
103 writeln!(f, "Risk-free rate: {:.4}", self.r)?;
104 writeln!(f, "Dividend yield: {:.4}", self.q)?;
105 writeln!(f, "Time to maturity: {:.4}", self.t)?;
106 match self.sigma {
107 Some(sigma) => writeln!(f, "Volatility: {:.4}", sigma)?,
108 None => writeln!(f, "Volatility: None")?,
109 }
110 Ok(())
111 }
112}
113
114/// Calculates the d1, d2, nd1, and nd2 values for the option.
115/// # Returns
116/// Tuple (f64, f64) of the nd1 and nd2 values for the given inputs.
117fn nd1nd2(inputs: &Inputs, normal: bool) -> (f64, f64) {
118 let sigma: f64 = match inputs.sigma {
119 Some(sigma) => sigma,
120 None => panic!("Expected an Option(f64) for inputs.sigma, received None"),
121 };
122
123 let nd1nd2 = {
124 // Calculating numerator of d1
125 let numd1: f64 =
126 (inputs.s / inputs.k).ln() + (inputs.r - inputs.q + (sigma.powi(2)) / 2.0) * inputs.t;
127
128 // Calculating denominator of d1 and d2
129 let den: f64 = sigma * (inputs.t.sqrt());
130
131 let d1: f64 = numd1 / den;
132 let d2: f64 = d1 - den;
133
134 let d1d2: (f64, f64) = (d1, d2);
135
136 // Returns d1 and d2 values if deriving from normal distribution is not necessary
137 // (i.e. gamma, vega, and theta calculations)
138 if !normal {
139 return d1d2;
140 }
141
142 // Creating normal distribution
143 let n: Normal = Normal::new(0.0, 1.0).unwrap();
144
145 // Calculates the nd1 and nd2 values
146 // Checks if OptionType is Call or Put
147 let nd1nd2: (f64, f64) = match inputs.option_type {
148 OptionType::Call => (n.cdf(d1d2.0), n.cdf(d1d2.1)),
149 OptionType::Put => (n.cdf(-d1d2.0), n.cdf(-d1d2.1)),
150 };
151 nd1nd2
152 };
153 nd1nd2
154}
155
156/// # Returns
157/// f64 of the derivative of the nd1.
158fn calc_nprimed1(inputs: &Inputs) -> f64 {
159 let (d1, _): (f64, f64) = nd1nd2(&inputs, false);
160
161 // Generate normal probability distribution
162 let n: Normal = Normal::new(0.0, 1.0).unwrap();
163
164 // Get the standard normal probability density function value of d1
165 let nprimed1: f64 = n.pdf(d1);
166 nprimed1
167}
168
169/// Methods for calculating the price, greeks, and implied volatility of an option.
170#[pymethods]
171impl Inputs {
172 /// Creates instance ot the `Inputs` struct.
173 /// # Arguments
174 /// * `option_type` - The type of option to be priced.
175 /// * `s` - The current price of the underlying asset.
176 /// * `k` - The strike price of the option.
177 /// * `p` - The dividend yield of the underlying asset.
178 /// * `r` - The risk-free interest rate.
179 /// * `q` - The dividend yield of the underlying asset.
180 /// * `t` - The time to maturity of the option in years.
181 /// * `sigma` - The volatility of the underlying asset.
182 /// # Example
183 /// ```
184 /// use blackscholes::Inputs;
185 /// let inputs = Inputs::new(OptionType::Call, 100.0, 100.0, None, 0.05, 0.2, 20/365.25, Some(0.2));
186 /// ```
187 /// # Returns
188 /// An instance of the `Inputs` struct.
189 #[new]
190 pub fn new(
191 option_type: OptionType,
192 s: f64,
193 k: f64,
194 p: Option<f64>,
195 r: f64,
196 q: f64,
197 t: f64,
198 sigma: Option<f64>,
199 ) -> Self {
200 Self {
201 option_type,
202 s,
203 k,
204 p,
205 r,
206 q,
207 t,
208 sigma,
209 }
210 }
211
212 /// # Returns
213 /// string representation of the `Inputs` struct.
214 pub fn __str__(&self) -> String {
215 format!(
216 "OptionType: {}, S: {}, K: {}, P: {}, R: {}, Q: {}, T: {}, Sigma: {}",
217 self.option_type,
218 self.s,
219 self.k,
220 match self.p {
221 Some(p) => format!("{}", p),
222 None => "None".to_string(),
223 },
224 self.r,
225 self.q,
226 self.t,
227 match self.sigma {
228 Some(sigma) => format!("{}", sigma),
229 None => "None".to_string(),
230 },
231 )
232 }
233 /// Calculates the price of the option.
234 /// # Requires
235 /// s, k, r, q, t, sigma.
236 /// # Returns
237 /// f64 of the price of the option.
238 /// # Example
239 /// ```
240 /// use blackscholes::Inputs;
241 /// let inputs = Inputs::new(OptionType::Call, 100.0, 100.0, None, 0.05, 0.2, 20/365.25, Some(0.2));
242 /// let price = inputs.calc_price();
243 /// ```
244 pub fn calc_price(&self) -> PyResult<f64> {
245 let (nd1, nd2): (f64, f64) = nd1nd2(self, true);
246 let price: f64 = match self.option_type {
247 OptionType::Call => f64::max(
248 0.0,
249 nd1 * self.s * E.powf(-self.q * self.t) - nd2 * self.k * E.powf(-self.r * self.t),
250 ),
251 OptionType::Put => f64::max(
252 0.0,
253 nd2 * self.k * E.powf(-self.r * self.t) - nd1 * self.s * E.powf(-self.q * self.t),
254 ),
255 };
256 Ok(price)
257 }
258 /// Calculates the delta of the option.
259 /// # Requires
260 /// s, k, r, q, t, sigma
261 /// # Returns
262 /// f64 of the delta of the option.
263 /// # Example
264 /// ```
265 /// use blackscholes::Inputs;
266 /// let inputs = Inputs::new(OptionType::Call, 100.0, 100.0, None, 0.05, 0.2, 20/365.25, Some(0.2));
267 /// let delta = inputs.calc_delta();
268 /// ```
269 pub fn calc_delta(&self) -> PyResult<f64> {
270 let (nd1, _): (f64, f64) = nd1nd2(self, true);
271 let delta: f64 = match self.option_type {
272 OptionType::Call => nd1 * E.powf(-self.q * self.t),
273 OptionType::Put => -nd1 * E.powf(-self.q * self.t),
274 };
275 Ok(delta)
276 }
277
278 /// Calculates the gamma of the option.
279 /// # Requires
280 /// s, k, r, q, t, sigma
281 /// # Returns
282 /// f64 of the gamma of the option.
283 /// # Example
284 /// ```
285 /// use blackscholes::Inputs;
286 /// let inputs = Inputs::new(OptionType::Call, 100.0, 100.0, None, 0.05, 0.2, 20/365.25, Some(0.2));
287 /// let gamma = inputs.calc_gamma();
288 /// ```
289 pub fn calc_gamma(&self) -> PyResult<f64> {
290 let sigma: f64 = match self.sigma {
291 Some(sigma) => sigma,
292 None => panic!("Expected an Option(f64) for self.sigma, received None"),
293 };
294
295 let nprimed1: f64 = calc_nprimed1(self);
296 let gamma: f64 = E.powf(-self.q * self.t) * nprimed1 / (self.s * sigma * self.t.sqrt());
297 Ok(gamma)
298 }
299
300 /// Calculates the theta of the option.
301 /// Uses 365.25 days in a year for calculations.
302 /// # Requires
303 /// s, k, r, q, t, sigma
304 /// # Returns
305 /// f64 of theta per day (not per year).
306 /// # Example
307 /// ```
308 /// use blackscholes::Inputs;
309 /// let inputs = Inputs::new(OptionType::Call, 100.0, 100.0, None, 0.05, 0.2, 20/365.25, Some(0.2));
310 /// let theta = inputs.calc_theta();
311 /// ```
312 pub fn calc_theta(&self) -> PyResult<f64> {
313 let sigma: f64 = match self.sigma {
314 Some(sigma) => sigma,
315 None => panic!("Expected an Option(f64) for self.sigma, received None"),
316 };
317
318 let nprimed1: f64 = calc_nprimed1(self);
319 let (nd1, nd2): (f64, f64) = nd1nd2(self, true);
320
321 // Calculation uses 360 for T: Time of days per year.
322 let theta: f64 = match self.option_type {
323 OptionType::Call => {
324 (-(self.s * sigma * E.powf(-self.q * self.t) * nprimed1 / (2.0 * self.t.sqrt()))
325 - self.r * self.k * E.powf(-self.r * self.t) * nd2
326 + self.q * self.s * E.powf(-self.q * self.t) * nd1)
327 / 365.25
328 }
329 OptionType::Put => {
330 (-(self.s * sigma * E.powf(-self.q * self.t) * nprimed1 / (2.0 * self.t.sqrt()))
331 + self.r * self.k * E.powf(-self.r * self.t) * nd2
332 - self.q * self.s * E.powf(-self.q * self.t) * nd1)
333 / 365.25
334 }
335 };
336 Ok(theta)
337 }
338
339 /// Calculates the vega of the option.
340 /// # Requires
341 /// s, k, r, q, t, sigma
342 /// # Returns
343 /// f64 of the vega of the option.
344 /// # Example
345 /// ```
346 /// use blackscholes::Inputs;
347 /// let inputs = Inputs::new(OptionType::Call, 100.0, 100.0, None, 0.05, 0.2, 20/365.25, Some(0.2));
348 /// let vega = inputs.calc_vega();
349 /// ```
350 pub fn calc_vega(&self) -> PyResult<f64> {
351 let nprimed1: f64 = calc_nprimed1(self);
352 let vega: f64 = 1.0 / 100.0 * self.s * E.powf(-self.q * self.t) * self.t.sqrt() * nprimed1;
353 Ok(vega)
354 }
355
356 /// Calculates the rho of the option.
357 /// # Requires
358 /// s, k, r, q, t, sigma
359 /// # Returns
360 /// f64 of the rho of the option.
361 /// # Example
362 /// ```
363 /// use blackscholes::Inputs;
364 /// let inputs = Inputs::new(OptionType::Call, 100.0, 100.0, None, 0.05, 0.2, 20/365.25, Some(0.2));
365 /// let rho = inputs.calc_rho();
366 /// ```
367 pub fn calc_rho(&self) -> PyResult<f64> {
368 let (_, nd2): (f64, f64) = nd1nd2(self, true);
369 let rho: f64 = match self.option_type {
370 OptionType::Call => 1.0 / 100.0 * self.k * self.t * E.powf(-self.r * self.t) * nd2,
371 OptionType::Put => -1.0 / 100.0 * self.k * self.t * E.powf(-self.r * self.t) * nd2,
372 };
373 Ok(rho)
374 }
375
376 /// Calculates the implied volatility of the option.
377 /// Tolerance is the max error allowed for the implied volatility,
378 /// the lower the tolerance the more iterations will be required.
379 /// Recommended to be a value between 0.001 - 0.0001 for highest efficiency/accuracy.
380 /// Initializes estimation of sigma using Brenn and Subrahmanyam (1998) method of calculating initial iv estimation.
381 /// Uses Newton Raphson algorithm to calculate implied volatility.
382 /// # Requires
383 /// s, k, r, q, t, p
384 /// # Returns
385 /// f64 of the implied volatility of the option.
386 /// # Example:
387 /// ```
388 /// use blackscholes::Inputs;
389 /// let inputs = Inputs::new(OptionType::Call, 100.0, 100.0, Some(10), 0.05, 0.02, 20.0 / 365.25, None);
390 /// let iv = inputs.calc_iv(0.0001);
391 /// ```
392 pub fn calc_iv(&self, tolerance: f64) -> PyResult<f64> {
393 let mut inputs: Inputs = self.clone();
394
395 let p: f64 = match inputs.p {
396 Some(p) => p,
397 None => panic!("inputs.p must contain Some(f64), found None"),
398 };
399 // Initialize estimation of sigma using Brenn and Subrahmanyam (1998) method of calculating initial iv estimation
400 let mut sigma: f64 = (2.0 * PI / inputs.t).sqrt() * (p / inputs.s);
401 // Initialize diff to 100 for use in while loop
402 let mut diff: f64 = 100.0;
403
404 // Uses Newton Raphson algorithm to calculate implied volatility
405 // Test if the difference between calculated option price and actual option price is > tolerance
406 // If so then iterate until the difference is less than tolerance
407 while diff.abs() > tolerance {
408 inputs.sigma = Some(sigma);
409 diff = inputs.calc_price().unwrap() - p;
410 sigma -= diff / (inputs.calc_vega().unwrap() * 100.0);
411 }
412 Ok(sigma)
413 }
414}