1use std::time::{Duration, Instant};
31
32use serde::{Deserialize, Serialize};
33
34use crate::core::errors::RustyQLibError;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
40#[serde(rename_all = "snake_case")]
41pub enum BinomialTreeType {
42 #[serde(alias = "CRR", alias = "crr")]
45 CoxRossRubinstein,
46 #[serde(alias = "JR", alias = "jr")]
48 JarrowRudd,
49 Tian,
51 Trigeorgis,
53 #[default]
57 #[serde(alias = "LR", alias = "lr")]
58 LeisenReimer,
59 #[serde(alias = "EQP", alias = "eqp")]
61 AdditiveEqp,
62}
63
64impl std::str::FromStr for BinomialTreeType {
65 type Err = RustyQLibError;
66 fn from_str(s: &str) -> Result<Self, RustyQLibError> {
67 use BinomialTreeType::*;
68 Ok(match s.trim().to_lowercase().as_str() {
69 "crr" | "coxrossrubinstein" | "cox_ross_rubinstein" => CoxRossRubinstein,
70 "jr" | "jarrowrudd" | "jarrow_rudd" => JarrowRudd,
71 "tian" => Tian,
72 "trigeorgis" => Trigeorgis,
73 "lr" | "leisenreimer" | "leisen_reimer" => LeisenReimer,
74 "eqp" | "additiveeqp" | "additive_eqp" => AdditiveEqp,
75 other => {
76 return Err(RustyQLibError::invalid_input(
77 "tree_type",
78 format!(
79 "unknown tree type '{other}' (use CRR, JarrowRudd, Tian, \
80 Trigeorgis, LeisenReimer or EQP)"
81 ),
82 ))
83 }
84 })
85 }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
90pub struct LatticeConfig {
91 pub tree_type: BinomialTreeType,
92 pub steps: usize,
93 #[serde(default)]
98 pub term_structure: bool,
99}
100
101impl Default for LatticeConfig {
102 fn default() -> Self {
103 LatticeConfig {
104 tree_type: BinomialTreeType::LeisenReimer,
105 steps: 1000,
106 term_structure: false,
107 }
108 }
109}
110
111impl LatticeConfig {
112 pub fn validate(&self) -> Result<(), RustyQLibError> {
114 if self.steps < 2 {
115 return Err(RustyQLibError::invalid_input(
116 "tree_steps",
117 format!("the binomial tree needs at least 2 steps, got {}", self.steps),
118 ));
119 }
120 Ok(())
121 }
122}
123
124#[derive(Debug, Clone, Copy, PartialEq)]
127pub struct LatticeParams {
128 pub log_up: f64,
129 pub log_down: f64,
130 pub p_up: f64,
131}
132
133impl BinomialTreeType {
134 pub fn effective_steps(&self, steps: usize) -> usize {
137 let steps = steps.max(2);
138 match self {
139 BinomialTreeType::LeisenReimer if steps % 2 == 0 => steps + 1,
140 _ => steps,
141 }
142 }
143
144 pub fn params(
148 &self,
149 s0: f64,
150 strike: f64,
151 b: f64,
152 sigma: f64,
153 t: f64,
154 n: usize,
155 ) -> Result<LatticeParams, RustyQLibError> {
156 if !(sigma > 0.0 && sigma.is_finite()) {
157 return Err(RustyQLibError::invalid_input(
158 "sigma",
159 format!("lattice volatility must be positive and finite, got {sigma}"),
160 ));
161 }
162 if !(t > 0.0) || n < 2 {
163 return Err(RustyQLibError::invalid_input(
164 "lattice",
165 "the lattice needs positive maturity and at least two steps",
166 ));
167 }
168 let dt = t / n as f64;
169 let nu = b - 0.5 * sigma * sigma;
170 let params = match self {
171 BinomialTreeType::CoxRossRubinstein => {
172 let dx = sigma * dt.sqrt();
173 let (u, d) = (dx.exp(), (-dx).exp());
174 LatticeParams {
175 log_up: dx,
176 log_down: -dx,
177 p_up: ((b * dt).exp() - d) / (u - d),
178 }
179 }
180 BinomialTreeType::JarrowRudd => LatticeParams {
181 log_up: nu * dt + sigma * dt.sqrt(),
182 log_down: nu * dt - sigma * dt.sqrt(),
183 p_up: 0.5,
184 },
185 BinomialTreeType::Tian => {
186 let m = (b * dt).exp();
187 let v = (sigma * sigma * dt).exp();
188 let root = (v * v + 2.0 * v - 3.0).sqrt();
189 let u = 0.5 * m * v * (v + 1.0 + root);
190 let d = 0.5 * m * v * (v + 1.0 - root);
191 LatticeParams { log_up: u.ln(), log_down: d.ln(), p_up: (m - d) / (u - d) }
192 }
193 BinomialTreeType::Trigeorgis => {
194 let dx = (sigma * sigma * dt + nu * nu * dt * dt).sqrt();
195 LatticeParams {
196 log_up: dx,
197 log_down: -dx,
198 p_up: 0.5 + 0.5 * nu * dt / dx,
199 }
200 }
201 BinomialTreeType::LeisenReimer => {
202 if !(s0 > 0.0 && strike > 0.0) {
203 return Err(RustyQLibError::invalid_input(
204 "lattice",
205 "Leisen-Reimer needs positive spot and strike",
206 ));
207 }
208 let n_odd = self.effective_steps(n);
209 if n_odd != n {
210 return Err(RustyQLibError::invalid_input(
211 "lattice",
212 "Leisen-Reimer needs an odd step count (use effective_steps)",
213 ));
214 }
215 let sq_t = sigma * t.sqrt();
216 let d1 = ((s0 / strike).ln() + (b + 0.5 * sigma * sigma) * t) / sq_t;
217 let d2 = d1 - sq_t;
218 let p = peizer_pratt(d2, n);
219 let p_star = peizer_pratt(d1, n);
220 let m = (b * dt).exp();
221 let u = m * p_star / p;
222 let d = (m - p * u) / (1.0 - p);
223 if !(d > 0.0 && u > d) {
224 return Err(RustyQLibError::NumericalError(format!(
225 "Leisen-Reimer step degenerated (u {u}, d {d}); increase the step count"
226 )));
227 }
228 LatticeParams { log_up: u.ln(), log_down: d.ln(), p_up: p }
229 }
230 BinomialTreeType::AdditiveEqp => {
231 let disc = 4.0 * sigma * sigma * dt - 3.0 * nu * nu * dt * dt;
232 if disc <= 0.0 {
233 return Err(RustyQLibError::NumericalError(
234 "EQP tree needs 4 sigma^2 dt > 3 nu^2 dt^2; increase the step count"
235 .to_string(),
236 ));
237 }
238 LatticeParams {
239 log_up: 0.5 * nu * dt + 0.5 * disc.sqrt(),
240 log_down: 1.5 * nu * dt - 0.5 * disc.sqrt(),
241 p_up: 0.5,
242 }
243 }
244 };
245 if !(params.p_up > 0.0 && params.p_up < 1.0) {
246 return Err(RustyQLibError::NumericalError(format!(
247 "lattice probability {:.6} outside (0, 1): the time step is too \
248 large for this drift/volatility (increase the step count)",
249 params.p_up
250 )));
251 }
252 Ok(params)
253 }
254}
255
256fn peizer_pratt(z: f64, n: usize) -> f64 {
258 let nf = n as f64;
259 let scaled = z / (nf + 1.0 / 3.0 + 0.1 / (nf + 1.0));
260 let inner = (1.0 - (-scaled * scaled * (nf + 1.0 / 6.0)).exp()).sqrt();
261 0.5 + 0.5 * inner.copysign(z)
262}
263
264pub fn price_backward(
274 s0: f64,
275 params: &LatticeParams,
276 n: usize,
277 df_step: f64,
278 terminal: &dyn Fn(f64) -> f64,
279 exercise: Option<&dyn Fn(usize, f64, f64) -> f64>,
280) -> f64 {
281 let up_pow: Vec<f64> = (0..=n).map(|j| (j as f64 * params.log_up).exp()).collect();
283 let down_pow: Vec<f64> = (0..=n).map(|j| (j as f64 * params.log_down).exp()).collect();
284 let spot = |i: usize, j: usize| s0 * up_pow[j] * down_pow[i - j];
285
286 let mut v: Vec<f64> = (0..=n).map(|j| terminal(spot(n, j))).collect();
287 let (p, q) = (params.p_up, 1.0 - params.p_up);
288 for i in (0..n).rev() {
289 for j in 0..=i {
290 let cont = df_step * (p * v[j + 1] + q * v[j]);
292 v[j] = match exercise {
293 Some(ex) => ex(i, spot(i, j), cont),
294 None => cont,
295 };
296 }
297 }
298 v[0]
299}
300
301#[derive(Debug, Clone, Copy, PartialEq)]
303pub struct LatticeSolution {
304 pub price: f64,
305 pub delta: f64,
307 pub gamma: f64,
309 pub theta: f64,
313}
314
315pub fn price_backward_with_greeks(
320 s0: f64,
321 params: &LatticeParams,
322 n: usize,
323 dt: f64,
324 df_step: f64,
325 terminal: &dyn Fn(f64) -> f64,
326 exercise: Option<&dyn Fn(usize, f64, f64) -> f64>,
327) -> LatticeSolution {
328 let up_pow: Vec<f64> = (0..=n).map(|j| (j as f64 * params.log_up).exp()).collect();
329 let down_pow: Vec<f64> = (0..=n).map(|j| (j as f64 * params.log_down).exp()).collect();
330 let spot = |i: usize, j: usize| s0 * up_pow[j] * down_pow[i - j];
331
332 let mut v: Vec<f64> = (0..=n).map(|j| terminal(spot(n, j))).collect();
333 let mut layer2 = [0.0; 3];
334 let mut layer1 = [0.0; 2];
335 if n == 2 {
336 layer2.copy_from_slice(&v[0..3]);
337 }
338 let (p, q) = (params.p_up, 1.0 - params.p_up);
339 for i in (0..n).rev() {
340 for j in 0..=i {
341 let cont = df_step * (p * v[j + 1] + q * v[j]);
342 v[j] = match exercise {
343 Some(ex) => ex(i, spot(i, j), cont),
344 None => cont,
345 };
346 }
347 match i {
348 2 => layer2.copy_from_slice(&v[0..3]),
349 1 => layer1.copy_from_slice(&v[0..2]),
350 _ => {}
351 }
352 }
353
354 let price = v[0];
355 let delta = (layer1[1] - layer1[0]) / (spot(1, 1) - spot(1, 0));
356 let (s_uu, s_ud, s_dd) = (spot(2, 2), spot(2, 1), spot(2, 0));
357 let d_up = (layer2[2] - layer2[1]) / (s_uu - s_ud);
358 let d_down = (layer2[1] - layer2[0]) / (s_ud - s_dd);
359 let gamma = (d_up - d_down) / (0.5 * (s_uu - s_dd));
360 let ds = s_ud - s0;
364 let theta = (layer2[1] - price - delta * ds - 0.5 * gamma * ds * ds) / (2.0 * dt);
365
366 LatticeSolution { price, delta, gamma, theta }
367}
368
369#[derive(Debug, Clone)]
373pub struct LatticeDiagnostics {
374 pub price: f64,
375 pub tree_type: BinomialTreeType,
376 pub steps: usize,
377 pub params: LatticeParams,
378 pub elapsed: Duration,
380 pub spot_tree: Vec<Vec<f64>>,
382 pub value_tree: Vec<Vec<f64>>,
383 pub exercise_boundary: Vec<Option<(f64, f64)>>,
386 pub delta: f64,
388 pub gamma: f64,
390 pub theta: f64,
394}
395
396pub fn price_with_diagnostics(
399 tree_type: BinomialTreeType,
400 s0: f64,
401 params: &LatticeParams,
402 n: usize,
403 dt: f64,
404 df_step: f64,
405 terminal: &dyn Fn(f64) -> f64,
406 exercise: Option<&dyn Fn(usize, f64, f64) -> f64>,
407) -> LatticeDiagnostics {
408 let start = Instant::now();
409 let up_pow: Vec<f64> = (0..=n).map(|j| (j as f64 * params.log_up).exp()).collect();
412 let down_pow: Vec<f64> = (0..=n).map(|j| (j as f64 * params.log_down).exp()).collect();
413 let spot_tree: Vec<Vec<f64>> = (0..=n)
414 .map(|i| (0..=i).map(|j| s0 * up_pow[j] * down_pow[i - j]).collect())
415 .collect();
416
417 let mut value_tree: Vec<Vec<f64>> = spot_tree
418 .iter()
419 .map(|layer| layer.iter().map(|_| 0.0).collect())
420 .collect();
421 value_tree[n] = spot_tree[n].iter().map(|&s| terminal(s)).collect();
422
423 let mut exercise_boundary: Vec<Option<(f64, f64)>> = vec![None; n + 1];
424 let (p, q) = (params.p_up, 1.0 - params.p_up);
425 for i in (0..n).rev() {
426 for j in 0..=i {
427 let cont = df_step * (p * value_tree[i + 1][j + 1] + q * value_tree[i + 1][j]);
428 let spot = spot_tree[i][j];
429 let value = match exercise {
430 Some(ex) => ex(i, spot, cont),
431 None => cont,
432 };
433 if value > cont {
434 let entry = exercise_boundary[i].get_or_insert((spot, spot));
435 entry.0 = entry.0.min(spot);
436 entry.1 = entry.1.max(spot);
437 }
438 value_tree[i][j] = value;
439 }
440 }
441
442 let price = value_tree[0][0];
443 let delta = (value_tree[1][1] - value_tree[1][0]) / (spot_tree[1][1] - spot_tree[1][0]);
444 let (s_uu, s_ud, s_dd) = (spot_tree[2][2], spot_tree[2][1], spot_tree[2][0]);
445 let (v_uu, v_ud, v_dd) = (value_tree[2][2], value_tree[2][1], value_tree[2][0]);
446 let d_up = (v_uu - v_ud) / (s_uu - s_ud);
447 let d_down = (v_ud - v_dd) / (s_ud - s_dd);
448 let gamma = (d_up - d_down) / (0.5 * (s_uu - s_dd));
449 let theta = (v_ud - price) / (2.0 * dt);
450
451 LatticeDiagnostics {
452 price,
453 tree_type,
454 steps: n,
455 params: *params,
456 elapsed: start.elapsed(),
457 spot_tree,
458 value_tree,
459 exercise_boundary,
460 delta,
461 gamma,
462 theta,
463 }
464}
465
466#[derive(Debug, Clone, Copy)]
470pub struct ConvergencePoint {
471 pub steps: usize,
472 pub price: f64,
473 pub elapsed: Duration,
474}
475
476#[allow(clippy::too_many_arguments)]
480pub fn convergence_study(
481 tree_type: BinomialTreeType,
482 s0: f64,
483 strike: f64,
484 b: f64,
485 sigma: f64,
486 r: f64,
487 t: f64,
488 steps_ladder: &[usize],
489 terminal: &dyn Fn(f64) -> f64,
490 exercise: Option<&dyn Fn(usize, f64, f64) -> f64>,
491) -> Result<Vec<ConvergencePoint>, RustyQLibError> {
492 steps_ladder
493 .iter()
494 .map(|&steps| {
495 let n = tree_type.effective_steps(steps);
496 let params = tree_type.params(s0, strike, b, sigma, t, n)?;
497 let df_step = (-r * t / n as f64).exp();
498 let start = Instant::now();
499 let price = price_backward(s0, ¶ms, n, df_step, terminal, exercise);
500 Ok(ConvergencePoint { steps: n, price, elapsed: start.elapsed() })
501 })
502 .collect()
503}
504
505
506#[derive(Debug, Clone)]
526pub struct TermLattice {
527 pub times: Vec<f64>,
529 pub dx: f64,
531 pub p_up: Vec<f64>,
533 pub df: Vec<f64>,
535}
536
537impl TermLattice {
538 pub fn build(
549 n: usize,
550 t: f64,
551 forward_rate: &dyn Fn(f64, f64) -> f64,
552 forward_carry: &dyn Fn(f64, f64) -> f64,
553 total_variance: &dyn Fn(f64) -> f64,
554 ) -> Result<TermLattice, RustyQLibError> {
555 if !(t > 0.0) || n < 2 {
556 return Err(RustyQLibError::invalid_input(
557 "lattice",
558 "the lattice needs positive maturity and at least two steps",
559 ));
560 }
561 let w_total = total_variance(t);
562 if !(w_total > 0.0 && w_total.is_finite()) {
563 return Err(RustyQLibError::invalid_input(
564 "total_variance",
565 format!("total variance to maturity must be positive and finite, got {w_total}"),
566 ));
567 }
568
569 let mut times = Vec::with_capacity(n + 1);
571 times.push(0.0);
572 for i in 1..n {
573 let target = w_total * i as f64 / n as f64;
574 let (mut lo, mut hi) = (*times.last().expect("nonempty"), t);
575 for _ in 0..80 {
576 let mid = 0.5 * (lo + hi);
577 if total_variance(mid) < target {
578 lo = mid;
579 } else {
580 hi = mid;
581 }
582 }
583 let ti = 0.5 * (lo + hi);
584 if ti <= *times.last().expect("nonempty") {
585 return Err(RustyQLibError::NumericalError(
586 "cumulative variance is not strictly increasing (calendar \
587 arbitrage in the vol term structure)"
588 .to_string(),
589 ));
590 }
591 times.push(ti);
592 }
593 times.push(t);
594
595 let dx = (w_total / n as f64).sqrt();
596 let (u, d) = (dx.exp(), (-dx).exp());
597 let mut p_up = Vec::with_capacity(n);
598 let mut df = Vec::with_capacity(n);
599 for i in 0..n {
600 let (t1, t2) = (times[i], times[i + 1]);
601 let dt = t2 - t1;
602 let r = forward_rate(t1, t2);
603 let b = r - forward_carry(t1, t2);
604 let growth = (b * dt).exp();
605 let p = (growth - d) / (u - d);
606 if !(p > 0.0 && p < 1.0) {
607 return Err(RustyQLibError::NumericalError(format!(
608 "step {i} probability {p:.6} outside (0, 1): the local drift \
609 is too large for the variance spacing (increase the step count)"
610 )));
611 }
612 p_up.push(p);
613 df.push((-r * dt).exp());
614 }
615
616 Ok(TermLattice { times, dx, p_up, df })
617 }
618
619 pub fn steps(&self) -> usize {
620 self.p_up.len()
621 }
622
623 pub fn price(
628 &self,
629 s0: f64,
630 terminal: &dyn Fn(f64) -> f64,
631 exercise: Option<&dyn Fn(usize, f64, f64, f64) -> f64>,
632 ) -> f64 {
633 let n = self.steps();
634 let pow: Vec<f64> = (0..=2 * n).map(|k| ((k as f64 - n as f64) * self.dx).exp()).collect();
636 let spot = |i: usize, j: usize| s0 * pow[2 * j + n - i];
637
638 let mut v: Vec<f64> = (0..=n).map(|j| terminal(spot(n, j))).collect();
639 for i in (0..n).rev() {
640 let (p, q, df) = (self.p_up[i], 1.0 - self.p_up[i], self.df[i]);
641 for j in 0..=i {
642 let cont = df * (p * v[j + 1] + q * v[j]);
643 v[j] = match exercise {
644 Some(ex) => ex(i, self.times[i], spot(i, j), cont),
645 None => cont,
646 };
647 }
648 }
649 v[0]
650 }
651
652 pub fn price_with_greeks(
659 &self,
660 s0: f64,
661 terminal: &dyn Fn(f64) -> f64,
662 exercise: Option<&dyn Fn(usize, f64, f64, f64) -> f64>,
663 ) -> LatticeSolution {
664 let n = self.steps();
665 let pow: Vec<f64> = (0..=2 * n).map(|k| ((k as f64 - n as f64) * self.dx).exp()).collect();
666 let spot = |i: usize, j: usize| s0 * pow[2 * j + n - i];
667
668 let mut v: Vec<f64> = (0..=n).map(|j| terminal(spot(n, j))).collect();
669 let mut layer2 = [0.0; 3];
670 let mut layer1 = [0.0; 2];
671 if n == 2 {
672 layer2.copy_from_slice(&v[0..3]);
673 }
674 for i in (0..n).rev() {
675 let (p, q, df) = (self.p_up[i], 1.0 - self.p_up[i], self.df[i]);
676 for j in 0..=i {
677 let cont = df * (p * v[j + 1] + q * v[j]);
678 v[j] = match exercise {
679 Some(ex) => ex(i, self.times[i], spot(i, j), cont),
680 None => cont,
681 };
682 }
683 match i {
684 2 => layer2.copy_from_slice(&v[0..3]),
685 1 => layer1.copy_from_slice(&v[0..2]),
686 _ => {}
687 }
688 }
689
690 let price = v[0];
691 let delta = (layer1[1] - layer1[0]) / (spot(1, 1) - spot(1, 0));
692 let (s_uu, s_ud, s_dd) = (spot(2, 2), spot(2, 1), spot(2, 0));
693 let d_up = (layer2[2] - layer2[1]) / (s_uu - s_ud);
694 let d_down = (layer2[1] - layer2[0]) / (s_ud - s_dd);
695 let gamma = (d_up - d_down) / (0.5 * (s_uu - s_dd));
696 let theta = (layer2[1] - price) / self.times[2];
697 LatticeSolution { price, delta, gamma, theta }
698 }
699}
700
701
702#[derive(Debug, Clone, Copy, PartialEq)]
711pub struct TrinomialBranch {
712 pub target: i32,
713 pub p_up: f64,
714 pub p_mid: f64,
715 pub p_down: f64,
716}
717
718#[derive(Debug, Clone)]
728pub struct TrinomialLattice {
729 pub dt: f64,
730 pub dx: f64,
731 j_min: Vec<i32>,
732 j_max: Vec<i32>,
733 branches: Vec<Vec<TrinomialBranch>>,
735}
736
737impl TrinomialLattice {
738 pub fn build(
743 n: usize,
744 dt: f64,
745 dx: f64,
746 branching: &dyn Fn(usize, i32) -> TrinomialBranch,
747 ) -> Result<TrinomialLattice, RustyQLibError> {
748 if n < 1 || !(dt > 0.0) || !(dx > 0.0) {
749 return Err(RustyQLibError::invalid_input(
750 "trinomial",
751 "the lattice needs at least one step and positive dt / dx",
752 ));
753 }
754 let mut j_min = vec![0i32];
755 let mut j_max = vec![0i32];
756 let mut branches: Vec<Vec<TrinomialBranch>> = Vec::with_capacity(n);
757 for i in 0..n {
758 let (lo, hi) = (j_min[i], j_max[i]);
759 let mut layer = Vec::with_capacity((hi - lo + 1) as usize);
760 let (mut next_lo, mut next_hi) = (i32::MAX, i32::MIN);
761 for j in lo..=hi {
762 let b = branching(i, j);
763 if (b.target - j).abs() > 1 {
764 return Err(RustyQLibError::NumericalError(format!(
765 "node ({i}, {j}) branches to target {} — more than one \
766 index away, which breaks recombination",
767 b.target
768 )));
769 }
770 for (name, prob) in
771 [("p_up", b.p_up), ("p_mid", b.p_mid), ("p_down", b.p_down)]
772 {
773 if !(prob >= 0.0 && prob <= 1.0) {
774 return Err(RustyQLibError::NumericalError(format!(
775 "node ({i}, {j}): {name} = {prob:.6} outside [0, 1] \
776 (adjust the spacing or the branching rule)"
777 )));
778 }
779 }
780 if (b.p_up + b.p_mid + b.p_down - 1.0).abs() > 1e-9 {
781 return Err(RustyQLibError::NumericalError(format!(
782 "node ({i}, {j}): probabilities sum to {:.9}, not 1",
783 b.p_up + b.p_mid + b.p_down
784 )));
785 }
786 next_lo = next_lo.min(b.target - 1);
787 next_hi = next_hi.max(b.target + 1);
788 layer.push(b);
789 }
790 branches.push(layer);
791 j_min.push(next_lo);
792 j_max.push(next_hi);
793 }
794 Ok(TrinomialLattice { dt, dx, j_min, j_max, branches })
795 }
796
797 pub fn steps(&self) -> usize {
798 self.branches.len()
799 }
800
801 pub fn layer_range(&self, i: usize) -> (i32, i32) {
803 (self.j_min[i], self.j_max[i])
804 }
805
806 pub fn price(
811 &self,
812 node_df: &dyn Fn(usize, i32) -> f64,
813 terminal: &dyn Fn(i32) -> f64,
814 exercise: Option<&dyn Fn(usize, i32, f64) -> f64>,
815 ) -> f64 {
816 let n = self.steps();
817 let (lo_n, hi_n) = (self.j_min[n], self.j_max[n]);
818 let mut values: Vec<f64> = (lo_n..=hi_n).map(terminal).collect();
819 for i in (0..n).rev() {
820 let (lo, hi) = (self.j_min[i], self.j_max[i]);
821 let next_lo = self.j_min[i + 1];
822 let mut layer = Vec::with_capacity((hi - lo + 1) as usize);
823 for j in lo..=hi {
824 let b = self.branches[i][(j - lo) as usize];
825 let k = (b.target - next_lo) as usize;
826 let expected = b.p_up * values[k + 1] + b.p_mid * values[k]
827 + b.p_down * values[k - 1];
828 let cont = node_df(i, j) * expected;
829 layer.push(match exercise {
830 Some(ex) => ex(i, j, cont),
831 None => cont,
832 });
833 }
834 values = layer;
835 }
836 values[0]
837 }
838
839 pub fn arrow_debreu(&self, node_df: &dyn Fn(usize, i32) -> f64) -> Vec<Vec<f64>> {
845 let n = self.steps();
846 let mut q: Vec<Vec<f64>> = Vec::with_capacity(n + 1);
847 q.push(vec![1.0]);
848 for i in 0..n {
849 let (lo, hi) = (self.j_min[i], self.j_max[i]);
850 let (next_lo, next_hi) = (self.j_min[i + 1], self.j_max[i + 1]);
851 let mut next = vec![0.0; (next_hi - next_lo + 1) as usize];
852 for j in lo..=hi {
853 let b = self.branches[i][(j - lo) as usize];
854 let flow = q[i][(j - lo) as usize] * node_df(i, j);
855 let k = (b.target - next_lo) as usize;
856 next[k + 1] += b.p_up * flow;
857 next[k] += b.p_mid * flow;
858 next[k - 1] += b.p_down * flow;
859 }
860 q.push(next);
861 }
862 q
863 }
864}
865
866pub fn diffusion_branching(nu: f64, sigma: f64, dt: f64, dx: f64) -> TrinomialBranch {
871 let v = (sigma * sigma * dt + nu * nu * dt * dt) / (dx * dx);
872 let m = nu * dt / dx;
873 TrinomialBranch {
874 target: 0, p_up: 0.5 * (v + m),
876 p_mid: 1.0 - v,
877 p_down: 0.5 * (v - m),
878 }
879}
880
881pub fn hull_white_branching(a: f64, dt: f64, j: i32, j_cap: i32) -> TrinomialBranch {
887 let ajdt = a * j as f64 * dt;
888 let ajdt2 = ajdt * ajdt;
889 if j >= j_cap {
890 TrinomialBranch {
892 target: j - 1,
893 p_up: 7.0 / 6.0 + 0.5 * (ajdt2 - 3.0 * ajdt),
894 p_mid: -1.0 / 3.0 - ajdt2 + 2.0 * ajdt,
895 p_down: 1.0 / 6.0 + 0.5 * (ajdt2 - ajdt),
896 }
897 } else if j <= -j_cap {
898 TrinomialBranch {
900 target: j + 1,
901 p_up: 1.0 / 6.0 + 0.5 * (ajdt2 + ajdt),
902 p_mid: -1.0 / 3.0 - ajdt2 - 2.0 * ajdt,
903 p_down: 7.0 / 6.0 + 0.5 * (ajdt2 + 3.0 * ajdt),
904 }
905 } else {
906 TrinomialBranch {
907 target: j,
908 p_up: 1.0 / 6.0 + 0.5 * (ajdt2 - ajdt),
909 p_mid: 2.0 / 3.0 - ajdt2,
910 p_down: 1.0 / 6.0 + 0.5 * (ajdt2 + ajdt),
911 }
912 }
913}
914
915pub fn hull_white_j_cap(a: f64, dt: f64) -> i32 {
917 (0.184 / (a * dt)).ceil() as i32
918}
919
920#[cfg(test)]
921mod tests {
922 use super::*;
923 use crate::core::utils::norm_cdf;
924
925 const S: f64 = 100.0;
926 const K: f64 = 100.0;
927 const R: f64 = 0.05;
928 const Q: f64 = 0.02;
929 const SIGMA: f64 = 0.3;
930 const T: f64 = 1.0;
931
932 fn bs_call() -> f64 {
933 let b = R - Q;
934 let d1 = ((S / K).ln() + (b + 0.5 * SIGMA * SIGMA) * T) / (SIGMA * T.sqrt());
935 let d2 = d1 - SIGMA * T.sqrt();
936 S * ((b - R) * T).exp() * norm_cdf(d1) - K * (-R * T).exp() * norm_cdf(d2)
937 }
938
939 fn tree_call(tree_type: BinomialTreeType, steps: usize) -> f64 {
940 let n = tree_type.effective_steps(steps);
941 let params = tree_type.params(S, K, R - Q, SIGMA, T, n).unwrap();
942 let df = (-R * T / n as f64).exp();
943 price_backward(S, ¶ms, n, df, &|s| (s - K).max(0.0), None)
944 }
945
946 fn all_types() -> [BinomialTreeType; 6] {
947 use BinomialTreeType::*;
948 [CoxRossRubinstein, JarrowRudd, Tian, Trigeorgis, LeisenReimer, AdditiveEqp]
949 }
950
951 #[test]
952 fn every_scheme_converges_to_black_scholes() {
953 let reference = bs_call();
954 for tree_type in all_types() {
955 let price = tree_call(tree_type, 1000);
956 let tol = if tree_type == BinomialTreeType::AdditiveEqp { 2e-2 } else { 5e-3 };
959 assert!(
960 (price - reference).abs() < tol,
961 "{tree_type:?}: {price} vs BS {reference}"
962 );
963 }
964 }
965
966 #[test]
967 fn leisen_reimer_beats_crr_by_orders_of_magnitude() {
968 let reference = bs_call();
969 let lr_err = (tree_call(BinomialTreeType::LeisenReimer, 101) - reference).abs();
970 let crr_err = (tree_call(BinomialTreeType::CoxRossRubinstein, 101) - reference).abs();
971 assert!(
972 lr_err < 1e-4,
973 "LR at 101 steps is second order, err {lr_err}"
974 );
975 assert!(
976 lr_err * 10.0 < crr_err,
977 "LR(101) err {lr_err} must be >10x tighter than CRR(101) err {crr_err}"
978 );
979 }
980
981 #[test]
982 fn leisen_reimer_bumps_even_step_counts_to_odd() {
983 assert_eq!(BinomialTreeType::LeisenReimer.effective_steps(100), 101);
984 assert_eq!(BinomialTreeType::LeisenReimer.effective_steps(101), 101);
985 assert_eq!(BinomialTreeType::CoxRossRubinstein.effective_steps(100), 100);
986 }
987
988 #[test]
989 fn optimized_and_diagnostic_engines_agree_exactly() {
990 for tree_type in all_types() {
991 let n = tree_type.effective_steps(200);
992 let params = tree_type.params(S, K, R - Q, SIGMA, T, n).unwrap();
993 let dt = T / n as f64;
994 let df = (-R * dt).exp();
995 let terminal = |s: f64| (K - s).max(0.0);
996 let exercise = |_: usize, s: f64, cont: f64| (K - s).max(0.0).max(cont);
997 let fast = price_backward(S, ¶ms, n, df, &terminal, Some(&exercise));
998 let diag = price_with_diagnostics(
999 tree_type, S, ¶ms, n, dt, df, &terminal, Some(&exercise),
1000 );
1001 assert_eq!(fast, diag.price, "{tree_type:?} engines disagree");
1002 assert_eq!(diag.spot_tree.len(), n + 1);
1003 assert_eq!(diag.spot_tree[n].len(), n + 1);
1004 }
1005 }
1006
1007 #[test]
1008 fn one_pass_greeks_match_black_scholes() {
1009 let norm_pdf = |x: f64| (-0.5 * x * x).exp() / (2.0 * std::f64::consts::PI).sqrt();
1010 let b = R - Q;
1011 let sq_t = SIGMA * T.sqrt();
1012 let d1 = ((S / K).ln() + (b + 0.5 * SIGMA * SIGMA) * T) / sq_t;
1013 let d2 = d1 - sq_t;
1014 let carry_df = ((b - R) * T).exp(); let bs_delta = carry_df * norm_cdf(d1);
1016 let bs_gamma = carry_df * norm_pdf(d1) / (S * sq_t);
1017 let bs_theta = -S * carry_df * norm_pdf(d1) * SIGMA / (2.0 * T.sqrt())
1018 - (b - R) * S * carry_df * norm_cdf(d1)
1019 - R * K * (-R * T).exp() * norm_cdf(d2);
1020
1021 for tree_type in [BinomialTreeType::LeisenReimer, BinomialTreeType::JarrowRudd] {
1023 let n = tree_type.effective_steps(1001);
1024 let params = tree_type.params(S, K, b, SIGMA, T, n).unwrap();
1025 let dt = T / n as f64;
1026 let df = (-R * dt).exp();
1027 let terminal = |s: f64| (s - K).max(0.0);
1028 let sol = price_backward_with_greeks(S, ¶ms, n, dt, df, &terminal, None);
1029 let plain = price_backward(S, ¶ms, n, df, &terminal, None);
1030 assert_eq!(sol.price, plain, "{tree_type:?}: one-pass price must match exactly");
1031 assert!(
1032 (sol.delta - bs_delta).abs() < 2e-3,
1033 "{tree_type:?} delta {} vs BS {bs_delta}",
1034 sol.delta
1035 );
1036 assert!(
1037 (sol.gamma - bs_gamma).abs() < 2e-4,
1038 "{tree_type:?} gamma {} vs BS {bs_gamma}",
1039 sol.gamma
1040 );
1041 assert!(
1042 (sol.theta - bs_theta).abs() < 2e-2,
1043 "{tree_type:?} theta {} vs BS {bs_theta}",
1044 sol.theta
1045 );
1046 }
1047 }
1048
1049 #[test]
1050 fn one_pass_greeks_agree_with_the_diagnostic_engine() {
1051 let tree_type = BinomialTreeType::CoxRossRubinstein;
1053 let n = 200;
1054 let params = tree_type.params(S, K, R - Q, SIGMA, T, n).unwrap();
1055 let dt = T / n as f64;
1056 let df = (-R * dt).exp();
1057 let terminal = |s: f64| (K - s).max(0.0);
1058 let exercise = |_: usize, s: f64, cont: f64| (K - s).max(0.0).max(cont);
1059 let sol =
1060 price_backward_with_greeks(S, ¶ms, n, dt, df, &terminal, Some(&exercise));
1061 let diag = price_with_diagnostics(
1062 tree_type, S, ¶ms, n, dt, df, &terminal, Some(&exercise),
1063 );
1064 assert_eq!(sol.price, diag.price);
1065 assert_eq!(sol.delta, diag.delta);
1066 assert_eq!(sol.gamma, diag.gamma);
1067 assert!((sol.theta - diag.theta).abs() < 1e-10, "{} vs {}", sol.theta, diag.theta);
1070 }
1071
1072 #[test]
1073 fn american_put_diagnostics_show_the_exercise_region() {
1074 let tree_type = BinomialTreeType::LeisenReimer;
1075 let n = tree_type.effective_steps(201);
1076 let params = tree_type.params(S, K, R - Q, SIGMA, T, n).unwrap();
1077 let dt = T / n as f64;
1078 let df = (-R * dt).exp();
1079 let terminal = |s: f64| (K - s).max(0.0);
1080 let exercise = |_: usize, s: f64, cont: f64| (K - s).max(0.0).max(cont);
1081 let diag = price_with_diagnostics(
1082 tree_type, S, ¶ms, n, dt, df, &terminal, Some(&exercise),
1083 );
1084 let exercised_layers = diag.exercise_boundary.iter().flatten().count();
1086 assert!(exercised_layers > 0, "the put must have an exercise region");
1087 for (lo, hi) in diag.exercise_boundary.iter().flatten() {
1089 assert!(*lo <= *hi && *hi < K);
1090 }
1091 let euro = price_backward(S, ¶ms, n, df, &terminal, None);
1093 assert!(diag.price > euro + 1e-4, "american {} european {euro}", diag.price);
1094 assert!(diag.delta > -1.0 && diag.delta < 0.0);
1096 assert!(diag.gamma > 0.0);
1097 assert!(diag.theta < 0.0);
1098 assert!(diag.elapsed > Duration::ZERO);
1099 }
1100
1101 #[test]
1102 fn convergence_ladder_reports_prices_and_timing() {
1103 let terminal = |s: f64| (s - K).max(0.0);
1104 let ladder =
1105 convergence_study(
1106 BinomialTreeType::LeisenReimer, S, K, R - Q, SIGMA, R, T,
1107 &[25, 51, 101, 201], &terminal, None,
1108 )
1109 .unwrap();
1110 assert_eq!(ladder.len(), 4);
1111 let reference = bs_call();
1112 let errors: Vec<f64> = ladder.iter().map(|p| (p.price - reference).abs()).collect();
1113 assert!(
1115 errors.windows(2).all(|w| w[1] <= w[0] * 1.5),
1116 "LR errors should shrink along the ladder: {errors:?}"
1117 );
1118 assert!(errors[3] < 2e-5, "LR(201) err {}", errors[3]);
1119 }
1120
1121 #[test]
1122 fn degenerate_steps_are_rejected_with_typed_errors() {
1123 let r = BinomialTreeType::CoxRossRubinstein.params(100.0, 100.0, 2.0, 0.05, 1.0, 2);
1125 assert!(matches!(r, Err(RustyQLibError::NumericalError(_))), "{r:?}");
1126 let r = BinomialTreeType::AdditiveEqp.params(100.0, 100.0, 2.0, 0.05, 1.0, 2);
1128 assert!(matches!(r, Err(RustyQLibError::NumericalError(_))), "{r:?}");
1129 let r = BinomialTreeType::Tian.params(100.0, 100.0, 0.03, -0.1, 1.0, 100);
1131 assert!(matches!(r, Err(RustyQLibError::InvalidInput { .. })));
1132 }
1133
1134 #[test]
1135 fn trinomial_diffusion_converges_to_black_scholes() {
1136 let nu = (R - Q) - 0.5 * SIGMA * SIGMA;
1138 let price_at = |n: usize| {
1139 let dt = T / n as f64;
1140 let dx = SIGMA * (3.0 * dt).sqrt();
1141 let proto = diffusion_branching(nu, SIGMA, dt, dx);
1142 let branching = |_: usize, j: i32| TrinomialBranch { target: j, ..proto };
1143 let lattice = TrinomialLattice::build(n, dt, dx, &branching).unwrap();
1144 let df = (-R * dt).exp();
1145 (
1146 lattice.price(&|_, _| df, &|j| (S * (j as f64 * dx).exp() - K).max(0.0), None),
1147 dx,
1148 lattice,
1149 df,
1150 )
1151 };
1152 let reference = bs_call();
1153 let coarse_err = (price_at(250).0 - reference).abs();
1154 let (call, dx, lattice, df) = price_at(1000);
1155 let fine_err = (call - reference).abs();
1156 assert!(fine_err < 5e-3, "trinomial {call} vs BS {reference}");
1157 assert!(
1159 fine_err < coarse_err,
1160 "error must shrink with steps: {fine_err} vs {coarse_err}"
1161 );
1162 let n = 1000usize;
1163 let _ = n;
1164
1165 let terminal_put = |j: i32| (K - S * (j as f64 * dx).exp()).max(0.0);
1167 let euro = lattice.price(&|_, _| df, &terminal_put, None);
1168 let ex = |_: usize, j: i32, cont: f64| {
1169 (K - S * (j as f64 * dx).exp()).max(0.0).max(cont)
1170 };
1171 let amer = lattice.price(&|_, _| df, &terminal_put, Some(&ex));
1172 assert!(amer >= euro - 1e-12, "american {amer} vs european {euro}");
1173 }
1174
1175 #[test]
1176 fn arrow_debreu_prices_sum_to_the_discount_factor() {
1177 let n = 100;
1178 let dt = 0.01;
1179 let dx = 0.2 * (3.0_f64 * dt).sqrt(); let proto = diffusion_branching(0.0, 0.2, dt, dx);
1181 let branching = |_: usize, j: i32| TrinomialBranch { target: j, ..proto };
1182 let lattice = TrinomialLattice::build(n, dt, dx, &branching).unwrap();
1183 let df = (-0.05_f64 * dt).exp();
1184 let q = lattice.arrow_debreu(&|_, _| df);
1185 for i in [1usize, 50, 100] {
1187 let total: f64 = q[i].iter().sum();
1188 let expected = (-0.05 * i as f64 * dt).exp();
1189 assert!(
1190 (total - expected).abs() < 1e-12,
1191 "layer {i}: sum {total} vs df {expected}"
1192 );
1193 }
1194 }
1195
1196 #[test]
1197 fn hull_white_tree_reprices_the_vasicek_bond() {
1198 let (a, sigma, t_mat) = (0.10, 0.015, 5.0);
1203 let n = 500;
1204 let dt = t_mat / n as f64;
1205 let dx = sigma * (3.0 * dt).sqrt();
1206 let cap = hull_white_j_cap(a, dt);
1207 let branching = |_: usize, j: i32| hull_white_branching(a, dt, j, cap);
1208 let lattice = TrinomialLattice::build(n, dt, dx, &branching).unwrap();
1209 assert!(lattice.layer_range(n).1 == cap, "edge branching untested");
1211 let node_df = |_: usize, j: i32| (-(j as f64 * dx) * dt).exp();
1212 let tree_price = lattice.price(&node_df, &|_| 1.0, None);
1213
1214 let b_t = (1.0 - (-a * t_mat).exp()) / a;
1215 let closed_form = (sigma * sigma * (t_mat - b_t) / (2.0 * a * a)
1216 - sigma * sigma * b_t * b_t / (4.0 * a))
1217 .exp();
1218 let rel_err = (tree_price - closed_form).abs() / closed_form;
1219 assert!(
1220 rel_err < 1e-3,
1221 "tree {tree_price} vs Vasicek {closed_form} (rel err {rel_err:.2e})"
1222 );
1223
1224 let q = lattice.arrow_debreu(&node_df);
1226 let via_q: f64 = q[n].iter().sum();
1227 assert!((via_q - tree_price).abs() < 1e-12);
1228 }
1229
1230 #[test]
1231 fn trinomial_rejects_invalid_branching() {
1232 let proto = diffusion_branching(5.0, 0.05, 0.5, 0.02);
1234 let branching = |_: usize, j: i32| TrinomialBranch { target: j, ..proto };
1235 let r = TrinomialLattice::build(4, 0.5, 0.02, &branching);
1236 assert!(matches!(r, Err(RustyQLibError::NumericalError(_))), "{r:?}");
1237 let jumpy = |_: usize, j: i32| TrinomialBranch {
1239 target: j + 2,
1240 p_up: 1.0 / 6.0,
1241 p_mid: 2.0 / 3.0,
1242 p_down: 1.0 / 6.0,
1243 };
1244 let r = TrinomialLattice::build(4, 0.01, 0.02, &jumpy);
1245 assert!(matches!(r, Err(RustyQLibError::NumericalError(_))), "{r:?}");
1246 }
1247
1248 #[test]
1249 fn term_lattice_with_flat_inputs_matches_the_uniform_tree() {
1250 let n = 500;
1251 let flat_rate = |_: f64, _: f64| R;
1252 let flat_carry = |_: f64, _: f64| Q;
1253 let flat_var = |t: f64| SIGMA * SIGMA * t;
1254 let lattice = TermLattice::build(n, T, &flat_rate, &flat_carry, &flat_var).unwrap();
1255 let term = lattice.price(S, &|s| (s - K).max(0.0), None);
1256 let params = BinomialTreeType::CoxRossRubinstein.params(S, K, R - Q, SIGMA, T, n).unwrap();
1258 let uniform =
1259 price_backward(S, ¶ms, n, (-R * T / n as f64).exp(), &|s| (s - K).max(0.0), None);
1260 assert!((term - uniform).abs() < 1e-9, "term {term} vs uniform {uniform}");
1261 }
1262
1263 #[test]
1264 fn time_dependent_vol_prices_to_the_equivalent_total_variance() {
1265 let (s1, s2) = (0.2, 0.4);
1269 let var = move |t: f64| {
1270 if t <= 0.5 { s1 * s1 * t } else { s1 * s1 * 0.5 + s2 * s2 * (t - 0.5) }
1271 };
1272 let sigma_eq = (0.5f64 * (s1 * s1 + s2 * s2)).sqrt();
1273 let flat_rate = |_: f64, _: f64| R;
1274 let flat_carry = |_: f64, _: f64| Q;
1275 let lattice = TermLattice::build(2000, T, &flat_rate, &flat_carry, &var).unwrap();
1276 let term = lattice.price(S, &|s| (s - K).max(0.0), None);
1277 let b = R - Q;
1278 let d1 = ((S / K).ln() + (b + 0.5 * sigma_eq * sigma_eq) * T) / (sigma_eq * T.sqrt());
1279 let d2 = d1 - sigma_eq * T.sqrt();
1280 let bs = S * ((b - R) * T).exp() * norm_cdf(d1) - K * (-R * T).exp() * norm_cdf(d2);
1281 assert!((term - bs).abs() < 5e-3, "term {term} vs BS(sigma_eq) {bs}");
1282 }
1283
1284 #[test]
1285 fn time_dependent_rates_discount_and_drift_exactly() {
1286 let fwd = |t1: f64, t2: f64| {
1289 let integral = |t: f64| {
1290 if t <= 0.5 { 0.02 * t } else { 0.02 * 0.5 + 0.08 * (t - 0.5) }
1291 };
1292 (integral(t2) - integral(t1)) / (t2 - t1)
1293 };
1294 let flat_carry = |_: f64, _: f64| Q;
1295 let var = |t: f64| SIGMA * SIGMA * t;
1296 let lattice = TermLattice::build(2000, T, &fwd, &flat_carry, &var).unwrap();
1297 let term = lattice.price(S, &|s| (s - K).max(0.0), None);
1298 let r_eq = 0.05;
1299 let b = r_eq - Q;
1300 let d1 = ((S / K).ln() + (b + 0.5 * SIGMA * SIGMA) * T) / (SIGMA * T.sqrt());
1301 let d2 = d1 - SIGMA * T.sqrt();
1302 let bs = S * ((b - r_eq) * T).exp() * norm_cdf(d1) - K * (-r_eq * T).exp() * norm_cdf(d2);
1303 assert!((term - bs).abs() < 5e-3, "term {term} vs BS(r_eq) {bs}");
1304
1305 let exercise = |_: usize, _: f64, s: f64, cont: f64| (K - s).max(0.0).max(cont);
1307 let amer = lattice.price(S, &|s| (K - s).max(0.0), Some(&exercise));
1308 let euro = lattice.price(S, &|s| (K - s).max(0.0), None);
1309 assert!(amer >= euro - 1e-12, "american {amer} vs european {euro}");
1310 }
1311
1312 #[test]
1313 fn term_lattice_rejects_bad_term_structures() {
1314 let flat_rate = |_: f64, _: f64| R;
1315 let flat_carry = |_: f64, _: f64| Q;
1316 let bad_var = |t: f64| 0.09 * (1.0 - t).max(0.01);
1318 let r = TermLattice::build(100, T, &flat_rate, &flat_carry, &bad_var);
1319 assert!(r.is_err(), "{r:?}");
1320 let big_rate = |_: f64, _: f64| 5.0;
1322 let var = |t: f64| 0.01 * t;
1323 let r = TermLattice::build(4, T, &big_rate, &flat_carry, &var);
1324 assert!(matches!(r, Err(RustyQLibError::NumericalError(_))), "{r:?}");
1325 }
1326
1327 #[test]
1328 fn tree_type_parses_from_contract_strings() {
1329 use std::str::FromStr;
1330 for (s, expected) in [
1331 ("CRR", BinomialTreeType::CoxRossRubinstein),
1332 ("LeisenReimer", BinomialTreeType::LeisenReimer),
1333 ("lr", BinomialTreeType::LeisenReimer),
1334 ("jarrow_rudd", BinomialTreeType::JarrowRudd),
1335 ("Tian", BinomialTreeType::Tian),
1336 ("trigeorgis", BinomialTreeType::Trigeorgis),
1337 ("EQP", BinomialTreeType::AdditiveEqp),
1338 ] {
1339 assert_eq!(BinomialTreeType::from_str(s).unwrap(), expected);
1340 }
1341 assert!(BinomialTreeType::from_str("no_such_tree").is_err());
1342 }
1343}