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}