1use 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 Bermudan(Vec<f64>),
17}
18
19pub 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)]
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 pub vanna: f64,
126 pub charm: f64,
128 pub gamma_p: f64,
130 pub zomma: f64,
132 #[serde(skip_serializing_if = "Option::is_none")]
134 pub std_err: Option<f64>,
135 #[serde(skip_serializing_if = "Option::is_none")]
137 pub deltas: Option<Vec<f64>>,
138 #[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 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
188pub fn norm_pdf(x: f64) -> f64 {
190 let t = -0.5 * x * x;
191 t.exp() / (SQRT_2 * PI.sqrt())
192}
193
194pub fn norm_cdf(x: f64) -> f64 {
196 0.5 * (1.0 + erf(x / SQRT_2))
197}
198
199pub 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 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 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
259pub 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 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 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 assert!((bivariate_norm_cdf(1.0, -0.5, 0.0) - norm_cdf(1.0) * norm_cdf(-0.5)).abs() < 1e-12);
336 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 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 assert!((bivariate_norm_cdf(0.5, 1.2, 1.0) - norm_cdf(0.5)).abs() < 1e-14);
348 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 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}