Skip to main content

rustyqlib/equity/
perpetual.rs

1//! Perpetual (infinite-maturity) American options — Merton (1973)
2//! closed forms. Unlike the finite-maturity approximations
3//! ([`baw`](crate::equity::baw),
4//! [`bjerksund_stensland`](crate::equity::bjerksund_stensland)) these are
5//! **exact**: with no expiry the American value solves the stationary ODE
6//! `1/2 sigma^2 S^2 V'' + b S V' - r V = 0` with value matching and
7//! smooth pasting at a constant exercise boundary.
8//!
9//! The exponents `y1 > 1 > 0 > y2` are the roots of the quadratic
10//! `1/2 sigma^2 y (y - 1) + b y - r = 0` — the same `beta` that drives
11//! the Barone-Adesi-Whaley and Bjerksund-Stensland boundaries, whose
12//! infinite-maturity limit these formulas are. Finite-maturity American
13//! prices increase in maturity toward the perpetual value (tested).
14//!
15//! Conventions match the rest of the library: `q` is the total carry
16//! (dividend yield + borrow), `b = r - q`.
17
18/// The positive (`y1`) and negative (`y2`) roots of the fundamental
19/// quadratic.
20fn roots(r: f64, b: f64, sigma: f64) -> (f64, f64) {
21    let v2 = sigma * sigma;
22    let half_shift = 0.5 - b / v2;
23    let disc = ((b / v2 - 0.5).powi(2) + 2.0 * r / v2).sqrt();
24    (half_shift + disc, half_shift - disc)
25}
26
27/// Perpetual American call.
28///
29/// Requires `q > 0` (i.e. `b < r`) for a finite value: without a carry
30/// cost early exercise is never optimal and the value equals the spot
31/// (`b = r`); with `b > r` the value is unbounded and infinity is
32/// returned.
33pub fn perpetual_call(s: f64, k: f64, r: f64, q: f64, sigma: f64) -> f64 {
34    assert!(s > 0.0 && k > 0.0 && sigma > 0.0, "need positive spot, strike and vol");
35    let b = r - q;
36    if b > r {
37        return f64::INFINITY; // undiscounted growth beats financing
38    }
39    if b == r {
40        return s; // never exercised; the option is worth the stock
41    }
42    let (y1, _) = roots(r, b, sigma);
43    let boundary = y1 / (y1 - 1.0) * k;
44    if s >= boundary {
45        return s - k;
46    }
47    k / (y1 - 1.0) * (((y1 - 1.0) / y1) * (s / k)).powf(y1)
48}
49
50/// Perpetual American put. Requires `r > 0` (with no discounting the
51/// optimal-stopping problem degenerates).
52pub fn perpetual_put(s: f64, k: f64, r: f64, q: f64, sigma: f64) -> f64 {
53    assert!(s > 0.0 && k > 0.0 && sigma > 0.0, "need positive spot, strike and vol");
54    assert!(r > 0.0, "the perpetual put needs a positive risk-free rate");
55    let b = r - q;
56    let (_, y2) = roots(r, b, sigma);
57    let boundary = y2 / (y2 - 1.0) * k;
58    if s <= boundary {
59        return k - s;
60    }
61    k / (1.0 - y2) * (((y2 - 1.0) / y2) * (s / k)).powf(y2)
62}
63
64/// The constant early-exercise boundary: exercise the call once the spot
65/// rises to `y1/(y1-1) K`, the put once it falls to `y2/(y2-1) K`.
66pub fn exercise_boundary(
67    k: f64,
68    r: f64,
69    q: f64,
70    sigma: f64,
71    put_or_call: crate::core::trade::PutOrCall,
72) -> f64 {
73    let b = r - q;
74    let (y1, y2) = roots(r, b, sigma);
75    match put_or_call {
76        crate::core::trade::PutOrCall::Call => {
77            assert!(b < r, "the perpetual call is never exercised when b >= r");
78            y1 / (y1 - 1.0) * k
79        }
80        crate::core::trade::PutOrCall::Put => y2 / (y2 - 1.0) * k,
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use crate::core::trade::PutOrCall;
88
89    const S: f64 = 100.0;
90    const K: f64 = 100.0;
91    const R: f64 = 0.05;
92    const Q: f64 = 0.03;
93    const V: f64 = 0.25;
94
95    #[test]
96    fn golden_values_match_the_validated_reference() {
97        assert!((perpetual_call(S, K, R, Q, V) - 40.3730823948).abs() < 1e-9);
98        assert!((perpetual_put(S, K, R, Q, V) - 23.4169723789).abs() < 1e-9);
99        assert!((exercise_boundary(K, R, Q, V, PutOrCall::Call) - 318.505635).abs() < 1e-5);
100        assert!((exercise_boundary(K, R, Q, V, PutOrCall::Put) - 52.327698).abs() < 1e-5);
101    }
102
103    #[test]
104    fn solves_the_stationary_ode_exactly() {
105        // 1/2 v^2 S^2 V'' + b S V' - r V = 0 in the continuation region
106        let b = R - Q;
107        for f in [
108            (|s: f64| perpetual_call(s, K, R, Q, V)) as fn(f64) -> f64,
109            |s: f64| perpetual_put(s, K, R, Q, V),
110        ] {
111            for s in [60.0, 80.0, 100.0, 150.0] {
112                let h = s * 1e-4;
113                let (v0, vp, vm) = (f(s), f(s + h), f(s - h));
114                let d1 = (vp - vm) / (2.0 * h);
115                let d2 = (vp - 2.0 * v0 + vm) / (h * h);
116                let residual = 0.5 * V * V * s * s * d2 + b * s * d1 - R * v0;
117                assert!(residual.abs() < 1e-5 * (1.0 + v0), "S = {s}: residual {residual}");
118            }
119        }
120    }
121
122    #[test]
123    fn value_matching_and_smooth_pasting_at_the_boundary() {
124        let call_boundary = exercise_boundary(K, R, Q, V, PutOrCall::Call);
125        let put_boundary = exercise_boundary(K, R, Q, V, PutOrCall::Put);
126        // value matching: the formula meets intrinsic at the boundary
127        assert!((perpetual_call(call_boundary, K, R, Q, V) - (call_boundary - K)).abs() < 1e-9);
128        assert!((perpetual_put(put_boundary, K, R, Q, V) - (K - put_boundary)).abs() < 1e-9);
129        // smooth pasting: the derivative meets +-1 there
130        let h = 1e-5;
131        let call_slope =
132            (perpetual_call(call_boundary - h, K, R, Q, V)
133                - perpetual_call(call_boundary - 2.0 * h, K, R, Q, V))
134                / h;
135        assert!((call_slope - 1.0).abs() < 1e-4, "call slope {call_slope}");
136        let put_slope = (perpetual_put(put_boundary + 2.0 * h, K, R, Q, V)
137            - perpetual_put(put_boundary + h, K, R, Q, V))
138            / h;
139        assert!((put_slope + 1.0).abs() < 1e-4, "put slope {put_slope}");
140    }
141
142    #[test]
143    fn put_call_duality_holds() {
144        // McDonald-Schroder: P(S, K, r, q) = C(K, S, r' = q, q' = r)
145        let p = perpetual_put(S, K, R, Q, V);
146        let c = perpetual_call(K, S, Q, R, V);
147        assert!((p - c).abs() < 1e-12, "{p} vs {c}");
148    }
149
150    #[test]
151    fn degenerate_carry_cases() {
152        // no carry cost: the perpetual call is worth the stock
153        assert_eq!(perpetual_call(100.0, 80.0, 0.05, 0.0, 0.3), 100.0);
154        // negative q (carry above r): unbounded
155        assert!(perpetual_call(100.0, 80.0, 0.05, -0.01, 0.3).is_infinite());
156        // deep in the exercise regions: intrinsic
157        assert_eq!(perpetual_call(500.0, 100.0, R, Q, V), 400.0);
158        assert_eq!(perpetual_put(30.0, 100.0, R, Q, V), 70.0);
159    }
160
161    #[test]
162    fn finite_maturity_american_prices_increase_toward_the_perpetual() {
163        // Bjerksund-Stensland is a lower bound on the true American price,
164        // and the true price is bounded by the perpetual — so the BS2002
165        // ladder must increase in maturity and stay below the perpetual.
166        // (BAW is NOT a bound: at T = 40 it overshoots this perpetual by
167        // ~0.6, which is exactly why it is not used for this test.)
168        use crate::equity::bjerksund_stensland;
169        let perpetual = perpetual_put(S, K, R, Q, V);
170        let mut last = 0.0;
171        for t in [1.0, 5.0, 15.0, 40.0] {
172            let finite = bjerksund_stensland::price(S, K, R, Q, V, t, PutOrCall::Put);
173            assert!(finite > last, "not increasing at T = {t}");
174            assert!(finite <= perpetual + 1e-9, "T = {t}: {finite} above perpetual {perpetual}");
175            last = finite;
176        }
177        // by T = 40 the finite price is close to the perpetual limit
178        assert!(perpetual - last < 0.10 * perpetual, "T=40 {last} vs perpetual {perpetual}");
179    }
180}