use crate::core::trade::PutOrCall;
use crate::core::utils::norm_cdf;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AveragingType {
Arithmetic,
Geometric,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AsianStrikeType {
FixedStrike,
FloatingStrike,
}
fn black(df_r: f64, forward: f64, k: f64, log_var: f64, put_or_call: PutOrCall) -> f64 {
let sqrt_v = log_var.sqrt();
let d1 = ((forward / k).ln() + 0.5 * log_var) / sqrt_v;
let d2 = d1 - sqrt_v;
match put_or_call {
PutOrCall::Call => df_r * (forward * norm_cdf(d1) - k * norm_cdf(d2)),
PutOrCall::Put => df_r * (k * norm_cdf(-d2) - forward * norm_cdf(-d1)),
}
}
#[allow(clippy::too_many_arguments)]
pub fn geometric_asian_price(
s: f64,
k: f64,
r: f64,
q: f64,
sigma: f64,
t: f64,
n: Option<usize>,
put_or_call: PutOrCall,
) -> f64 {
assert!(s > 0.0 && k > 0.0 && sigma > 0.0 && t > 0.0);
let b = r - q;
let (mean_factor, var_factor) = match n {
Some(n) => {
assert!(n > 0);
let nf = n as f64;
((nf + 1.0) / (2.0 * nf), (nf + 1.0) * (2.0 * nf + 1.0) / (6.0 * nf * nf))
}
None => (0.5, 1.0 / 3.0),
};
let mu = (b - 0.5 * sigma * sigma) * t * mean_factor;
let log_var = sigma * sigma * t * var_factor;
let forward = s * (mu + 0.5 * log_var).exp();
black((-r * t).exp(), forward, k, log_var, put_or_call)
}
pub fn geometric_average_strike_price(
s: f64,
r: f64,
q: f64,
sigma: f64,
t: f64,
n: Option<usize>,
put_or_call: PutOrCall,
) -> f64 {
assert!(s > 0.0 && sigma > 0.0 && t > 0.0);
let b = r - q;
let (mean_factor, var_factor, cov_factor) = match n {
Some(n) => {
assert!(n > 0);
let nf = n as f64;
(
(nf + 1.0) / (2.0 * nf),
(nf + 1.0) * (2.0 * nf + 1.0) / (6.0 * nf * nf),
(nf + 1.0) / (2.0 * nf),
)
}
None => (0.5, 1.0 / 3.0, 0.5),
};
let e_terminal = s * (b * t).exp();
let e_average = s
* ((b - 0.5 * sigma * sigma) * t * mean_factor
+ 0.5 * sigma * sigma * t * var_factor)
.exp();
let spread_var = sigma * sigma * t * (1.0 + var_factor - 2.0 * cov_factor);
let df = (-r * t).exp();
if spread_var <= 1e-16 {
return match put_or_call {
PutOrCall::Call => df * (e_terminal - e_average).max(0.0),
PutOrCall::Put => df * (e_average - e_terminal).max(0.0),
};
}
let sv = spread_var.sqrt();
let d1 = ((e_terminal / e_average).ln() + 0.5 * spread_var) / sv;
let d2 = d1 - sv;
match put_or_call {
PutOrCall::Call => df * (e_terminal * norm_cdf(d1) - e_average * norm_cdf(d2)),
PutOrCall::Put => df * (e_average * norm_cdf(-d2) - e_terminal * norm_cdf(-d1)),
}
}
pub fn turnbull_wakeman_average_strike_price(
s: f64,
r: f64,
q: f64,
sigma: f64,
t: f64,
put_or_call: PutOrCall,
) -> f64 {
match put_or_call {
PutOrCall::Call => turnbull_wakeman_price(s, s, q, r, sigma, t, PutOrCall::Put),
PutOrCall::Put => turnbull_wakeman_price(s, s, q, r, sigma, t, PutOrCall::Call),
}
}
pub fn turnbull_wakeman_price(
s: f64,
k: f64,
r: f64,
q: f64,
sigma: f64,
t: f64,
put_or_call: PutOrCall,
) -> f64 {
assert!(s > 0.0 && k > 0.0 && sigma > 0.0 && t > 0.0);
let b = r - q;
let s2 = sigma * sigma;
let (m1, m2) = if b.abs() > 1e-8 {
let m1 = ((b * t).exp() - 1.0) / (b * t);
let m2 = 2.0 * ((2.0 * b + s2) * t).exp() / ((b + s2) * (2.0 * b + s2) * t * t)
+ 2.0 / (b * t * t) * (1.0 / (2.0 * b + s2) - (b * t).exp() / (b + s2));
(m1, m2)
} else {
let m1 = 1.0;
let m2 = (2.0 * (s2 * t).exp() - 2.0 * (1.0 + s2 * t)) / (s2 * s2 * t * t);
(m1, m2)
};
let forward = s * m1;
let log_var = (m2 / (m1 * m1)).ln(); black((-r * t).exp(), forward, k, log_var, put_or_call)
}
#[cfg(test)]
mod tests {
use super::*;
const S: f64 = 100.0;
const R: f64 = 0.05;
const Q: f64 = 0.02;
const SIG: f64 = 0.3;
#[test]
fn average_strike_single_fixing_is_worthless() {
for pc in [PutOrCall::Call, PutOrCall::Put] {
let p = geometric_average_strike_price(S, R, Q, SIG, 1.0, Some(1), pc);
assert!(p.abs() < 1e-12, "{pc:?}: {p}");
}
}
#[test]
fn average_strike_exchange_parity_is_exact() {
for n in [Some(4), Some(12), None] {
let c = geometric_average_strike_price(S, R, Q, SIG, 1.0, n, PutOrCall::Call);
let p = geometric_average_strike_price(S, R, Q, SIG, 1.0, n, PutOrCall::Put);
let b = R - Q;
let (mf, vf) = match n {
Some(n) => {
let nf = n as f64;
((nf + 1.0) / (2.0 * nf), (nf + 1.0) * (2.0 * nf + 1.0) / (6.0 * nf * nf))
}
None => (0.5, 1.0 / 3.0),
};
let e_st = S * b.exp();
let e_g = S * ((b - 0.5 * SIG * SIG) * mf + 0.5 * SIG * SIG * vf).exp();
let parity = (-R * 1.0f64).exp() * (e_st - e_g);
assert!((c - p - parity).abs() < 1e-12, "n = {n:?}");
}
}
#[test]
fn arithmetic_average_strike_preserves_exact_parity() {
let t = 1.0;
let b = R - Q;
let c = turnbull_wakeman_average_strike_price(S, R, Q, SIG, t, PutOrCall::Call);
let p = turnbull_wakeman_average_strike_price(S, R, Q, SIG, t, PutOrCall::Put);
let parity = (-R * t).exp() * (S * (b * t).exp() - S * ((b * t).exp() - 1.0) / (b * t));
assert!((c - p - parity).abs() < 1e-12, "{}", c - p - parity);
let c0 = turnbull_wakeman_average_strike_price(S, 0.03, 0.03, SIG, t, PutOrCall::Call);
let p0 = turnbull_wakeman_average_strike_price(S, 0.03, 0.03, SIG, t, PutOrCall::Put);
assert!(c0 > 0.0 && p0 > 0.0);
assert!((c0 - p0).abs() < 1e-12, "b = 0 parity: E[S_T] = E[A]");
}
#[test]
fn arithmetic_average_strike_orders_against_geometric_by_am_gm() {
let t = 1.0;
let arith_call = turnbull_wakeman_average_strike_price(S, R, Q, SIG, t, PutOrCall::Call);
let geo_call = geometric_average_strike_price(S, R, Q, SIG, t, None, PutOrCall::Call);
assert!(arith_call < geo_call, "call: arith {arith_call} vs geo {geo_call}");
let arith_put = turnbull_wakeman_average_strike_price(S, R, Q, SIG, t, PutOrCall::Put);
let geo_put = geometric_average_strike_price(S, R, Q, SIG, t, None, PutOrCall::Put);
assert!(arith_put > geo_put, "put: arith {arith_put} vs geo {geo_put}");
}
#[test]
fn average_strike_value_grows_with_fixings_toward_the_continuous_limit() {
let price = |n| geometric_average_strike_price(S, R, Q, SIG, 1.0, n, PutOrCall::Call);
assert!(price(Some(2)) > 0.0);
assert!(price(Some(4)) > price(Some(2)));
assert!(price(Some(12)) > price(Some(4)));
let continuous = price(None);
assert!(price(Some(12)) < continuous);
assert!((price(Some(5000)) - continuous).abs() < 2e-3, "limit");
}
const T: f64 = 1.0;
#[test]
fn geometric_golden_values() {
assert!((geometric_asian_price(S, 100.0, R, Q, SIG, T, Some(252), PutOrCall::Call)
- 6.976295)
.abs()
< 1e-5);
assert!((geometric_asian_price(S, 100.0, R, Q, SIG, T, None, PutOrCall::Call) - 6.953600)
.abs()
< 1e-5);
}
#[test]
fn turnbull_wakeman_golden_value() {
let price = turnbull_wakeman_price(S, 100.0, R, Q, SIG, T, PutOrCall::Call);
assert!((price - 7.409272).abs() < 1e-5, "{price}");
}
#[test]
fn geometric_put_call_parity() {
for n in [Some(12), Some(252), None] {
let c = geometric_asian_price(S, 90.0, R, Q, SIG, T, n, PutOrCall::Call);
let p = geometric_asian_price(S, 90.0, R, Q, SIG, T, n, PutOrCall::Put);
let c2 = geometric_asian_price(S, 110.0, R, Q, SIG, T, n, PutOrCall::Call);
let p2 = geometric_asian_price(S, 110.0, R, Q, SIG, T, n, PutOrCall::Put);
let df = (-R * T).exp();
assert!((((c - p) - (c2 - p2)) - df * 20.0).abs() < 1e-10);
}
}
#[test]
fn discrete_averaging_converges_to_continuous() {
let continuous = geometric_asian_price(S, 100.0, R, Q, SIG, T, None, PutOrCall::Call);
let fine = geometric_asian_price(S, 100.0, R, Q, SIG, T, Some(100_000), PutOrCall::Call);
assert!((fine - continuous).abs() < 1e-3);
}
#[test]
fn averaging_reduces_option_value_below_vanilla() {
use crate::equity::blackscholes::bs_price;
let vanilla = bs_price(S, 100.0, R, Q, SIG, T, PutOrCall::Call);
let geo = geometric_asian_price(S, 100.0, R, Q, SIG, T, None, PutOrCall::Call);
let arith = turnbull_wakeman_price(S, 100.0, R, Q, SIG, T, PutOrCall::Call);
assert!(geo < arith, "AM-GM: arithmetic average dominates geometric");
assert!(arith < vanilla, "averaging reduces effective volatility");
}
#[test]
fn zero_cost_of_carry_branch() {
let price = turnbull_wakeman_price(S, 100.0, 0.03, 0.03, SIG, T, PutOrCall::Call);
assert!(price > 0.0 && price.is_finite());
let near = turnbull_wakeman_price(S, 100.0, 0.03 + 1e-9, 0.03, SIG, T, PutOrCall::Call);
assert!((price - near).abs() < 1e-5);
}
}