Skip to main content

rustyqlib/equity/
asian.rs

1//! Analytic pricing of Asian (average) options.
2//!
3//! - **Geometric average price**: exact closed form — the geometric average
4//!   of lognormals is lognormal. Supports discrete equally spaced averaging
5//!   (`n` points, matching Monte Carlo monitoring) and the continuous limit.
6//! - **Arithmetic average price**: Turnbull-Wakeman (1991) lognormal
7//!   moment-matching approximation, continuous averaging.
8//!
9//! Both assume the averaging period spans the whole life of the option and
10//! has not yet started. Floating-strike (average strike) Asians have no
11//! implemented closed form and price on the Monte Carlo engine.
12
13use crate::core::trade::PutOrCall;
14use crate::core::utils::N;
15
16/// How the average is computed along the path.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum AveragingType {
19    Arithmetic,
20    Geometric,
21}
22
23/// Fixed strike (average price) vs floating strike (average strike).
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum AsianStrikeType {
26    FixedStrike,
27    FloatingStrike,
28}
29
30fn black(df_r: f64, forward: f64, k: f64, log_var: f64, put_or_call: PutOrCall) -> f64 {
31    let sqrt_v = log_var.sqrt();
32    let d1 = ((forward / k).ln() + 0.5 * log_var) / sqrt_v;
33    let d2 = d1 - sqrt_v;
34    match put_or_call {
35        PutOrCall::Call => df_r * (forward * N(d1) - k * N(d2)),
36        PutOrCall::Put => df_r * (k * N(-d2) - forward * N(-d1)),
37    }
38}
39
40/// Exact price of a geometric average-price Asian.
41///
42/// `n = Some(count)`: discrete averaging at `t_i = i*T/n`, `i = 1..=n`
43/// (matches Monte Carlo monitoring, spot excluded). `n = None`: continuous
44/// averaging limit.
45#[allow(clippy::too_many_arguments)]
46pub fn geometric_asian_price(
47    s: f64,
48    k: f64,
49    r: f64,
50    q: f64,
51    sigma: f64,
52    t: f64,
53    n: Option<usize>,
54    put_or_call: PutOrCall,
55) -> f64 {
56    assert!(s > 0.0 && k > 0.0 && sigma > 0.0 && t > 0.0);
57    let b = r - q;
58    let (mean_factor, var_factor) = match n {
59        Some(n) => {
60            assert!(n > 0);
61            let nf = n as f64;
62            ((nf + 1.0) / (2.0 * nf), (nf + 1.0) * (2.0 * nf + 1.0) / (6.0 * nf * nf))
63        }
64        None => (0.5, 1.0 / 3.0),
65    };
66    let mu = (b - 0.5 * sigma * sigma) * t * mean_factor;
67    let log_var = sigma * sigma * t * var_factor;
68    let forward = s * (mu + 0.5 * log_var).exp();
69    black((-r * t).exp(), forward, k, log_var, put_or_call)
70}
71
72/// Turnbull-Wakeman approximation for an arithmetic average-price Asian
73/// (continuous averaging): the first two moments of the average are matched
74/// to a lognormal and priced with Black's formula.
75pub fn turnbull_wakeman_price(
76    s: f64,
77    k: f64,
78    r: f64,
79    q: f64,
80    sigma: f64,
81    t: f64,
82    put_or_call: PutOrCall,
83) -> f64 {
84    assert!(s > 0.0 && k > 0.0 && sigma > 0.0 && t > 0.0);
85    let b = r - q;
86    let s2 = sigma * sigma;
87    let (m1, m2) = if b.abs() > 1e-8 {
88        let m1 = ((b * t).exp() - 1.0) / (b * t);
89        let m2 = 2.0 * ((2.0 * b + s2) * t).exp() / ((b + s2) * (2.0 * b + s2) * t * t)
90            + 2.0 / (b * t * t) * (1.0 / (2.0 * b + s2) - (b * t).exp() / (b + s2));
91        (m1, m2)
92    } else {
93        let m1 = 1.0;
94        let m2 = (2.0 * (s2 * t).exp() - 2.0 * (1.0 + s2 * t)) / (s2 * s2 * t * t);
95        (m1, m2)
96    };
97    let forward = s * m1;
98    let log_var = (m2 / (m1 * m1)).ln(); // sigma_A^2 * T
99    black((-r * t).exp(), forward, k, log_var, put_or_call)
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    const S: f64 = 100.0;
107    const R: f64 = 0.05;
108    const Q: f64 = 0.02;
109    const SIG: f64 = 0.3;
110    const T: f64 = 1.0;
111
112    #[test]
113    fn geometric_golden_values() {
114        // independently generated oracle values
115        assert!((geometric_asian_price(S, 100.0, R, Q, SIG, T, Some(252), PutOrCall::Call)
116            - 6.976295)
117            .abs()
118            < 1e-5);
119        assert!((geometric_asian_price(S, 100.0, R, Q, SIG, T, None, PutOrCall::Call) - 6.953600)
120            .abs()
121            < 1e-5);
122    }
123
124    #[test]
125    fn turnbull_wakeman_golden_value() {
126        let price = turnbull_wakeman_price(S, 100.0, R, Q, SIG, T, PutOrCall::Call);
127        assert!((price - 7.409272).abs() < 1e-5, "{price}");
128    }
129
130    #[test]
131    fn geometric_put_call_parity() {
132        // C - P = e^{-rT} (F_G - K) with the same lognormal forward
133        for n in [Some(12), Some(252), None] {
134            let c = geometric_asian_price(S, 90.0, R, Q, SIG, T, n, PutOrCall::Call);
135            let p = geometric_asian_price(S, 90.0, R, Q, SIG, T, n, PutOrCall::Put);
136            // recover F_G from a deep parity-free identity: price both at a
137            // strike and check C - P is strike-linear with slope -e^{-rT}
138            let c2 = geometric_asian_price(S, 110.0, R, Q, SIG, T, n, PutOrCall::Call);
139            let p2 = geometric_asian_price(S, 110.0, R, Q, SIG, T, n, PutOrCall::Put);
140            let df = (-R * T).exp();
141            assert!((((c - p) - (c2 - p2)) - df * 20.0).abs() < 1e-10);
142        }
143    }
144
145    #[test]
146    fn discrete_averaging_converges_to_continuous() {
147        let continuous = geometric_asian_price(S, 100.0, R, Q, SIG, T, None, PutOrCall::Call);
148        let fine = geometric_asian_price(S, 100.0, R, Q, SIG, T, Some(100_000), PutOrCall::Call);
149        assert!((fine - continuous).abs() < 1e-3);
150    }
151
152    #[test]
153    fn averaging_reduces_option_value_below_vanilla() {
154        use crate::equity::blackscholes::bs_price;
155        let vanilla = bs_price(S, 100.0, R, Q, SIG, T, PutOrCall::Call);
156        let geo = geometric_asian_price(S, 100.0, R, Q, SIG, T, None, PutOrCall::Call);
157        let arith = turnbull_wakeman_price(S, 100.0, R, Q, SIG, T, PutOrCall::Call);
158        assert!(geo < arith, "AM-GM: arithmetic average dominates geometric");
159        assert!(arith < vanilla, "averaging reduces effective volatility");
160    }
161
162    #[test]
163    fn zero_cost_of_carry_branch() {
164        // r = q exercises the b = 0 moment formulas
165        let price = turnbull_wakeman_price(S, 100.0, 0.03, 0.03, SIG, T, PutOrCall::Call);
166        assert!(price > 0.0 && price.is_finite());
167        // continuity across the branch: b = 1e-9 vs b = 0
168        let near = turnbull_wakeman_price(S, 100.0, 0.03 + 1e-9, 0.03, SIG, T, PutOrCall::Call);
169        assert!((price - near).abs() < 1e-5);
170    }
171}