scirs2_integrate/specialized/finance/utils/
math.rs

1//! Mathematical utilities for quantitative finance
2//!
3//! This module provides specialized mathematical functions commonly used in financial
4//! calculations including option pricing formulas, statistical functions, and numerical methods.
5//!
6//! # Features
7//! - Black-Scholes formula and Greeks
8//! - Implied volatility calculation (Newton-Raphson, Brent)
9//! - Normal distribution CDF/PDF (fast approximations)
10//! - Bachelier model (for negative rates)
11//! - Numerical stability helpers
12//!
13//! # Example
14//! ```
15//! use scirs2_integrate::specialized::finance::utils::math::{
16//!     black_scholes_call, implied_volatility_newton, norm_cdf
17//! };
18//!
19//! // Price a call option
20//! let price = black_scholes_call(100.0, 100.0, 1.0, 0.05, 0.2);
21//! assert!(price > 0.0);
22//!
23//! // Calculate implied volatility
24//! let iv = implied_volatility_newton(price, 100.0, 100.0, 1.0, 0.05, true);
25//! ```
26
27use crate::error::{IntegrateError, IntegrateResult as Result};
28use std::f64::consts::{PI, SQRT_2};
29
30/// SABR model parameters
31#[derive(Debug, Clone)]
32pub struct SABRParameters {
33    /// Alpha (initial volatility)
34    pub alpha: f64,
35    /// Beta (elasticity parameter, typically 0, 0.5, or 1)
36    pub beta: f64,
37    /// Rho (correlation between forward and volatility)
38    pub rho: f64,
39    /// Nu (volatility of volatility)
40    pub nu: f64,
41}
42
43impl SABRParameters {
44    /// Create new SABR parameters with validation
45    pub fn new(alpha: f64, beta: f64, rho: f64, nu: f64) -> Result<Self> {
46        if alpha <= 0.0 {
47            return Err(IntegrateError::ValueError(
48                "Alpha must be positive".to_string(),
49            ));
50        }
51        if !(0.0..=1.0).contains(&beta) {
52            return Err(IntegrateError::ValueError(
53                "Beta must be between 0 and 1".to_string(),
54            ));
55        }
56        if !(-1.0..=1.0).contains(&rho) {
57            return Err(IntegrateError::ValueError(
58                "Rho must be between -1 and 1".to_string(),
59            ));
60        }
61        if nu < 0.0 {
62            return Err(IntegrateError::ValueError(
63                "Nu must be non-negative".to_string(),
64            ));
65        }
66
67        Ok(Self {
68            alpha,
69            beta,
70            rho,
71            nu,
72        })
73    }
74
75    /// Calculate implied volatility using SABR formula (Hagan et al. 2002)
76    pub fn implied_volatility(&self, forward: f64, strike: f64, time: f64) -> Result<f64> {
77        if forward <= 0.0 || strike <= 0.0 {
78            return Err(IntegrateError::ValueError(
79                "Forward and strike must be positive".to_string(),
80            ));
81        }
82        if time <= 0.0 {
83            return Err(IntegrateError::ValueError(
84                "Time must be positive".to_string(),
85            ));
86        }
87
88        // ATM case
89        if (forward - strike).abs() / forward < 1e-8 {
90            let f_mid_beta = forward.powf(1.0 - self.beta);
91            let term1 = self.alpha / f_mid_beta;
92
93            let term2 = 1.0
94                + ((1.0 - self.beta).powi(2) / 24.0 * self.alpha.powi(2)
95                    / forward.powf(2.0 - 2.0 * self.beta)
96                    + 0.25 * self.rho * self.beta * self.nu * self.alpha / f_mid_beta
97                    + (2.0 - 3.0 * self.rho.powi(2)) / 24.0 * self.nu.powi(2))
98                    * time;
99
100            return Ok(term1 * term2);
101        }
102
103        // Non-ATM case
104        let log_fk = (forward / strike).ln();
105        let f_k_mid_beta = (forward * strike).powf((1.0 - self.beta) / 2.0);
106
107        let z = (self.nu / self.alpha) * f_k_mid_beta * log_fk;
108
109        // Calculate x(z) with numerical stability
110        let x = if z.abs() < 1e-6 {
111            // Taylor expansion for small z
112            1.0 - 0.5 * self.rho * z
113        } else {
114            let sqrt_term = (1.0 - 2.0 * self.rho * z + z.powi(2)).sqrt();
115            ((sqrt_term + z - self.rho) / (1.0 - self.rho)).ln() / z
116        };
117
118        let numerator = self.alpha;
119        let denominator = f_k_mid_beta
120            * (1.0
121                + (1.0 - self.beta).powi(2) / 24.0 * log_fk.powi(2)
122                + (1.0 - self.beta).powi(4) / 1920.0 * log_fk.powi(4));
123
124        let correction = 1.0
125            + ((1.0 - self.beta).powi(2) / 24.0 * self.alpha.powi(2)
126                / forward.powf(2.0 - 2.0 * self.beta)
127                + 0.25 * self.rho * self.beta * self.nu * self.alpha / f_k_mid_beta
128                + (2.0 - 3.0 * self.rho.powi(2)) / 24.0 * self.nu.powi(2))
129                * time;
130
131        Ok((numerator / denominator) * x * correction)
132    }
133}
134
135// Deprecated placeholder functions for backward compatibility
136#[deprecated(
137    since = "0.1.0-beta.4",
138    note = "Use SABRParameters::implied_volatility instead"
139)]
140pub fn interpolate_smile() -> Result<()> {
141    Err(IntegrateError::ValueError(
142        "Use SABRParameters for smile interpolation".to_string(),
143    ))
144}
145
146#[deprecated(
147    since = "0.1.0-beta.4",
148    note = "Arbitrage checking not yet implemented"
149)]
150pub fn vol_surface_arbitrage_free() -> Result<()> {
151    Err(IntegrateError::ValueError(
152        "Arbitrage-free surface checking not yet implemented".to_string(),
153    ))
154}
155
156/// Standard normal cumulative distribution function
157///
158/// Uses rational approximation with max error < 1.5e-7
159pub fn norm_cdf(x: f64) -> f64 {
160    if x.is_nan() {
161        return f64::NAN;
162    }
163    if x.is_infinite() {
164        return if x > 0.0 { 1.0 } else { 0.0 };
165    }
166
167    // Abramowitz and Stegun approximation (1964)
168    // Maximum error: 1.5×10^-7
169    let abs_x = x.abs();
170
171    if abs_x > 10.0 {
172        // For large |x|, return extremes directly
173        return if x > 0.0 { 1.0 } else { 0.0 };
174    }
175
176    let t = 1.0 / (1.0 + 0.2316419 * abs_x);
177    let poly = t
178        * (0.319381530
179            + t * (-0.356563782 + t * (1.781477937 + t * (-1.821255978 + t * 1.330274429))));
180
181    let pdf = (-0.5 * x * x).exp() / (2.0 * PI).sqrt();
182    let cdf = 1.0 - pdf * poly;
183
184    if x >= 0.0 {
185        cdf
186    } else {
187        1.0 - cdf
188    }
189}
190
191/// Standard normal probability density function
192#[inline]
193pub fn norm_pdf(x: f64) -> f64 {
194    (2.0 * PI).sqrt().recip() * (-0.5 * x * x).exp()
195}
196
197/// Natural logarithm with protection against domain errors
198#[inline]
199pub fn safe_log(x: f64) -> f64 {
200    if x > 0.0 {
201        x.ln()
202    } else if x == 0.0 {
203        f64::NEG_INFINITY
204    } else {
205        f64::NAN
206    }
207}
208
209/// Square root with protection against domain errors
210#[inline]
211pub fn safe_sqrt(x: f64) -> f64 {
212    if x >= 0.0 {
213        x.sqrt()
214    } else {
215        f64::NAN
216    }
217}
218
219/// Black-Scholes d1 parameter
220pub fn d1(spot: f64, strike: f64, time: f64, rate: f64, volatility: f64) -> f64 {
221    if time <= 0.0 || volatility <= 0.0 {
222        return f64::NAN;
223    }
224    let vol_sqrt_t = volatility * time.sqrt();
225    (safe_log(spot / strike) + (rate + 0.5 * volatility * volatility) * time) / vol_sqrt_t
226}
227
228/// Black-Scholes d2 parameter
229pub fn d2(spot: f64, strike: f64, time: f64, rate: f64, volatility: f64) -> f64 {
230    if time <= 0.0 || volatility <= 0.0 {
231        return f64::NAN;
232    }
233    let vol_sqrt_t = volatility * time.sqrt();
234    d1(spot, strike, time, rate, volatility) - vol_sqrt_t
235}
236
237/// Black-Scholes call option price
238pub fn black_scholes_call(spot: f64, strike: f64, time: f64, rate: f64, volatility: f64) -> f64 {
239    if time <= 0.0 {
240        return (spot - strike).max(0.0);
241    }
242    if volatility <= 0.0 {
243        return ((spot - strike * (-rate * time).exp()).max(0.0)).max(0.0);
244    }
245
246    let d1_val = d1(spot, strike, time, rate, volatility);
247    let d2_val = d2(spot, strike, time, rate, volatility);
248
249    spot * norm_cdf(d1_val) - strike * (-rate * time).exp() * norm_cdf(d2_val)
250}
251
252/// Black-Scholes put option price
253pub fn black_scholes_put(spot: f64, strike: f64, time: f64, rate: f64, volatility: f64) -> f64 {
254    if time <= 0.0 {
255        return (strike - spot).max(0.0);
256    }
257    if volatility <= 0.0 {
258        return ((strike * (-rate * time).exp() - spot).max(0.0)).max(0.0);
259    }
260
261    let d1_val = d1(spot, strike, time, rate, volatility);
262    let d2_val = d2(spot, strike, time, rate, volatility);
263
264    strike * (-rate * time).exp() * norm_cdf(-d2_val) - spot * norm_cdf(-d1_val)
265}
266
267/// Vega (sensitivity to volatility) - same for calls and puts
268pub fn vega(spot: f64, strike: f64, time: f64, rate: f64, volatility: f64) -> f64 {
269    if time <= 0.0 || volatility <= 0.0 {
270        return 0.0;
271    }
272
273    let d1_val = d1(spot, strike, time, rate, volatility);
274    spot * norm_pdf(d1_val) * time.sqrt()
275}
276
277/// Implied volatility using Newton-Raphson method
278pub fn implied_volatility_newton(
279    market_price: f64,
280    spot: f64,
281    strike: f64,
282    time: f64,
283    rate: f64,
284    is_call: bool,
285) -> Result<f64> {
286    if market_price <= 0.0 {
287        return Err(IntegrateError::ValueError(
288            "Market price must be positive".to_string(),
289        ));
290    }
291
292    // Initial guess using Brenner-Subrahmanyam approximation
293    let intrinsic = if is_call {
294        (spot - strike * (-rate * time).exp()).max(0.0)
295    } else {
296        (strike * (-rate * time).exp() - spot).max(0.0)
297    };
298
299    if market_price <= intrinsic {
300        return Err(IntegrateError::ValueError(
301            "Market price below intrinsic value".to_string(),
302        ));
303    }
304
305    let time_value = market_price - intrinsic;
306    let mut vol = (2.0 * PI / time).sqrt() * (time_value / spot);
307    vol = vol.max(0.01).min(5.0); // Clamp initial guess
308
309    const MAX_ITERATIONS: usize = 100;
310    const TOLERANCE: f64 = 1e-8;
311
312    for _ in 0..MAX_ITERATIONS {
313        let price = if is_call {
314            black_scholes_call(spot, strike, time, rate, vol)
315        } else {
316            black_scholes_put(spot, strike, time, rate, vol)
317        };
318
319        let diff = price - market_price;
320
321        if diff.abs() < TOLERANCE {
322            return Ok(vol);
323        }
324
325        let vega_val = vega(spot, strike, time, rate, vol);
326
327        if vega_val < 1e-10 {
328            return Err(IntegrateError::ValueError(
329                "Vega too small for convergence".to_string(),
330            ));
331        }
332
333        let vol_new = vol - diff / vega_val;
334
335        // Ensure volatility stays positive and reasonable
336        let vol_new = vol_new.max(0.001).min(10.0);
337
338        if (vol_new - vol).abs() < TOLERANCE {
339            return Ok(vol_new);
340        }
341
342        vol = vol_new;
343    }
344
345    Err(IntegrateError::ValueError(
346        "Implied volatility did not converge".to_string(),
347    ))
348}
349
350/// Bachelier model for call options (handles negative rates/prices)
351pub fn bachelier_call(spot: f64, strike: f64, time: f64, rate: f64, volatility: f64) -> f64 {
352    if time <= 0.0 {
353        return (spot - strike).max(0.0);
354    }
355    if volatility <= 0.0 {
356        return ((spot - strike * (-rate * time).exp()).max(0.0)).max(0.0);
357    }
358
359    let forward = spot * (rate * time).exp();
360    let std_dev = volatility * time.sqrt();
361    let d = (forward - strike) / std_dev;
362
363    (forward - strike) * norm_cdf(d) + std_dev * norm_pdf(d)
364}
365
366/// Bachelier model for put options
367pub fn bachelier_put(spot: f64, strike: f64, time: f64, rate: f64, volatility: f64) -> f64 {
368    if time <= 0.0 {
369        return (strike - spot).max(0.0);
370    }
371    if volatility <= 0.0 {
372        return ((strike * (-rate * time).exp() - spot).max(0.0)).max(0.0);
373    }
374
375    let forward = spot * (rate * time).exp();
376    let std_dev = volatility * time.sqrt();
377    let d = (forward - strike) / std_dev;
378
379    (strike - forward) * norm_cdf(-d) + std_dev * norm_pdf(d)
380}
381
382/// Binary search for implied volatility (more robust than Newton-Raphson)
383pub fn implied_volatility_brent(
384    market_price: f64,
385    spot: f64,
386    strike: f64,
387    time: f64,
388    rate: f64,
389    is_call: bool,
390) -> Result<f64> {
391    if market_price <= 0.0 {
392        return Err(IntegrateError::ValueError(
393            "Market price must be positive".to_string(),
394        ));
395    }
396
397    let mut vol_low = 0.001;
398    let mut vol_high = 5.0;
399
400    const MAX_ITERATIONS: usize = 100;
401    const TOLERANCE: f64 = 1e-8;
402
403    // Check bounds
404    let price_low = if is_call {
405        black_scholes_call(spot, strike, time, rate, vol_low)
406    } else {
407        black_scholes_put(spot, strike, time, rate, vol_low)
408    };
409
410    let price_high = if is_call {
411        black_scholes_call(spot, strike, time, rate, vol_high)
412    } else {
413        black_scholes_put(spot, strike, time, rate, vol_high)
414    };
415
416    if market_price < price_low || market_price > price_high {
417        return Err(IntegrateError::ValueError(
418            "Market price outside pricing bounds".to_string(),
419        ));
420    }
421
422    // Binary search
423    for _ in 0..MAX_ITERATIONS {
424        let vol_mid = (vol_low + vol_high) / 2.0;
425
426        let price_mid = if is_call {
427            black_scholes_call(spot, strike, time, rate, vol_mid)
428        } else {
429            black_scholes_put(spot, strike, time, rate, vol_mid)
430        };
431
432        if (price_mid - market_price).abs() < TOLERANCE {
433            return Ok(vol_mid);
434        }
435
436        if price_mid < market_price {
437            vol_low = vol_mid;
438        } else {
439            vol_high = vol_mid;
440        }
441
442        if vol_high - vol_low < TOLERANCE {
443            return Ok(vol_mid);
444        }
445    }
446
447    Err(IntegrateError::ValueError(
448        "Implied volatility did not converge".to_string(),
449    ))
450}
451
452/// Delta (sensitivity to spot price) for call option
453pub fn delta_call(spot: f64, strike: f64, time: f64, rate: f64, volatility: f64) -> f64 {
454    if time <= 0.0 {
455        return if spot > strike { 1.0 } else { 0.0 };
456    }
457    if volatility <= 0.0 {
458        return if spot > strike { 1.0 } else { 0.0 };
459    }
460
461    let d1_val = d1(spot, strike, time, rate, volatility);
462    norm_cdf(d1_val)
463}
464
465/// Delta for put option
466pub fn delta_put(spot: f64, strike: f64, time: f64, rate: f64, volatility: f64) -> f64 {
467    delta_call(spot, strike, time, rate, volatility) - 1.0
468}
469
470/// Gamma (sensitivity of delta to spot) - same for calls and puts
471pub fn gamma(spot: f64, strike: f64, time: f64, rate: f64, volatility: f64) -> f64 {
472    if time <= 0.0 || volatility <= 0.0 || spot <= 0.0 {
473        return 0.0;
474    }
475
476    let d1_val = d1(spot, strike, time, rate, volatility);
477    norm_pdf(d1_val) / (spot * volatility * time.sqrt())
478}
479
480/// Theta (time decay) for call option
481pub fn theta_call(spot: f64, strike: f64, time: f64, rate: f64, volatility: f64) -> f64 {
482    if time <= 0.0 {
483        return 0.0;
484    }
485    if volatility <= 0.0 {
486        return 0.0;
487    }
488
489    let d1_val = d1(spot, strike, time, rate, volatility);
490    let d2_val = d2(spot, strike, time, rate, volatility);
491
492    let term1 = -(spot * norm_pdf(d1_val) * volatility) / (2.0 * time.sqrt());
493    let term2 = rate * strike * (-rate * time).exp() * norm_cdf(d2_val);
494
495    term1 - term2
496}
497
498/// Theta (time decay) for put option
499pub fn theta_put(spot: f64, strike: f64, time: f64, rate: f64, volatility: f64) -> f64 {
500    if time <= 0.0 {
501        return 0.0;
502    }
503    if volatility <= 0.0 {
504        return 0.0;
505    }
506
507    let d1_val = d1(spot, strike, time, rate, volatility);
508    let d2_val = d2(spot, strike, time, rate, volatility);
509
510    let term1 = -(spot * norm_pdf(d1_val) * volatility) / (2.0 * time.sqrt());
511    let term2 = rate * strike * (-rate * time).exp() * norm_cdf(-d2_val);
512
513    term1 + term2
514}
515
516/// Rho (sensitivity to interest rate) for call option
517pub fn rho_call(spot: f64, strike: f64, time: f64, rate: f64, volatility: f64) -> f64 {
518    if time <= 0.0 {
519        return 0.0;
520    }
521    if volatility <= 0.0 {
522        return if spot > strike {
523            strike * time * (-rate * time).exp()
524        } else {
525            0.0
526        };
527    }
528
529    let d2_val = d2(spot, strike, time, rate, volatility);
530    strike * time * (-rate * time).exp() * norm_cdf(d2_val)
531}
532
533/// Rho (sensitivity to interest rate) for put option
534pub fn rho_put(spot: f64, strike: f64, time: f64, rate: f64, volatility: f64) -> f64 {
535    if time <= 0.0 {
536        return 0.0;
537    }
538    if volatility <= 0.0 {
539        return if spot < strike {
540            -strike * time * (-rate * time).exp()
541        } else {
542            0.0
543        };
544    }
545
546    let d2_val = d2(spot, strike, time, rate, volatility);
547    -strike * time * (-rate * time).exp() * norm_cdf(-d2_val)
548}
549
550/// Calculate all Greeks at once (more efficient than individual calls)
551pub struct Greeks {
552    pub delta: f64,
553    pub gamma: f64,
554    pub vega: f64,
555    pub theta: f64,
556    pub rho: f64,
557}
558
559impl Greeks {
560    /// Calculate all Greeks for a call option
561    pub fn call(spot: f64, strike: f64, time: f64, rate: f64, volatility: f64) -> Self {
562        Self {
563            delta: delta_call(spot, strike, time, rate, volatility),
564            gamma: gamma(spot, strike, time, rate, volatility),
565            vega: vega(spot, strike, time, rate, volatility),
566            theta: theta_call(spot, strike, time, rate, volatility),
567            rho: rho_call(spot, strike, time, rate, volatility),
568        }
569    }
570
571    /// Calculate all Greeks for a put option
572    pub fn put(spot: f64, strike: f64, time: f64, rate: f64, volatility: f64) -> Self {
573        Self {
574            delta: delta_put(spot, strike, time, rate, volatility),
575            gamma: gamma(spot, strike, time, rate, volatility),
576            vega: vega(spot, strike, time, rate, volatility),
577            theta: theta_put(spot, strike, time, rate, volatility),
578            rho: rho_put(spot, strike, time, rate, volatility),
579        }
580    }
581}
582
583#[cfg(test)]
584mod tests {
585    use super::*;
586
587    #[test]
588    fn test_norm_cdf() {
589        assert!((norm_cdf(0.0) - 0.5).abs() < 1e-7);
590        assert!(norm_cdf(-10.0) < 1e-10);
591        assert!(norm_cdf(10.0) > 0.9999999);
592        assert!((norm_cdf(1.0) - 0.8413447).abs() < 1e-5);
593    }
594
595    #[test]
596    fn test_norm_pdf() {
597        let pdf_0 = norm_pdf(0.0);
598        assert!((pdf_0 - 0.3989423).abs() < 1e-6);
599
600        let pdf_1 = norm_pdf(1.0);
601        assert!((pdf_1 - 0.2419707).abs() < 1e-6);
602    }
603
604    #[test]
605    fn test_black_scholes_call() {
606        let price = black_scholes_call(100.0, 100.0, 1.0, 0.05, 0.2);
607        assert!(price > 9.0 && price < 12.0); // ATM call should be around 10.45
608    }
609
610    #[test]
611    fn test_black_scholes_put() {
612        let price = black_scholes_put(100.0, 100.0, 1.0, 0.05, 0.2);
613        assert!(price > 5.0 && price < 8.0); // ATM put should be around 5.57
614    }
615
616    #[test]
617    fn test_put_call_parity() {
618        let s = 100.0;
619        let k = 100.0;
620        let t = 1.0;
621        let r = 0.05;
622        let v = 0.2;
623
624        let call = black_scholes_call(s, k, t, r, v);
625        let put = black_scholes_put(s, k, t, r, v);
626
627        // Put-call parity: C - P = S - K*e^(-rT)
628        let lhs = call - put;
629        let rhs = s - k * (-r * t).exp();
630
631        assert!((lhs - rhs).abs() < 1e-10);
632    }
633
634    #[test]
635    fn test_vega() {
636        let vega_val = vega(100.0, 100.0, 1.0, 0.05, 0.2);
637        assert!(vega_val > 0.0);
638        assert!(vega_val < 50.0); // Reasonable range for ATM option
639    }
640
641    #[test]
642    fn test_implied_volatility_newton() {
643        let spot = 100.0;
644        let strike = 100.0;
645        let time = 1.0;
646        let rate = 0.05;
647        let true_vol = 0.25;
648
649        let market_price = black_scholes_call(spot, strike, time, rate, true_vol);
650        let implied_vol =
651            implied_volatility_newton(market_price, spot, strike, time, rate, true).unwrap();
652
653        assert!((implied_vol - true_vol).abs() < 1e-6);
654    }
655
656    #[test]
657    fn test_implied_volatility_brent() {
658        let spot = 100.0;
659        let strike = 100.0;
660        let time = 1.0;
661        let rate = 0.05;
662        let true_vol = 0.25;
663
664        let market_price = black_scholes_call(spot, strike, time, rate, true_vol);
665        let implied_vol =
666            implied_volatility_brent(market_price, spot, strike, time, rate, true).unwrap();
667
668        assert!((implied_vol - true_vol).abs() < 1e-6);
669    }
670
671    #[test]
672    fn test_delta_call() {
673        let delta = delta_call(100.0, 100.0, 1.0, 0.05, 0.2);
674        // ATM call delta with positive rate is slightly above 0.5
675        assert!(delta > 0.50 && delta < 0.65);
676    }
677
678    #[test]
679    fn test_delta_put() {
680        let delta = delta_put(100.0, 100.0, 1.0, 0.05, 0.2);
681        // ATM put delta = call_delta - 1
682        assert!(delta > -0.50 && delta < -0.35);
683    }
684
685    #[test]
686    fn test_gamma() {
687        let gamma_val = gamma(100.0, 100.0, 1.0, 0.05, 0.2);
688        assert!(gamma_val > 0.0);
689        assert!(gamma_val < 0.1); // Reasonable range
690    }
691
692    #[test]
693    fn test_bachelier_call() {
694        let price = bachelier_call(100.0, 100.0, 1.0, 0.05, 20.0);
695        assert!(price > 0.0);
696    }
697
698    #[test]
699    fn test_bachelier_put() {
700        let price = bachelier_put(100.0, 100.0, 1.0, 0.05, 20.0);
701        assert!(price > 0.0);
702    }
703
704    #[test]
705    fn test_zero_time() {
706        assert_eq!(black_scholes_call(110.0, 100.0, 0.0, 0.05, 0.2), 10.0);
707        assert_eq!(black_scholes_put(90.0, 100.0, 0.0, 0.05, 0.2), 10.0);
708    }
709
710    #[test]
711    fn test_safe_functions() {
712        assert!(safe_log(-1.0).is_nan());
713        assert_eq!(safe_log(0.0), f64::NEG_INFINITY);
714        assert!(safe_sqrt(-1.0).is_nan());
715        assert_eq!(safe_sqrt(4.0), 2.0);
716    }
717
718    #[test]
719    fn test_theta_call() {
720        let theta = theta_call(100.0, 100.0, 1.0, 0.05, 0.2);
721        // Theta is negative (time decay) for long positions
722        assert!(theta < 0.0);
723        // Reasonable magnitude for ATM option
724        assert!(theta.abs() < 20.0);
725    }
726
727    #[test]
728    fn test_theta_put() {
729        let theta = theta_put(100.0, 100.0, 1.0, 0.05, 0.2);
730        // Theta for put depends on interest rate
731        // With positive rate, ATM put theta may be positive or negative
732        assert!(theta.abs() < 20.0);
733    }
734
735    #[test]
736    fn test_rho_call() {
737        let rho = rho_call(100.0, 100.0, 1.0, 0.05, 0.2);
738        // Rho is positive for call options
739        assert!(rho > 0.0);
740        // Reasonable magnitude
741        assert!(rho < 100.0);
742    }
743
744    #[test]
745    fn test_rho_put() {
746        let rho = rho_put(100.0, 100.0, 1.0, 0.05, 0.2);
747        // Rho is negative for put options
748        assert!(rho < 0.0);
749        // Reasonable magnitude
750        assert!(rho.abs() < 100.0);
751    }
752
753    #[test]
754    fn test_greeks_struct_call() {
755        let greeks = Greeks::call(100.0, 100.0, 1.0, 0.05, 0.2);
756
757        // Check all Greeks are reasonable
758        assert!(greeks.delta > 0.5 && greeks.delta < 0.65);
759        assert!(greeks.gamma > 0.0);
760        assert!(greeks.vega > 0.0);
761        assert!(greeks.theta < 0.0);
762        assert!(greeks.rho > 0.0);
763    }
764
765    #[test]
766    fn test_greeks_struct_put() {
767        let greeks = Greeks::put(100.0, 100.0, 1.0, 0.05, 0.2);
768
769        // Check all Greeks are reasonable
770        assert!(greeks.delta < 0.0 && greeks.delta > -0.5);
771        assert!(greeks.gamma > 0.0); // Same as call
772        assert!(greeks.vega > 0.0); // Same as call
773        assert!(greeks.rho < 0.0);
774    }
775
776    #[test]
777    fn test_theta_zero_time() {
778        assert_eq!(theta_call(100.0, 100.0, 0.0, 0.05, 0.2), 0.0);
779        assert_eq!(theta_put(100.0, 100.0, 0.0, 0.05, 0.2), 0.0);
780    }
781
782    #[test]
783    fn test_rho_zero_time() {
784        assert_eq!(rho_call(100.0, 100.0, 0.0, 0.05, 0.2), 0.0);
785        assert_eq!(rho_put(100.0, 100.0, 0.0, 0.05, 0.2), 0.0);
786    }
787
788    #[test]
789    fn test_sabr_parameters_creation() {
790        let sabr = SABRParameters::new(0.2, 0.5, -0.3, 0.4).unwrap();
791        assert_eq!(sabr.alpha, 0.2);
792        assert_eq!(sabr.beta, 0.5);
793        assert_eq!(sabr.rho, -0.3);
794        assert_eq!(sabr.nu, 0.4);
795    }
796
797    #[test]
798    fn test_sabr_parameters_validation() {
799        assert!(SABRParameters::new(-0.1, 0.5, 0.0, 0.4).is_err()); // Negative alpha
800        assert!(SABRParameters::new(0.2, 1.5, 0.0, 0.4).is_err()); // Beta > 1
801        assert!(SABRParameters::new(0.2, 0.5, 1.5, 0.4).is_err()); // Rho > 1
802        assert!(SABRParameters::new(0.2, 0.5, 0.0, -0.1).is_err()); // Negative nu
803    }
804
805    #[test]
806    fn test_sabr_atm_volatility() {
807        let sabr = SABRParameters::new(0.2, 0.5, -0.3, 0.4).unwrap();
808        let forward = 100.0;
809        let strike = 100.0;
810        let time = 1.0;
811
812        let vol = sabr.implied_volatility(forward, strike, time).unwrap();
813        assert!(vol > 0.0);
814        assert!(vol < 1.0); // Reasonable volatility range
815    }
816
817    #[test]
818    fn test_sabr_smile_shape() {
819        let sabr = SABRParameters::new(0.2, 0.5, -0.3, 0.4).unwrap();
820        let forward = 100.0;
821        let time = 1.0;
822
823        let vol_otm = sabr.implied_volatility(forward, 110.0, time).unwrap();
824        let vol_atm = sabr.implied_volatility(forward, 100.0, time).unwrap();
825        let vol_itm = sabr.implied_volatility(forward, 90.0, time).unwrap();
826
827        // All vols should be reasonable and positive
828        assert!(vol_otm > 0.0 && vol_otm < 1.0);
829        assert!(vol_atm > 0.0 && vol_atm < 1.0);
830        assert!(vol_itm > 0.0 && vol_itm < 1.0);
831
832        // Verify smile exists (not flat)
833        let smile_measure = (vol_itm - vol_atm).abs() + (vol_otm - vol_atm).abs();
834        assert!(smile_measure > 0.001); // Some curvature exists
835    }
836
837    #[test]
838    fn test_sabr_beta_zero() {
839        // Beta = 0 corresponds to normal model
840        let sabr = SABRParameters::new(20.0, 0.0, 0.0, 0.0).unwrap();
841        let vol = sabr.implied_volatility(100.0, 100.0, 1.0).unwrap();
842        // For beta=0, vol should be positive and reasonable
843        assert!(vol > 0.0 && vol < 100.0);
844    }
845
846    #[test]
847    fn test_sabr_beta_one() {
848        // Beta = 1 corresponds to lognormal model
849        let sabr = SABRParameters::new(0.2, 1.0, 0.0, 0.0).unwrap();
850        let vol = sabr.implied_volatility(100.0, 100.0, 1.0).unwrap();
851        assert!((vol - 0.2).abs() < 0.01); // Should be close to alpha
852    }
853
854    #[test]
855    fn test_sabr_symmetry() {
856        let sabr = SABRParameters::new(0.2, 0.5, 0.0, 0.2).unwrap();
857        let forward = 100.0;
858        let time = 1.0;
859
860        // With rho = 0, smile should be roughly symmetric
861        let vol_up = sabr.implied_volatility(forward, 105.0, time).unwrap();
862        let vol_down = sabr.implied_volatility(forward, 95.0, time).unwrap();
863
864        // Should be similar (within 10%)
865        assert!((vol_up - vol_down).abs() / vol_up < 0.1);
866    }
867}