use crate::core::trade::PutOrCall;
use crate::core::utils::norm_cdf;
pub fn floating_strike_lookback_price(
s: f64,
extremum: f64,
r: f64,
q: f64,
sigma: f64,
t: f64,
put_or_call: PutOrCall,
) -> f64 {
assert!(s > 0.0 && extremum > 0.0 && sigma > 0.0 && t > 0.0);
let mut b = r - q;
if b.abs() < 1e-7 {
b = if b >= 0.0 { 1e-7 } else { -1e-7 };
}
let sq = sigma * t.sqrt();
let two_b_over_v2 = 2.0 * b / (sigma * sigma);
let carry_df = ((b - r) * t).exp();
let df = (-r * t).exp();
match put_or_call {
PutOrCall::Call => {
let m = extremum; assert!(m <= s * (1.0 + 1e-12), "call extremum must be the running minimum");
let a1 = ((s / m).ln() + (b + 0.5 * sigma * sigma) * t) / sq;
let a2 = a1 - sq;
s * carry_df * norm_cdf(a1) - m * df * norm_cdf(a2)
+ s * df * (1.0 / two_b_over_v2)
* ((s / m).powf(-two_b_over_v2) * norm_cdf(-a1 + (2.0 * b / sigma) * t.sqrt())
- (b * t).exp() * norm_cdf(-a1))
}
PutOrCall::Put => {
let m = extremum; assert!(m >= s * (1.0 - 1e-12), "put extremum must be the running maximum");
let b1 = ((s / m).ln() + (b + 0.5 * sigma * sigma) * t) / sq;
let b2 = b1 - sq;
m * df * norm_cdf(-b2) - s * carry_df * norm_cdf(-b1)
+ s * df * (1.0 / two_b_over_v2)
* (-(s / m).powf(-two_b_over_v2) * norm_cdf(b1 - (2.0 * b / sigma) * t.sqrt())
+ (b * t).exp() * norm_cdf(b1))
}
}
}
pub fn fixed_strike_lookback_price(
s: f64,
k: f64,
extremum: f64,
r: f64,
q: f64,
sigma: f64,
t: f64,
put_or_call: PutOrCall,
) -> f64 {
assert!(s > 0.0 && k > 0.0 && extremum > 0.0 && sigma > 0.0 && t > 0.0);
let mut b = r - q;
if b.abs() < 1e-7 {
b = if b >= 0.0 { 1e-7 } else { -1e-7 };
}
let sq = sigma * t.sqrt();
let two_b_over_v2 = 2.0 * b / (sigma * sigma);
let carry_df = ((b - r) * t).exp();
let df = (-r * t).exp();
let two_b_sq = (2.0 * b / sigma) * t.sqrt();
match put_or_call {
PutOrCall::Call => {
let m = extremum; assert!(m >= s * (1.0 - 1e-12), "call extremum must be the running maximum");
if k > m {
let d1 = ((s / k).ln() + (b + 0.5 * sigma * sigma) * t) / sq;
let d2 = d1 - sq;
s * carry_df * norm_cdf(d1) - k * df * norm_cdf(d2)
+ s * df * (1.0 / two_b_over_v2)
* (-(s / k).powf(-two_b_over_v2) * norm_cdf(d1 - two_b_sq)
+ (b * t).exp() * norm_cdf(d1))
} else {
let e1 = ((s / m).ln() + (b + 0.5 * sigma * sigma) * t) / sq;
let e2 = e1 - sq;
df * (m - k) + s * carry_df * norm_cdf(e1) - m * df * norm_cdf(e2)
+ s * df * (1.0 / two_b_over_v2)
* (-(s / m).powf(-two_b_over_v2) * norm_cdf(e1 - two_b_sq)
+ (b * t).exp() * norm_cdf(e1))
}
}
PutOrCall::Put => {
let m = extremum; assert!(m <= s * (1.0 + 1e-12), "put extremum must be the running minimum");
if k < m {
let d1 = ((s / k).ln() + (b + 0.5 * sigma * sigma) * t) / sq;
let d2 = d1 - sq;
k * df * norm_cdf(-d2) - s * carry_df * norm_cdf(-d1)
+ s * df * (1.0 / two_b_over_v2)
* ((s / k).powf(-two_b_over_v2) * norm_cdf(-d1 + two_b_sq)
- (b * t).exp() * norm_cdf(-d1))
} else {
let f1 = ((s / m).ln() + (b + 0.5 * sigma * sigma) * t) / sq;
let f2 = f1 - sq;
df * (k - m) - s * carry_df * norm_cdf(-f1) + m * df * norm_cdf(-f2)
+ s * df * (1.0 / two_b_over_v2)
* ((s / m).powf(-two_b_over_v2) * norm_cdf(-f1 + two_b_sq)
- (b * t).exp() * norm_cdf(-f1))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::equity::blackscholes::bs_price;
const S: f64 = 100.0;
const R: f64 = 0.05;
const Q: f64 = 0.02;
const SIG: f64 = 0.3;
const T: f64 = 1.0;
#[test]
fn floating_lookbacks_dominate_atm_vanillas() {
let call = floating_strike_lookback_price(S, S, R, Q, SIG, T, PutOrCall::Call);
let put = floating_strike_lookback_price(S, S, R, Q, SIG, T, PutOrCall::Put);
assert!(call > bs_price(S, S, R, Q, SIG, T, PutOrCall::Call), "{call}");
assert!(put > bs_price(S, S, R, Q, SIG, T, PutOrCall::Put), "{put}");
assert!(call > 10.0 && put > 10.0);
}
#[test]
fn fixed_strike_branches_are_continuous_at_the_extremum() {
let eps = 1e-9;
let call_above =
fixed_strike_lookback_price(S, S + eps, S, R, Q, SIG, T, PutOrCall::Call);
let call_below =
fixed_strike_lookback_price(S, S - eps, S, R, Q, SIG, T, PutOrCall::Call);
assert!((call_above - call_below).abs() < 1e-6, "{call_above} vs {call_below}");
let put_above =
fixed_strike_lookback_price(S, S + eps, S, R, Q, SIG, T, PutOrCall::Put);
let put_below =
fixed_strike_lookback_price(S, S - eps, S, R, Q, SIG, T, PutOrCall::Put);
assert!((put_above - put_below).abs() < 1e-6, "{put_above} vs {put_below}");
}
#[test]
fn fixed_and_floating_forms_are_linked_by_exact_identities() {
let k_small = 1e-7;
let fixed_call =
fixed_strike_lookback_price(S, k_small, S, R, Q, SIG, T, PutOrCall::Call);
let floating_put = floating_strike_lookback_price(S, S, R, Q, SIG, T, PutOrCall::Put);
let expect = S * (-Q * T).exp() - k_small * (-R * T).exp();
assert!(
(fixed_call - floating_put - expect).abs() < 1e-4,
"{} vs {}",
fixed_call - floating_put,
expect
);
let k_big = 100_000.0;
let fixed_put =
fixed_strike_lookback_price(S, k_big, S, R, Q, SIG, T, PutOrCall::Put);
let floating_call = floating_strike_lookback_price(S, S, R, Q, SIG, T, PutOrCall::Call);
let expect2 = k_big * (-R * T).exp() - S * (-Q * T).exp();
assert!(
(fixed_put - floating_call - expect2).abs() < 1e-4,
"{} vs {}",
fixed_put - floating_call,
expect2
);
}
#[test]
fn zero_carry_nudge_is_smooth() {
let at = floating_strike_lookback_price(S, S, 0.03, 0.03, SIG, T, PutOrCall::Call);
let up = floating_strike_lookback_price(S, S, 0.03 + 1e-5, 0.03, SIG, T, PutOrCall::Call);
let dn = floating_strike_lookback_price(S, S, 0.03 - 1e-5, 0.03, SIG, T, PutOrCall::Call);
assert!(dn < at && at < up, "{dn} {at} {up}");
assert!((up - dn).abs() < 0.02, "smooth through b = 0");
}
#[test]
fn seasoned_extrema_move_prices_the_right_way() {
let fresh = floating_strike_lookback_price(S, S, R, Q, SIG, T, PutOrCall::Call);
let seasoned = floating_strike_lookback_price(S, 80.0, R, Q, SIG, T, PutOrCall::Call);
assert!(seasoned > fresh + 1.0, "{seasoned} vs {fresh}");
assert!(seasoned > S * (-Q * T).exp() - 80.0 * (-R * T).exp());
let fresh_fix = fixed_strike_lookback_price(S, 110.0, S, R, Q, SIG, T, PutOrCall::Call);
let seasoned_fix =
fixed_strike_lookback_price(S, 110.0, 130.0, R, Q, SIG, T, PutOrCall::Call);
assert!(seasoned_fix > fresh_fix + 1.0, "{seasoned_fix} vs {fresh_fix}");
assert!(seasoned_fix >= (130.0_f64 - 110.0) * (-R * T).exp());
}
}