1pub mod greeks;
23
24pub mod surface;
26
27pub use greeks::{
28 GreekError, Greeks, OptionParams, OptionType, bsm_greeks, bsm_price, implied_volatility,
29};
30pub use surface::{VolPoint, VolSmile, VolSurface};
31
32use crate::error::FinError;
33use rust_decimal::prelude::ToPrimitive;
34use rust_decimal::Decimal;
35
36#[inline]
40fn phi(x: f64) -> f64 {
41 crate::normal::pdf(x)
42}
43
44#[inline]
46fn big_phi(x: f64) -> f64 {
47 crate::normal::cdf(x)
48}
49
50fn to_f64(d: Decimal) -> Result<f64, FinError> {
51 d.to_f64().ok_or(FinError::ArithmeticOverflow)
52}
53
54fn from_f64(f: f64) -> Result<Decimal, FinError> {
55 if !f.is_finite() {
56 return Err(FinError::ArithmeticOverflow);
57 }
58 Decimal::try_from(f).map_err(|_| FinError::ArithmeticOverflow)
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
66pub enum OptionKind {
67 Call,
69 Put,
71}
72
73#[derive(Debug, Clone, Copy)]
75#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
76pub struct OptionSpec {
77 pub kind: OptionKind,
79 pub spot: Decimal,
81 pub strike: Decimal,
83 pub time_to_expiry: Decimal,
85 pub risk_free_rate: Decimal,
87 pub volatility: Decimal,
89}
90
91#[derive(Debug, Clone, Copy)]
93#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
94pub struct OptionGreeks {
95 pub price: Decimal,
97 pub delta: Decimal,
99 pub gamma: Decimal,
101 pub theta: Decimal,
103 pub vega: Decimal,
105 pub rho: Decimal,
107}
108
109pub struct BlackScholes;
113
114impl BlackScholes {
115 pub fn price(spec: &OptionSpec) -> Result<OptionGreeks, FinError> {
122 Self::validate(spec)?;
123
124 let s = to_f64(spec.spot)?;
125 let k = to_f64(spec.strike)?;
126 let t = to_f64(spec.time_to_expiry)?;
127 let r = to_f64(spec.risk_free_rate)?;
128 let v = to_f64(spec.volatility)?;
129
130 let sqrt_t = t.sqrt();
131 let d1 = ((s / k).ln() + (r + 0.5 * v * v) * t) / (v * sqrt_t);
132 let d2 = d1 - v * sqrt_t;
133
134 let (price_f, delta_f, rho_f) = match spec.kind {
135 OptionKind::Call => {
136 let price = s * big_phi(d1) - k * (-r * t).exp() * big_phi(d2);
137 let delta = big_phi(d1);
138 let rho = k * t * (-r * t).exp() * big_phi(d2) * 0.01;
139 (price, delta, rho)
140 }
141 OptionKind::Put => {
142 let price = k * (-r * t).exp() * big_phi(-d2) - s * big_phi(-d1);
143 let delta = big_phi(d1) - 1.0;
144 let rho = -k * t * (-r * t).exp() * big_phi(-d2) * 0.01;
145 (price, delta, rho)
146 }
147 };
148
149 let gamma_f = phi(d1) / (s * v * sqrt_t);
150 let theta_f = match spec.kind {
152 OptionKind::Call => {
153 (-s * phi(d1) * v / (2.0 * sqrt_t)
154 - r * k * (-r * t).exp() * big_phi(d2))
155 / 365.0
156 }
157 OptionKind::Put => {
158 (-s * phi(d1) * v / (2.0 * sqrt_t)
159 + r * k * (-r * t).exp() * big_phi(-d2))
160 / 365.0
161 }
162 };
163 let vega_f = s * sqrt_t * phi(d1) * 0.01;
165
166 Ok(OptionGreeks {
167 price: from_f64(price_f)?,
168 delta: from_f64(delta_f)?,
169 gamma: from_f64(gamma_f)?,
170 theta: from_f64(theta_f)?,
171 vega: from_f64(vega_f)?,
172 rho: from_f64(rho_f)?,
173 })
174 }
175
176 pub fn implied_volatility(
187 market_price: Decimal,
188 spot: Decimal,
189 strike: Decimal,
190 time_to_expiry: Decimal,
191 risk_free_rate: Decimal,
192 kind: OptionKind,
193 max_iter: usize,
194 tolerance: Decimal,
195 ) -> Result<Decimal, FinError> {
196 if market_price <= Decimal::ZERO {
197 return Err(FinError::InvalidInput(
198 "Market price must be positive for IV solve".to_owned(),
199 ));
200 }
201 let tol_f = to_f64(tolerance)?;
202 let target = to_f64(market_price)?;
203
204 let s_f = to_f64(spot)?;
206 let k_f = to_f64(strike)?;
207 let t_f = to_f64(time_to_expiry)?;
208 let mut sigma = (2.0 * std::f64::consts::PI / t_f).sqrt() * (target / s_f);
209 sigma = sigma.clamp(1e-6, 10.0);
210
211 for _ in 0..max_iter {
212 let vol_dec = from_f64(sigma)?;
213 let spec = OptionSpec {
214 kind,
215 spot,
216 strike,
217 time_to_expiry,
218 risk_free_rate,
219 volatility: vol_dec,
220 };
221 let greeks = Self::price(&spec)?;
222 let price_f = to_f64(greeks.price)?;
223 let vega_f = to_f64(greeks.vega)? * 100.0; let diff = price_f - target;
226 if diff.abs() < tol_f {
227 return from_f64(sigma);
228 }
229 if vega_f.abs() < 1e-12 {
230 return Err(FinError::InvalidInput(
231 "Implied volatility solver: vega near zero, cannot converge".to_owned(),
232 ));
233 }
234 sigma -= diff / vega_f;
235 sigma = sigma.clamp(1e-6, 10.0);
236
237 let _ = (s_f, k_f); }
240
241 Err(FinError::InvalidInput(format!(
242 "Implied volatility solver did not converge in {max_iter} iterations"
243 )))
244 }
245
246 fn validate(spec: &OptionSpec) -> Result<(), FinError> {
247 if spec.spot <= Decimal::ZERO {
248 return Err(FinError::InvalidPrice(spec.spot));
249 }
250 if spec.strike <= Decimal::ZERO {
251 return Err(FinError::InvalidPrice(spec.strike));
252 }
253 if spec.time_to_expiry <= Decimal::ZERO {
254 return Err(FinError::InvalidInput(
255 "time_to_expiry must be positive".to_owned(),
256 ));
257 }
258 if spec.volatility <= Decimal::ZERO {
259 return Err(FinError::InvalidInput(
260 "volatility must be positive".to_owned(),
261 ));
262 }
263 Ok(())
264 }
265}
266
267#[cfg(test)]
270mod tests {
271 use super::*;
272 use rust_decimal_macros::dec;
273
274 fn atm_call() -> OptionSpec {
275 OptionSpec {
276 kind: OptionKind::Call,
277 spot: dec!(100),
278 strike: dec!(100),
279 time_to_expiry: dec!(1),
280 risk_free_rate: dec!(0.05),
281 volatility: dec!(0.2),
282 }
283 }
284
285 #[test]
286 fn test_call_price_positive() {
287 let g = BlackScholes::price(&atm_call()).unwrap();
288 assert!(g.price > Decimal::ZERO);
289 }
290
291 #[test]
292 fn test_put_call_parity() {
293 let spec = atm_call();
295 let call = BlackScholes::price(&spec).unwrap();
296 let put_spec = OptionSpec { kind: OptionKind::Put, ..spec };
297 let put = BlackScholes::price(&put_spec).unwrap();
298 let diff = (call.price - put.price).abs();
300 assert!(diff > dec!(4) && diff < dec!(6), "put-call parity failed: {diff}");
302 }
303
304 #[test]
305 fn test_delta_call_between_zero_and_one() {
306 let g = BlackScholes::price(&atm_call()).unwrap();
307 assert!(g.delta > Decimal::ZERO && g.delta < dec!(1));
308 }
309
310 #[test]
311 fn test_gamma_positive() {
312 let g = BlackScholes::price(&atm_call()).unwrap();
313 assert!(g.gamma > Decimal::ZERO);
314 }
315
316 #[test]
317 fn test_vega_positive() {
318 let g = BlackScholes::price(&atm_call()).unwrap();
319 assert!(g.vega > Decimal::ZERO);
320 }
321
322 #[test]
323 fn test_invalid_spot_errors() {
324 let mut spec = atm_call();
325 spec.spot = dec!(0);
326 assert!(matches!(BlackScholes::price(&spec), Err(FinError::InvalidPrice(_))));
327 }
328
329 #[test]
330 fn test_implied_volatility_roundtrip() {
331 let spec = atm_call();
332 let g = BlackScholes::price(&spec).unwrap();
333 let iv = BlackScholes::implied_volatility(
334 g.price,
335 spec.spot,
336 spec.strike,
337 spec.time_to_expiry,
338 spec.risk_free_rate,
339 spec.kind,
340 200,
341 dec!(0.0001),
342 )
343 .unwrap();
344 let diff = (iv - spec.volatility).abs();
345 assert!(diff < dec!(0.001), "IV roundtrip error too large: {diff}");
346 }
347}