Skip to main content

rustyqlib/core/
utils.rs

1//mod dis{
2use libm::erf;
3use std::f64::consts::{PI, SQRT_2};
4use serde::{Deserialize, Serialize};
5use crate::core::data_models::ProductData;
6
7#[derive(PartialEq,Clone,Debug)]
8pub enum ContractStyle {
9    European,
10    American,
11    /// Exercisable only at the given times (year fractions from
12    /// valuation, strictly increasing). Expiry is always an exercise
13    /// opportunity through the terminal payoff; the listed times are the
14    /// intermediate rights, e.g. from a business-day adjusted
15    /// [`Schedule`](crate::core::calendar::Schedule).
16    Bermudan(Vec<f64>),
17}
18
19/// Map exercise/observation times to 1-based indices on an equally
20/// spaced grid of `steps` steps over `[0, t]`: nearest step, clamped to
21/// `[1, steps]`, strictly increasing so no two times collapse.
22pub fn times_to_grid_steps(times: &[f64], t: f64, steps: usize) -> Vec<usize> {
23    let mut out = Vec::with_capacity(times.len());
24    let mut prev: i64 = 0;
25    for &tm in times {
26        let i = ((tm / t) * steps as f64).round().max(1.0) as i64;
27        let i = i.max(prev + 1).min(steps as i64);
28        if i > prev {
29            out.push(i as usize);
30            prev = i;
31        }
32    }
33    out
34}
35
36#[derive(strum_macros::Display)]
37pub enum EngineType {
38    Analytical,
39    MonteCarlo,
40    Binomial,
41    FiniteDifference,
42    FFT,
43}
44pub trait Engine<I> {
45    fn npv(&self, instrument: &I) -> f64;
46}
47
48impl EngineType {
49    pub fn as_str(&self) -> &'static str {
50        match self {
51            EngineType::Analytical => "Analytical",
52            EngineType::MonteCarlo => "MonteCarlo",
53            EngineType::Binomial => "Binomial",
54            EngineType::FiniteDifference => "FiniteDifference",
55            EngineType::FFT => "FFT",
56        }
57    }
58}
59
60
61
62// #[derive(Clone,Debug,Deserialize,Serialize)]
63// pub struct MarketData {
64//     pub underlying_price:f64,
65//     pub option_type:Option<String>,
66//     pub strike_price:Option<f64>,
67//     pub volatility:Option<f64>,
68//     pub option_price:Option<f64>,
69//     pub risk_free_rate:Option<f64>,
70//     pub maturity:String,
71//     pub dividend: Option<f64>,
72//     pub simulation:Option<u64>,
73//     pub current_price:Option<f64>,
74//     pub notional: Option<f64>,
75//     pub long_short:Option<i32>,
76//     pub multiplier:Option<f64>,
77//     pub entry_price:Option<f64>,
78// }
79
80
81#[derive(Clone,Debug,Deserialize,Serialize)]
82pub struct RateData {
83    pub instrument: String,
84    pub currency: String,
85    pub start_date: String,
86    pub maturity_date: String,
87    pub valuation_date: String,
88    pub notional: f64,
89    pub fix_rate: f64,
90    pub day_count: String,
91    pub business_day_adjustment: i8,
92}
93
94#[derive(Clone,Debug,Deserialize,Serialize)]
95pub struct Contract {
96    pub action: String,
97    pub asset: String,
98    pub product_type: ProductData,
99    pub rate_data: Option<RateData>,
100}
101#[derive(Deserialize,Serialize)]
102pub struct CombinedContract{
103    pub contract: Contract,
104    pub output: ContractOutput
105}
106
107#[derive(Debug, Deserialize,Serialize)]
108pub struct Contracts {
109    pub asset: String,
110    pub contracts: Vec<Contract>,
111}
112#[derive(Debug, Deserialize,Serialize)]
113pub struct OutputJson {
114    pub contracts: Vec<String>,
115}
116#[derive(Deserialize,Serialize)]
117pub struct ContractOutput {
118    pub pv: f64,
119    pub delta: f64,
120    pub gamma: f64,
121    pub vega: f64,
122    pub theta: f64,
123    pub rho: f64,
124    /// Change in delta per unit change in implied volatility.
125    pub vanna: f64,
126    /// Change in delta per year of calendar time.
127    pub charm: f64,
128    /// Delta elasticity, `S * gamma / delta`.
129    pub gamma_p: f64,
130    /// Change in gamma per unit change in implied volatility.
131    pub zomma: f64,
132    /// Monte Carlo standard error of `pv` (None for deterministic engines).
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub std_err: Option<f64>,
135    /// Per-asset deltas for multi-asset (rainbow) products.
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub deltas: Option<Vec<f64>>,
138    /// Per-asset vegas for multi-asset (rainbow) products.
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub vegas: Option<Vec<f64>>,
141    pub error: Option<String>
142}
143
144impl From<crate::core::results::PricingResult> for ContractOutput {
145    fn from(r: crate::core::results::PricingResult) -> Self {
146        ContractOutput {
147            pv: r.pv,
148            delta: r.greeks.delta,
149            gamma: r.greeks.gamma,
150            vega: r.greeks.vega,
151            theta: r.greeks.theta,
152            rho: r.greeks.rho,
153            vanna: r.greeks.vanna,
154            charm: r.greeks.charm,
155            gamma_p: r.greeks.gamma_p,
156            zomma: r.greeks.zomma,
157            std_err: r.std_err,
158            deltas: None,
159            vegas: None,
160            error: None,
161        }
162    }
163}
164
165impl ContractOutput {
166    /// Output for a contract that could not be priced: all figures zero,
167    /// with the failure recorded in `error`.
168    pub fn from_error(message: String) -> Self {
169        ContractOutput {
170            pv: 0.0,
171            delta: 0.0,
172            gamma: 0.0,
173            vega: 0.0,
174            theta: 0.0,
175            rho: 0.0,
176            vanna: 0.0,
177            charm: 0.0,
178            gamma_p: 0.0,
179            zomma: 0.0,
180            std_err: None,
181            deltas: None,
182            vegas: None,
183            error: Some(message),
184        }
185    }
186}
187
188/// Probability density function of a standard normal random variable x.
189pub fn norm_pdf(x: f64) -> f64 {
190    let t = -0.5 * x * x;
191    t.exp() / (SQRT_2 * PI.sqrt())
192}
193
194/// Cumulative distribution function of a standard normal random variable x.
195pub fn norm_cdf(x: f64) -> f64 {
196    0.5 * (1.0 + erf(x / SQRT_2))
197}
198
199/// Cumulative bivariate normal distribution `P(X <= a, Y <= b)` for
200/// standard normals with correlation `rho`.
201///
202/// Genz (2004) Gauss-Legendre quadrature on the arcsine form for
203/// `|rho| <= 0.925` (accurate to ~1e-14); an adaptive-free Simpson
204/// integration of `phi(x) N((b - rho x)/sqrt(1-rho^2))` for the highly
205/// correlated tail. Used by the Bjerksund-Stensland (2002) two-boundary
206/// American approximation.
207pub fn bivariate_norm_cdf(a: f64, b: f64, rho: f64) -> f64 {
208    assert!((-1.0..=1.0).contains(&rho), "correlation must be in [-1, 1]");
209    if rho == 1.0 {
210        return norm_cdf(a.min(b));
211    }
212    if rho == -1.0 {
213        return (norm_cdf(a) + norm_cdf(b) - 1.0).max(0.0);
214    }
215    if rho.abs() <= 0.925 {
216        // 10-point Gauss-Legendre on each half interval
217        const WEIGHTS: [f64; 10] = [
218            0.01761400713915212, 0.04060142980038694, 0.06267204833410906,
219            0.08327674157670475, 0.1019301198172404, 0.1181945319615184,
220            0.1316886384491766, 0.1420961093183821, 0.1491729864726037,
221            0.1527533871307259,
222        ];
223        const ABSCISSAE: [f64; 10] = [
224            0.9931285991850949, 0.9639719272779138, 0.9122344282513259,
225            0.8391169718222188, 0.7463319064601508, 0.6360536807265150,
226            0.5108670019508271, 0.3737060887154196, 0.2277858511416451,
227            0.07652652113349733,
228        ];
229        let (h, k) = (-a, -b);
230        let hs = 0.5 * (h * h + k * k);
231        let asr = rho.asin();
232        let mut sum = 0.0;
233        for (w, x) in WEIGHTS.iter().zip(&ABSCISSAE) {
234            for sign in [-1.0, 1.0] {
235                let sn = (asr * (sign * x + 1.0) / 2.0).sin();
236                sum += w * ((sn * h * k - hs) / (1.0 - sn * sn)).exp();
237            }
238        }
239        sum * asr / (4.0 * PI) + norm_cdf(-h) * norm_cdf(-k)
240    } else {
241        // high correlation: integrate the conditional CDF directly
242        let denom = (1.0 - rho * rho).sqrt();
243        let lo = -8.5_f64;
244        if a <= lo {
245            return 0.0;
246        }
247        let n_steps = 2000;
248        let dx = (a - lo) / n_steps as f64;
249        let f = |x: f64| norm_pdf(x) * norm_cdf((b - rho * x) / denom);
250        let mut sum = f(lo) + f(a);
251        for i in 1..n_steps {
252            let x = lo + i as f64 * dx;
253            sum += if i % 2 == 1 { 4.0 } else { 2.0 } * f(x);
254        }
255        sum * dx / 3.0
256    }
257}
258
259/// Inverse of the standard normal CDF (quantile function).
260///
261/// Acklam's rational approximation refined with one Halley step against the
262/// erf-based [`norm_cdf`], giving close to machine precision. `p` must be in (0, 1);
263/// values outside return NaN.
264pub fn inv_norm_cdf(p: f64) -> f64 {
265    if !(p > 0.0 && p < 1.0) {
266        return f64::NAN;
267    }
268    const A: [f64; 6] = [
269        -3.969683028665376e+01,
270        2.209460984245205e+02,
271        -2.759285104469687e+02,
272        1.383577518672690e+02,
273        -3.066479806614716e+01,
274        2.506628277459239e+00,
275    ];
276    const B: [f64; 5] = [
277        -5.447609879822406e+01,
278        1.615858368580409e+02,
279        -1.556989798598866e+02,
280        6.680131188771972e+01,
281        -1.328068155288572e+01,
282    ];
283    const C: [f64; 6] = [
284        -7.784894002430293e-03,
285        -3.223964580411365e-01,
286        -2.400758277161838e+00,
287        -2.549732539343734e+00,
288        4.374664141464968e+00,
289        2.938163982698783e+00,
290    ];
291    const D: [f64; 4] = [
292        7.784695709041462e-03,
293        3.224671290700398e-01,
294        2.445134137142996e+00,
295        3.754408661907416e+00,
296    ];
297    const P_LOW: f64 = 0.02425;
298
299    let tail = |q: f64| -> f64 {
300        (((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
301            / ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
302    };
303    let x = if p < P_LOW {
304        tail((-2.0 * p.ln()).sqrt())
305    } else if p <= 1.0 - P_LOW {
306        let q = p - 0.5;
307        let r = q * q;
308        (((((A[0] * r + A[1]) * r + A[2]) * r + A[3]) * r + A[4]) * r + A[5]) * q
309            / (((((B[0] * r + B[1]) * r + B[2]) * r + B[3]) * r + B[4]) * r + 1.0)
310    } else {
311        -tail((-2.0 * (1.0 - p).ln()).sqrt())
312    };
313    // one Halley refinement step on f(x) = N(x) - p (f' = phi, f'' = -x phi)
314    crate::core::solvers::Solver1d::new(0.0, 1)
315        .halley(|x| norm_cdf(x) - p, norm_pdf, |x| -x * norm_pdf(x), x)
316        .x
317}
318
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323
324    #[test]
325    fn bivariate_normal_identities() {
326        // exact identity at the origin: 1/4 + asin(rho)/(2 pi)
327        for rho in [-0.9_f64, -0.5, 0.0, 0.3, 0.786, 0.9] {
328            let exact = 0.25 + rho.asin() / (2.0 * PI);
329            assert!(
330                (bivariate_norm_cdf(0.0, 0.0, rho) - exact).abs() < 1e-12,
331                "rho {rho}"
332            );
333        }
334        // independence factorizes
335        assert!((bivariate_norm_cdf(1.0, -0.5, 0.0) - norm_cdf(1.0) * norm_cdf(-0.5)).abs() < 1e-12);
336        // symmetry in the arguments
337        assert!(
338            (bivariate_norm_cdf(0.7, -0.2, 0.4) - bivariate_norm_cdf(-0.2, 0.7, 0.4)).abs() < 1e-12
339        );
340        // golden value from the cross-checked reference implementation
341        assert!((bivariate_norm_cdf(0.5, -0.3, 0.786) - 0.367657814886).abs() < 1e-9);
342    }
343
344    #[test]
345    fn bivariate_normal_high_correlation_branch() {
346        // rho -> 1: P(X <= a, Y <= b) -> N(min(a, b))
347        assert!((bivariate_norm_cdf(0.5, 1.2, 1.0) - norm_cdf(0.5)).abs() < 1e-14);
348        // the Simpson branch (|rho| > 0.925) must join the Genz branch
349        // smoothly: compare both at rho just inside each side
350        let genz = bivariate_norm_cdf(0.4, -0.1, 0.92);
351        let simpson = bivariate_norm_cdf(0.4, -0.1, 0.93);
352        assert!((genz - simpson).abs() < 5e-3, "{genz} vs {simpson}");
353        // and rho = 0.99 stays close to the perfect-correlation limit
354        let near = bivariate_norm_cdf(0.5, 1.2, 0.99);
355        assert!(near < norm_cdf(0.5) + 1e-9 && near > norm_cdf(0.5) - 0.02, "{near}");
356    }
357}