1use crate::core::trade::PutOrCall;
22use crate::core::utils::norm_cdf;
23
24pub fn floating_strike_lookback_price(
28 s: f64,
29 extremum: f64,
30 r: f64,
31 q: f64,
32 sigma: f64,
33 t: f64,
34 put_or_call: PutOrCall,
35) -> f64 {
36 assert!(s > 0.0 && extremum > 0.0 && sigma > 0.0 && t > 0.0);
37 let mut b = r - q;
38 if b.abs() < 1e-7 {
39 b = if b >= 0.0 { 1e-7 } else { -1e-7 };
40 }
41 let sq = sigma * t.sqrt();
42 let two_b_over_v2 = 2.0 * b / (sigma * sigma);
43 let carry_df = ((b - r) * t).exp();
44 let df = (-r * t).exp();
45 match put_or_call {
46 PutOrCall::Call => {
47 let m = extremum; assert!(m <= s * (1.0 + 1e-12), "call extremum must be the running minimum");
49 let a1 = ((s / m).ln() + (b + 0.5 * sigma * sigma) * t) / sq;
50 let a2 = a1 - sq;
51 s * carry_df * norm_cdf(a1) - m * df * norm_cdf(a2)
52 + s * df * (1.0 / two_b_over_v2)
53 * ((s / m).powf(-two_b_over_v2) * norm_cdf(-a1 + (2.0 * b / sigma) * t.sqrt())
54 - (b * t).exp() * norm_cdf(-a1))
55 }
56 PutOrCall::Put => {
57 let m = extremum; assert!(m >= s * (1.0 - 1e-12), "put extremum must be the running maximum");
59 let b1 = ((s / m).ln() + (b + 0.5 * sigma * sigma) * t) / sq;
60 let b2 = b1 - sq;
61 m * df * norm_cdf(-b2) - s * carry_df * norm_cdf(-b1)
62 + s * df * (1.0 / two_b_over_v2)
63 * (-(s / m).powf(-two_b_over_v2) * norm_cdf(b1 - (2.0 * b / sigma) * t.sqrt())
64 + (b * t).exp() * norm_cdf(b1))
65 }
66 }
67}
68
69pub fn fixed_strike_lookback_price(
73 s: f64,
74 k: f64,
75 extremum: f64,
76 r: f64,
77 q: f64,
78 sigma: f64,
79 t: f64,
80 put_or_call: PutOrCall,
81) -> f64 {
82 assert!(s > 0.0 && k > 0.0 && extremum > 0.0 && sigma > 0.0 && t > 0.0);
83 let mut b = r - q;
84 if b.abs() < 1e-7 {
85 b = if b >= 0.0 { 1e-7 } else { -1e-7 };
86 }
87 let sq = sigma * t.sqrt();
88 let two_b_over_v2 = 2.0 * b / (sigma * sigma);
89 let carry_df = ((b - r) * t).exp();
90 let df = (-r * t).exp();
91 let two_b_sq = (2.0 * b / sigma) * t.sqrt();
92 match put_or_call {
93 PutOrCall::Call => {
94 let m = extremum; assert!(m >= s * (1.0 - 1e-12), "call extremum must be the running maximum");
96 if k > m {
97 let d1 = ((s / k).ln() + (b + 0.5 * sigma * sigma) * t) / sq;
98 let d2 = d1 - sq;
99 s * carry_df * norm_cdf(d1) - k * df * norm_cdf(d2)
100 + s * df * (1.0 / two_b_over_v2)
101 * (-(s / k).powf(-two_b_over_v2) * norm_cdf(d1 - two_b_sq)
102 + (b * t).exp() * norm_cdf(d1))
103 } else {
104 let e1 = ((s / m).ln() + (b + 0.5 * sigma * sigma) * t) / sq;
105 let e2 = e1 - sq;
106 df * (m - k) + s * carry_df * norm_cdf(e1) - m * df * norm_cdf(e2)
107 + s * df * (1.0 / two_b_over_v2)
108 * (-(s / m).powf(-two_b_over_v2) * norm_cdf(e1 - two_b_sq)
109 + (b * t).exp() * norm_cdf(e1))
110 }
111 }
112 PutOrCall::Put => {
113 let m = extremum; assert!(m <= s * (1.0 + 1e-12), "put extremum must be the running minimum");
115 if k < m {
116 let d1 = ((s / k).ln() + (b + 0.5 * sigma * sigma) * t) / sq;
117 let d2 = d1 - sq;
118 k * df * norm_cdf(-d2) - s * carry_df * norm_cdf(-d1)
119 + s * df * (1.0 / two_b_over_v2)
120 * ((s / k).powf(-two_b_over_v2) * norm_cdf(-d1 + two_b_sq)
121 - (b * t).exp() * norm_cdf(-d1))
122 } else {
123 let f1 = ((s / m).ln() + (b + 0.5 * sigma * sigma) * t) / sq;
124 let f2 = f1 - sq;
125 df * (k - m) - s * carry_df * norm_cdf(-f1) + m * df * norm_cdf(-f2)
126 + s * df * (1.0 / two_b_over_v2)
127 * ((s / m).powf(-two_b_over_v2) * norm_cdf(-f1 + two_b_sq)
128 - (b * t).exp() * norm_cdf(-f1))
129 }
130 }
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use crate::equity::blackscholes::bs_price;
138
139 const S: f64 = 100.0;
140 const R: f64 = 0.05;
141 const Q: f64 = 0.02;
142 const SIG: f64 = 0.3;
143 const T: f64 = 1.0;
144
145 #[test]
146 fn floating_lookbacks_dominate_atm_vanillas() {
147 let call = floating_strike_lookback_price(S, S, R, Q, SIG, T, PutOrCall::Call);
150 let put = floating_strike_lookback_price(S, S, R, Q, SIG, T, PutOrCall::Put);
151 assert!(call > bs_price(S, S, R, Q, SIG, T, PutOrCall::Call), "{call}");
152 assert!(put > bs_price(S, S, R, Q, SIG, T, PutOrCall::Put), "{put}");
153 assert!(call > 10.0 && put > 10.0);
155 }
156
157 #[test]
158 fn fixed_strike_branches_are_continuous_at_the_extremum() {
159 let eps = 1e-9;
161 let call_above =
162 fixed_strike_lookback_price(S, S + eps, S, R, Q, SIG, T, PutOrCall::Call);
163 let call_below =
164 fixed_strike_lookback_price(S, S - eps, S, R, Q, SIG, T, PutOrCall::Call);
165 assert!((call_above - call_below).abs() < 1e-6, "{call_above} vs {call_below}");
166 let put_above =
167 fixed_strike_lookback_price(S, S + eps, S, R, Q, SIG, T, PutOrCall::Put);
168 let put_below =
169 fixed_strike_lookback_price(S, S - eps, S, R, Q, SIG, T, PutOrCall::Put);
170 assert!((put_above - put_below).abs() < 1e-6, "{put_above} vs {put_below}");
171 }
172
173 #[test]
174 fn fixed_and_floating_forms_are_linked_by_exact_identities() {
175 let k_small = 1e-7;
179 let fixed_call =
180 fixed_strike_lookback_price(S, k_small, S, R, Q, SIG, T, PutOrCall::Call);
181 let floating_put = floating_strike_lookback_price(S, S, R, Q, SIG, T, PutOrCall::Put);
182 let expect = S * (-Q * T).exp() - k_small * (-R * T).exp();
183 assert!(
184 (fixed_call - floating_put - expect).abs() < 1e-4,
185 "{} vs {}",
186 fixed_call - floating_put,
187 expect
188 );
189 let k_big = 100_000.0;
191 let fixed_put =
192 fixed_strike_lookback_price(S, k_big, S, R, Q, SIG, T, PutOrCall::Put);
193 let floating_call = floating_strike_lookback_price(S, S, R, Q, SIG, T, PutOrCall::Call);
194 let expect2 = k_big * (-R * T).exp() - S * (-Q * T).exp();
195 assert!(
196 (fixed_put - floating_call - expect2).abs() < 1e-4,
197 "{} vs {}",
198 fixed_put - floating_call,
199 expect2
200 );
201 }
202
203 #[test]
204 fn zero_carry_nudge_is_smooth() {
205 let at = floating_strike_lookback_price(S, S, 0.03, 0.03, SIG, T, PutOrCall::Call);
207 let up = floating_strike_lookback_price(S, S, 0.03 + 1e-5, 0.03, SIG, T, PutOrCall::Call);
208 let dn = floating_strike_lookback_price(S, S, 0.03 - 1e-5, 0.03, SIG, T, PutOrCall::Call);
209 assert!(dn < at && at < up, "{dn} {at} {up}");
210 assert!((up - dn).abs() < 0.02, "smooth through b = 0");
211 }
212
213 #[test]
214 fn seasoned_extrema_move_prices_the_right_way() {
215 let fresh = floating_strike_lookback_price(S, S, R, Q, SIG, T, PutOrCall::Call);
218 let seasoned = floating_strike_lookback_price(S, 80.0, R, Q, SIG, T, PutOrCall::Call);
219 assert!(seasoned > fresh + 1.0, "{seasoned} vs {fresh}");
220 assert!(seasoned > S * (-Q * T).exp() - 80.0 * (-R * T).exp());
222 let fresh_fix = fixed_strike_lookback_price(S, 110.0, S, R, Q, SIG, T, PutOrCall::Call);
225 let seasoned_fix =
226 fixed_strike_lookback_price(S, 110.0, 130.0, R, Q, SIG, T, PutOrCall::Call);
227 assert!(seasoned_fix > fresh_fix + 1.0, "{seasoned_fix} vs {fresh_fix}");
228 assert!(seasoned_fix >= (130.0_f64 - 110.0) * (-R * T).exp());
229 }
230}