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 (-0.5 * x * x).exp() / (2.0_f64 * std::f64::consts::PI).sqrt()
42}
43
44#[inline]
46fn big_phi(x: f64) -> f64 {
47 let t = 1.0 / (1.0 + 0.2316419 * x.abs());
48 let poly = t * (0.319_381_530
49 + t * (-0.356_563_782
50 + t * (1.781_477_937 + t * (-1.821_255_978 + t * 1.330_274_429))));
51 let cdf_pos = 1.0 - phi(x) * poly;
52 if x >= 0.0 { cdf_pos } else { 1.0 - cdf_pos }
53}
54
55fn to_f64(d: Decimal) -> Result<f64, FinError> {
56 d.to_f64().ok_or(FinError::ArithmeticOverflow)
57}
58
59fn from_f64(f: f64) -> Result<Decimal, FinError> {
60 if !f.is_finite() {
61 return Err(FinError::ArithmeticOverflow);
62 }
63 Decimal::try_from(f).map_err(|_| FinError::ArithmeticOverflow)
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
70pub enum OptionKind {
71 Call,
73 Put,
75}
76
77#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
79pub struct OptionSpec {
80 pub kind: OptionKind,
82 pub spot: Decimal,
84 pub strike: Decimal,
86 pub time_to_expiry: Decimal,
88 pub risk_free_rate: Decimal,
90 pub volatility: Decimal,
92}
93
94#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
96pub struct OptionGreeks {
97 pub price: Decimal,
99 pub delta: Decimal,
101 pub gamma: Decimal,
103 pub theta: Decimal,
105 pub vega: Decimal,
107 pub rho: Decimal,
109}
110
111pub struct BlackScholes;
115
116impl BlackScholes {
117 pub fn price(spec: &OptionSpec) -> Result<OptionGreeks, FinError> {
124 Self::validate(spec)?;
125
126 let s = to_f64(spec.spot)?;
127 let k = to_f64(spec.strike)?;
128 let t = to_f64(spec.time_to_expiry)?;
129 let r = to_f64(spec.risk_free_rate)?;
130 let v = to_f64(spec.volatility)?;
131
132 let sqrt_t = t.sqrt();
133 let d1 = ((s / k).ln() + (r + 0.5 * v * v) * t) / (v * sqrt_t);
134 let d2 = d1 - v * sqrt_t;
135
136 let (price_f, delta_f, rho_f) = match spec.kind {
137 OptionKind::Call => {
138 let price = s * big_phi(d1) - k * (-r * t).exp() * big_phi(d2);
139 let delta = big_phi(d1);
140 let rho = k * t * (-r * t).exp() * big_phi(d2) * 0.01;
141 (price, delta, rho)
142 }
143 OptionKind::Put => {
144 let price = k * (-r * t).exp() * big_phi(-d2) - s * big_phi(-d1);
145 let delta = big_phi(d1) - 1.0;
146 let rho = -k * t * (-r * t).exp() * big_phi(-d2) * 0.01;
147 (price, delta, rho)
148 }
149 };
150
151 let gamma_f = phi(d1) / (s * v * sqrt_t);
152 let theta_f = match spec.kind {
154 OptionKind::Call => {
155 (-s * phi(d1) * v / (2.0 * sqrt_t)
156 - r * k * (-r * t).exp() * big_phi(d2))
157 / 365.0
158 }
159 OptionKind::Put => {
160 (-s * phi(d1) * v / (2.0 * sqrt_t)
161 + r * k * (-r * t).exp() * big_phi(-d2))
162 / 365.0
163 }
164 };
165 let vega_f = s * sqrt_t * phi(d1) * 0.01;
167
168 Ok(OptionGreeks {
169 price: from_f64(price_f)?,
170 delta: from_f64(delta_f)?,
171 gamma: from_f64(gamma_f)?,
172 theta: from_f64(theta_f)?,
173 vega: from_f64(vega_f)?,
174 rho: from_f64(rho_f)?,
175 })
176 }
177
178 pub fn implied_volatility(
189 market_price: Decimal,
190 spot: Decimal,
191 strike: Decimal,
192 time_to_expiry: Decimal,
193 risk_free_rate: Decimal,
194 kind: OptionKind,
195 max_iter: usize,
196 tolerance: Decimal,
197 ) -> Result<Decimal, FinError> {
198 if market_price <= Decimal::ZERO {
199 return Err(FinError::InvalidInput(
200 "Market price must be positive for IV solve".to_owned(),
201 ));
202 }
203 let tol_f = to_f64(tolerance)?;
204 let target = to_f64(market_price)?;
205
206 let s_f = to_f64(spot)?;
208 let k_f = to_f64(strike)?;
209 let t_f = to_f64(time_to_expiry)?;
210 let mut sigma = (2.0 * std::f64::consts::PI / t_f).sqrt() * (target / s_f);
211 sigma = sigma.clamp(1e-6, 10.0);
212
213 for _ in 0..max_iter {
214 let vol_dec = from_f64(sigma)?;
215 let spec = OptionSpec {
216 kind,
217 spot,
218 strike,
219 time_to_expiry,
220 risk_free_rate,
221 volatility: vol_dec,
222 };
223 let greeks = Self::price(&spec)?;
224 let price_f = to_f64(greeks.price)?;
225 let vega_f = to_f64(greeks.vega)? * 100.0; let diff = price_f - target;
228 if diff.abs() < tol_f {
229 return from_f64(sigma);
230 }
231 if vega_f.abs() < 1e-12 {
232 return Err(FinError::InvalidInput(
233 "Implied volatility solver: vega near zero, cannot converge".to_owned(),
234 ));
235 }
236 sigma -= diff / vega_f;
237 sigma = sigma.clamp(1e-6, 10.0);
238
239 let _ = (s_f, k_f); }
242
243 Err(FinError::InvalidInput(format!(
244 "Implied volatility solver did not converge in {max_iter} iterations"
245 )))
246 }
247
248 fn validate(spec: &OptionSpec) -> Result<(), FinError> {
249 if spec.spot <= Decimal::ZERO {
250 return Err(FinError::InvalidPrice(spec.spot));
251 }
252 if spec.strike <= Decimal::ZERO {
253 return Err(FinError::InvalidPrice(spec.strike));
254 }
255 if spec.time_to_expiry <= Decimal::ZERO {
256 return Err(FinError::InvalidInput(
257 "time_to_expiry must be positive".to_owned(),
258 ));
259 }
260 if spec.volatility <= Decimal::ZERO {
261 return Err(FinError::InvalidInput(
262 "volatility must be positive".to_owned(),
263 ));
264 }
265 Ok(())
266 }
267}
268
269#[cfg(test)]
272mod tests {
273 use super::*;
274 use rust_decimal_macros::dec;
275
276 fn atm_call() -> OptionSpec {
277 OptionSpec {
278 kind: OptionKind::Call,
279 spot: dec!(100),
280 strike: dec!(100),
281 time_to_expiry: dec!(1),
282 risk_free_rate: dec!(0.05),
283 volatility: dec!(0.2),
284 }
285 }
286
287 #[test]
288 fn test_call_price_positive() {
289 let g = BlackScholes::price(&atm_call()).unwrap();
290 assert!(g.price > Decimal::ZERO);
291 }
292
293 #[test]
294 fn test_put_call_parity() {
295 let spec = atm_call();
297 let call = BlackScholes::price(&spec).unwrap();
298 let put_spec = OptionSpec { kind: OptionKind::Put, ..spec };
299 let put = BlackScholes::price(&put_spec).unwrap();
300 let diff = (call.price - put.price).abs();
302 assert!(diff > dec!(4) && diff < dec!(6), "put-call parity failed: {diff}");
304 }
305
306 #[test]
307 fn test_delta_call_between_zero_and_one() {
308 let g = BlackScholes::price(&atm_call()).unwrap();
309 assert!(g.delta > Decimal::ZERO && g.delta < dec!(1));
310 }
311
312 #[test]
313 fn test_gamma_positive() {
314 let g = BlackScholes::price(&atm_call()).unwrap();
315 assert!(g.gamma > Decimal::ZERO);
316 }
317
318 #[test]
319 fn test_vega_positive() {
320 let g = BlackScholes::price(&atm_call()).unwrap();
321 assert!(g.vega > Decimal::ZERO);
322 }
323
324 #[test]
325 fn test_invalid_spot_errors() {
326 let mut spec = atm_call();
327 spec.spot = dec!(0);
328 assert!(matches!(BlackScholes::price(&spec), Err(FinError::InvalidPrice(_))));
329 }
330
331 #[test]
332 fn test_implied_volatility_roundtrip() {
333 let spec = atm_call();
334 let g = BlackScholes::price(&spec).unwrap();
335 let iv = BlackScholes::implied_volatility(
336 g.price,
337 spec.spot,
338 spec.strike,
339 spec.time_to_expiry,
340 spec.risk_free_rate,
341 spec.kind,
342 200,
343 dec!(0.0001),
344 )
345 .unwrap();
346 let diff = (iv - spec.volatility).abs();
347 assert!(diff < dec!(0.001), "IV roundtrip error too large: {diff}");
348 }
349}