1use crate::core::trade::PutOrCall;
14use crate::core::utils::norm_cdf;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum AveragingType {
19 Arithmetic,
20 Geometric,
21}
22
23#[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 * norm_cdf(d1) - k * norm_cdf(d2)),
36 PutOrCall::Put => df_r * (k * norm_cdf(-d2) - forward * norm_cdf(-d1)),
37 }
38}
39
40#[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
72pub fn geometric_average_strike_price(
84 s: f64,
85 r: f64,
86 q: f64,
87 sigma: f64,
88 t: f64,
89 n: Option<usize>,
90 put_or_call: PutOrCall,
91) -> f64 {
92 assert!(s > 0.0 && sigma > 0.0 && t > 0.0);
93 let b = r - q;
94 let (mean_factor, var_factor, cov_factor) = match n {
97 Some(n) => {
98 assert!(n > 0);
99 let nf = n as f64;
100 (
101 (nf + 1.0) / (2.0 * nf),
102 (nf + 1.0) * (2.0 * nf + 1.0) / (6.0 * nf * nf),
103 (nf + 1.0) / (2.0 * nf),
104 )
105 }
106 None => (0.5, 1.0 / 3.0, 0.5),
107 };
108 let e_terminal = s * (b * t).exp();
109 let e_average = s
110 * ((b - 0.5 * sigma * sigma) * t * mean_factor
111 + 0.5 * sigma * sigma * t * var_factor)
112 .exp();
113 let spread_var = sigma * sigma * t * (1.0 + var_factor - 2.0 * cov_factor);
114 let df = (-r * t).exp();
115 if spread_var <= 1e-16 {
116 return match put_or_call {
118 PutOrCall::Call => df * (e_terminal - e_average).max(0.0),
119 PutOrCall::Put => df * (e_average - e_terminal).max(0.0),
120 };
121 }
122 let sv = spread_var.sqrt();
123 let d1 = ((e_terminal / e_average).ln() + 0.5 * spread_var) / sv;
124 let d2 = d1 - sv;
125 match put_or_call {
126 PutOrCall::Call => df * (e_terminal * norm_cdf(d1) - e_average * norm_cdf(d2)),
127 PutOrCall::Put => df * (e_average * norm_cdf(-d2) - e_terminal * norm_cdf(-d1)),
128 }
129}
130
131pub fn turnbull_wakeman_average_strike_price(
141 s: f64,
142 r: f64,
143 q: f64,
144 sigma: f64,
145 t: f64,
146 put_or_call: PutOrCall,
147) -> f64 {
148 match put_or_call {
149 PutOrCall::Call => turnbull_wakeman_price(s, s, q, r, sigma, t, PutOrCall::Put),
150 PutOrCall::Put => turnbull_wakeman_price(s, s, q, r, sigma, t, PutOrCall::Call),
151 }
152}
153
154pub fn turnbull_wakeman_price(
158 s: f64,
159 k: f64,
160 r: f64,
161 q: f64,
162 sigma: f64,
163 t: f64,
164 put_or_call: PutOrCall,
165) -> f64 {
166 assert!(s > 0.0 && k > 0.0 && sigma > 0.0 && t > 0.0);
167 let b = r - q;
168 let s2 = sigma * sigma;
169 let (m1, m2) = if b.abs() > 1e-8 {
170 let m1 = ((b * t).exp() - 1.0) / (b * t);
171 let m2 = 2.0 * ((2.0 * b + s2) * t).exp() / ((b + s2) * (2.0 * b + s2) * t * t)
172 + 2.0 / (b * t * t) * (1.0 / (2.0 * b + s2) - (b * t).exp() / (b + s2));
173 (m1, m2)
174 } else {
175 let m1 = 1.0;
176 let m2 = (2.0 * (s2 * t).exp() - 2.0 * (1.0 + s2 * t)) / (s2 * s2 * t * t);
177 (m1, m2)
178 };
179 let forward = s * m1;
180 let log_var = (m2 / (m1 * m1)).ln(); black((-r * t).exp(), forward, k, log_var, put_or_call)
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 const S: f64 = 100.0;
189 const R: f64 = 0.05;
190 const Q: f64 = 0.02;
191 const SIG: f64 = 0.3;
192
193 #[test]
194 fn average_strike_single_fixing_is_worthless() {
195 for pc in [PutOrCall::Call, PutOrCall::Put] {
197 let p = geometric_average_strike_price(S, R, Q, SIG, 1.0, Some(1), pc);
198 assert!(p.abs() < 1e-12, "{pc:?}: {p}");
199 }
200 }
201
202 #[test]
203 fn average_strike_exchange_parity_is_exact() {
204 for n in [Some(4), Some(12), None] {
206 let c = geometric_average_strike_price(S, R, Q, SIG, 1.0, n, PutOrCall::Call);
207 let p = geometric_average_strike_price(S, R, Q, SIG, 1.0, n, PutOrCall::Put);
208 let b = R - Q;
209 let (mf, vf) = match n {
210 Some(n) => {
211 let nf = n as f64;
212 ((nf + 1.0) / (2.0 * nf), (nf + 1.0) * (2.0 * nf + 1.0) / (6.0 * nf * nf))
213 }
214 None => (0.5, 1.0 / 3.0),
215 };
216 let e_st = S * b.exp();
217 let e_g = S * ((b - 0.5 * SIG * SIG) * mf + 0.5 * SIG * SIG * vf).exp();
218 let parity = (-R * 1.0f64).exp() * (e_st - e_g);
219 assert!((c - p - parity).abs() < 1e-12, "n = {n:?}");
220 }
221 }
222
223 #[test]
224 fn arithmetic_average_strike_preserves_exact_parity() {
225 let t = 1.0;
229 let b = R - Q;
230 let c = turnbull_wakeman_average_strike_price(S, R, Q, SIG, t, PutOrCall::Call);
231 let p = turnbull_wakeman_average_strike_price(S, R, Q, SIG, t, PutOrCall::Put);
232 let parity = (-R * t).exp() * (S * (b * t).exp() - S * ((b * t).exp() - 1.0) / (b * t));
233 assert!((c - p - parity).abs() < 1e-12, "{}", c - p - parity);
234 let c0 = turnbull_wakeman_average_strike_price(S, 0.03, 0.03, SIG, t, PutOrCall::Call);
236 let p0 = turnbull_wakeman_average_strike_price(S, 0.03, 0.03, SIG, t, PutOrCall::Put);
237 assert!(c0 > 0.0 && p0 > 0.0);
238 assert!((c0 - p0).abs() < 1e-12, "b = 0 parity: E[S_T] = E[A]");
239 }
240
241 #[test]
242 fn arithmetic_average_strike_orders_against_geometric_by_am_gm() {
243 let t = 1.0;
247 let arith_call = turnbull_wakeman_average_strike_price(S, R, Q, SIG, t, PutOrCall::Call);
248 let geo_call = geometric_average_strike_price(S, R, Q, SIG, t, None, PutOrCall::Call);
249 assert!(arith_call < geo_call, "call: arith {arith_call} vs geo {geo_call}");
250 let arith_put = turnbull_wakeman_average_strike_price(S, R, Q, SIG, t, PutOrCall::Put);
251 let geo_put = geometric_average_strike_price(S, R, Q, SIG, t, None, PutOrCall::Put);
252 assert!(arith_put > geo_put, "put: arith {arith_put} vs geo {geo_put}");
253 }
254
255 #[test]
256 fn average_strike_value_grows_with_fixings_toward_the_continuous_limit() {
257 let price = |n| geometric_average_strike_price(S, R, Q, SIG, 1.0, n, PutOrCall::Call);
258 assert!(price(Some(2)) > 0.0);
259 assert!(price(Some(4)) > price(Some(2)));
260 assert!(price(Some(12)) > price(Some(4)));
261 let continuous = price(None);
262 assert!(price(Some(12)) < continuous);
263 assert!((price(Some(5000)) - continuous).abs() < 2e-3, "limit");
264 }
265 const T: f64 = 1.0;
266
267 #[test]
268 fn geometric_golden_values() {
269 assert!((geometric_asian_price(S, 100.0, R, Q, SIG, T, Some(252), PutOrCall::Call)
271 - 6.976295)
272 .abs()
273 < 1e-5);
274 assert!((geometric_asian_price(S, 100.0, R, Q, SIG, T, None, PutOrCall::Call) - 6.953600)
275 .abs()
276 < 1e-5);
277 }
278
279 #[test]
280 fn turnbull_wakeman_golden_value() {
281 let price = turnbull_wakeman_price(S, 100.0, R, Q, SIG, T, PutOrCall::Call);
282 assert!((price - 7.409272).abs() < 1e-5, "{price}");
283 }
284
285 #[test]
286 fn geometric_put_call_parity() {
287 for n in [Some(12), Some(252), None] {
289 let c = geometric_asian_price(S, 90.0, R, Q, SIG, T, n, PutOrCall::Call);
290 let p = geometric_asian_price(S, 90.0, R, Q, SIG, T, n, PutOrCall::Put);
291 let c2 = geometric_asian_price(S, 110.0, R, Q, SIG, T, n, PutOrCall::Call);
294 let p2 = geometric_asian_price(S, 110.0, R, Q, SIG, T, n, PutOrCall::Put);
295 let df = (-R * T).exp();
296 assert!((((c - p) - (c2 - p2)) - df * 20.0).abs() < 1e-10);
297 }
298 }
299
300 #[test]
301 fn discrete_averaging_converges_to_continuous() {
302 let continuous = geometric_asian_price(S, 100.0, R, Q, SIG, T, None, PutOrCall::Call);
303 let fine = geometric_asian_price(S, 100.0, R, Q, SIG, T, Some(100_000), PutOrCall::Call);
304 assert!((fine - continuous).abs() < 1e-3);
305 }
306
307 #[test]
308 fn averaging_reduces_option_value_below_vanilla() {
309 use crate::equity::blackscholes::bs_price;
310 let vanilla = bs_price(S, 100.0, R, Q, SIG, T, PutOrCall::Call);
311 let geo = geometric_asian_price(S, 100.0, R, Q, SIG, T, None, PutOrCall::Call);
312 let arith = turnbull_wakeman_price(S, 100.0, R, Q, SIG, T, PutOrCall::Call);
313 assert!(geo < arith, "AM-GM: arithmetic average dominates geometric");
314 assert!(arith < vanilla, "averaging reduces effective volatility");
315 }
316
317 #[test]
318 fn zero_cost_of_carry_branch() {
319 let price = turnbull_wakeman_price(S, 100.0, 0.03, 0.03, SIG, T, PutOrCall::Call);
321 assert!(price > 0.0 && price.is_finite());
322 let near = turnbull_wakeman_price(S, 100.0, 0.03 + 1e-9, 0.03, SIG, T, PutOrCall::Call);
324 assert!((price - near).abs() < 1e-5);
325 }
326}