quant/src/markovfunctional.rs
The Markov functional construction: one driver, and a functional form chosen by matching quantiles rather than by writing down an SDE for the rate itself.
//! The Markov functional construction: one driver, and a functional form//! chosen by matching quantiles rather than by writing down an SDE for the//! rate itself.//!//! The fitting-and-testing chapter's table lists Markov functional models//! alongside short rate and market models without ever building one. This is//! that model, in the simplest case that still shows the whole mechanism: a//! single driftless Brownian motion under the terminal measure, and a rate at//! each fixing date defined as the monotone function of the driver that sends//! the driver's own quantile to the market's. use crate::black::norm_cdf;use crate::special::norm_inv; /// The Markov functional form for a rate whose market-implied quantile/// function (inverse CDF, under the measure the driver lives in) is `target`.////// `x` is the driver's value at the fixing date `t`, driftless Brownian so/// `x ~ N(0, t)`; its own CDF is `norm_cdf(x / sqrt(t))`. Composing that with/// the market's quantile function is the whole construction: send the/// driver's quantile to the rate's. There is no freedom left once `target` is/// fixed --- the map is the unique monotone one reproducing that marginal,/// which is what "impose one dimension and choose the functional form" means/// once it is written down.pub fn functional_form(target_quantile: impl Fn(f64) -> f64, t: f64, x: f64) -> f64 { target_quantile(norm_cdf(x / t.sqrt()))} /// The quantile function of a lognormal with the given forward and/// volatility, `F exp(sigma sqrt(t) z - sigma^2 t / 2)` at the normal quantile/// `z = norm_inv(p)`.pub fn lognormal_quantile(forward: f64, sigma: f64, t: f64, p: f64) -> f64 { let z = norm_inv(p); forward * (sigma * t.sqrt() * z - 0.5 * sigma * sigma * t).exp()} /// The closed-form driftless lognormal transform of the driver,/// `F exp(sigma x - sigma^2 t / 2)`.////// When the target marginal is itself lognormal, [`functional_form`] reduces/// to exactly this: the flat-smile special case of the general construction,/// checked in the tests below.pub fn lognormal_functional_form(forward: f64, sigma: f64, t: f64, x: f64) -> f64 { forward * (sigma * x - 0.5 * sigma * sigma * t).exp()} #[cfg(test)]mod tests { use super::*; #[test] fn quantile_matching_a_lognormal_target_reproduces_the_lognormal_transform() { // Two routes to the same number: compose norm_cdf with the lognormal // quantile (the general construction), against the closed form the // composition is supposed to collapse to. They share only the claim // that they must agree, nothing about how either is computed. let (forward, sigma, t) = (100.0f64, 0.3f64, 2.0f64); for x in [-2.5f64, -1.0, -0.1, 0.0, 0.4, 1.3, 2.8] { let general = functional_form(|p| lognormal_quantile(forward, sigma, t, p), t, x); let closed = lognormal_functional_form(forward, sigma, t, x); assert!( (general / closed - 1.0).abs() < 1e-9, "x={x}: general form {general} against closed form {closed}" ); } } #[test] fn the_functional_form_is_monotone_in_the_driver() { // Reproducing a marginal exactly forces the map to be monotone in the // driver, for any target: two drivers in the same order must give // rates in the same order, or the map could not be a quantile // function composed with another quantile function. let (forward, sigma, t) = (0.03f64, 0.6f64, 5.0f64); let xs = [-3.0f64, -1.5, -0.2, 0.5, 1.7, 3.2]; let mut previous = f64::NEG_INFINITY; for &x in &xs { let rate = functional_form(|p| lognormal_quantile(forward, sigma, t, p), t, x); assert!(rate > previous, "not increasing at x={x}"); previous = rate; } }}