1use crate::core::trade::PutOrCall;
11use crate::core::utils::norm_cdf;
12use super::blackscholes::bs_price;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum BarrierDirection {
17 Up,
18 Down,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum KnockType {
25 In,
26 Out,
27}
28
29#[allow(clippy::too_many_arguments)]
34pub fn barrier_price(
35 s: f64,
36 k: f64,
37 h: f64,
38 r: f64,
39 q: f64,
40 sigma: f64,
41 t: f64,
42 direction: BarrierDirection,
43 knock: KnockType,
44 put_or_call: PutOrCall,
45) -> f64 {
46 assert!(s > 0.0 && k > 0.0 && h > 0.0 && sigma > 0.0 && t > 0.0);
47 let down = direction == BarrierDirection::Down;
48 let knocked_now = if down { s <= h } else { s >= h };
49 if knocked_now {
50 return match knock {
51 KnockType::Out => 0.0,
52 KnockType::In => bs_price(s, k, r, q, sigma, t, put_or_call),
53 };
54 }
55
56 let call = put_or_call == PutOrCall::Call;
57 let phi: f64 = if call { 1.0 } else { -1.0 };
58 let eta: f64 = if down { 1.0 } else { -1.0 };
59 let st = sigma * t.sqrt();
60 let mu = (r - q - 0.5 * sigma * sigma) / (sigma * sigma);
61 let df_q = (-q * t).exp();
62 let df_r = (-r * t).exp();
63 let hs = h / s;
64
65 let x1 = (s / k).ln() / st + (1.0 + mu) * st;
66 let x2 = (s / h).ln() / st + (1.0 + mu) * st;
67 let y1 = (h * h / (s * k)).ln() / st + (1.0 + mu) * st;
68 let y2 = (h / s).ln() / st + (1.0 + mu) * st;
69
70 let a = phi * s * df_q * norm_cdf(phi * x1) - phi * k * df_r * norm_cdf(phi * x1 - phi * st);
71 let b = phi * s * df_q * norm_cdf(phi * x2) - phi * k * df_r * norm_cdf(phi * x2 - phi * st);
72 let c = phi * s * df_q * hs.powf(2.0 * (mu + 1.0)) * norm_cdf(eta * y1)
73 - phi * k * df_r * hs.powf(2.0 * mu) * norm_cdf(eta * y1 - eta * st);
74 let d = phi * s * df_q * hs.powf(2.0 * (mu + 1.0)) * norm_cdf(eta * y2)
75 - phi * k * df_r * hs.powf(2.0 * mu) * norm_cdf(eta * y2 - eta * st);
76
77 let k_above_barrier = k >= h;
78 let knock_in = match (call, down) {
79 (true, true) => if k_above_barrier { c } else { a - b + d },
80 (true, false) => if k_above_barrier { a } else { b - c + d },
81 (false, true) => if k_above_barrier { b - c + d } else { a },
82 (false, false) => if k_above_barrier { a - b + d } else { c },
83 };
84 match knock {
85 KnockType::In => knock_in,
86 KnockType::Out => bs_price(s, k, r, q, sigma, t, put_or_call) - knock_in,
88 }
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum RebateTiming {
94 AtHit,
96 AtExpiry,
98}
99
100#[allow(clippy::too_many_arguments)]
108pub fn barrier_rebate_value(
109 s: f64,
110 h: f64,
111 rebate: f64,
112 r: f64,
113 q: f64,
114 sigma: f64,
115 t: f64,
116 direction: BarrierDirection,
117 knock: KnockType,
118 timing: RebateTiming,
119) -> f64 {
120 assert!(s > 0.0 && h > 0.0 && sigma > 0.0 && t > 0.0);
121 if rebate == 0.0 {
122 return 0.0;
123 }
124 let down = direction == BarrierDirection::Down;
125 let eta: f64 = if down { 1.0 } else { -1.0 };
126 let knocked_now = if down { s <= h } else { s >= h };
127 let df_r = (-r * t).exp();
128 if knocked_now {
129 return match knock {
130 KnockType::In => 0.0, KnockType::Out => match timing {
132 RebateTiming::AtHit => rebate,
133 RebateTiming::AtExpiry => rebate * df_r,
134 },
135 };
136 }
137 let st = sigma * t.sqrt();
138 let mu = (r - q - 0.5 * sigma * sigma) / (sigma * sigma);
139 let hs = h / s;
140 let x2 = (s / h).ln() / st + (1.0 + mu) * st;
142 let y2 = (h / s).ln() / st + (1.0 + mu) * st;
143 let survival_pv =
144 rebate * df_r * (norm_cdf(eta * (x2 - st)) - hs.powf(2.0 * mu) * norm_cdf(eta * (y2 - st)));
145 match knock {
146 KnockType::In => survival_pv,
147 KnockType::Out => match timing {
148 RebateTiming::AtExpiry => rebate * df_r - survival_pv,
149 RebateTiming::AtHit => {
150 let lambda = (mu * mu + 2.0 * r / (sigma * sigma)).sqrt();
152 let z = (h / s).ln() / st + lambda * st;
153 rebate
154 * (hs.powf(mu + lambda) * norm_cdf(eta * z)
155 + hs.powf(mu - lambda) * norm_cdf(eta * (z - 2.0 * lambda * st)))
156 }
157 },
158 }
159}
160
161#[allow(clippy::too_many_arguments)]
164pub fn barrier_price_with_rebate(
165 s: f64,
166 k: f64,
167 h: f64,
168 rebate: f64,
169 r: f64,
170 q: f64,
171 sigma: f64,
172 t: f64,
173 direction: BarrierDirection,
174 knock: KnockType,
175 timing: RebateTiming,
176 put_or_call: PutOrCall,
177) -> f64 {
178 barrier_price(s, k, h, r, q, sigma, t, direction, knock, put_or_call)
179 + barrier_rebate_value(s, h, rebate, r, q, sigma, t, direction, knock, timing)
180}
181
182#[allow(clippy::too_many_arguments)]
191pub fn double_barrier_price(
192 s: f64,
193 k: f64,
194 l: f64,
195 u: f64,
196 r: f64,
197 q: f64,
198 sigma: f64,
199 t: f64,
200 knock: KnockType,
201 put_or_call: PutOrCall,
202) -> f64 {
203 assert!(s > 0.0 && k > 0.0 && sigma > 0.0 && t > 0.0);
204 assert!(l < u, "lower barrier must be below the upper barrier");
205 let knocked_now = s <= l || s >= u;
206 let vanilla = bs_price(s, k, r, q, sigma, t, put_or_call);
207 if knocked_now {
208 return match knock {
209 KnockType::Out => 0.0,
210 KnockType::In => vanilla,
211 };
212 }
213 let b = r - q;
214 let st = sigma * t.sqrt();
215 let mu = 2.0 * b / (sigma * sigma) + 1.0;
216 let df_q = ((b - r) * t).exp();
217 let df_r = (-r * t).exp();
218 let call = put_or_call == PutOrCall::Call;
219 let f = if call { u } else { l };
221 let mut spot_sum = 0.0;
222 let mut strike_sum = 0.0;
223 for n in -5i32..=5 {
224 let un = u.powi(n);
225 let ln = l.powi(n);
226 let ratio1 = (un / ln).powf(mu);
227 let ratio2 = (l.powi(n + 1) / (un * s)).powf(mu);
228 let d1 = ((s * un * un / (k * ln * ln)).ln() + (b + 0.5 * sigma * sigma) * t) / st;
229 let d2 = ((s * un * un / (f * ln * ln)).ln() + (b + 0.5 * sigma * sigma) * t) / st;
230 let d3 = ((l.powi(2 * n + 2) / (k * s * un * un)).ln()
231 + (b + 0.5 * sigma * sigma) * t)
232 / st;
233 let d4 = ((l.powi(2 * n + 2) / (f * s * un * un)).ln()
234 + (b + 0.5 * sigma * sigma) * t)
235 / st;
236 if call {
237 spot_sum += ratio1 * (norm_cdf(d1) - norm_cdf(d2)) - ratio2 * (norm_cdf(d3) - norm_cdf(d4));
238 strike_sum += (un / ln).powf(mu - 2.0) * (norm_cdf(d1 - st) - norm_cdf(d2 - st))
239 - (l.powi(n + 1) / (un * s)).powf(mu - 2.0) * (norm_cdf(d3 - st) - norm_cdf(d4 - st));
240 } else {
241 spot_sum += ratio1 * (norm_cdf(d2) - norm_cdf(d1)) - ratio2 * (norm_cdf(d4) - norm_cdf(d3));
244 strike_sum += (un / ln).powf(mu - 2.0) * (norm_cdf(d2 - st) - norm_cdf(d1 - st))
245 - (l.powi(n + 1) / (un * s)).powf(mu - 2.0) * (norm_cdf(d4 - st) - norm_cdf(d3 - st));
246 }
247 }
248 let phi = if call { 1.0 } else { -1.0 };
249 let out = phi * (s * df_q * spot_sum - k * df_r * strike_sum);
250 match knock {
251 KnockType::Out => out.max(0.0),
252 KnockType::In => (vanilla - out.max(0.0)).max(0.0),
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 const S: f64 = 100.0;
261 const R: f64 = 0.05;
262 const Q: f64 = 0.02;
263 const SIG: f64 = 0.3;
264 const T: f64 = 1.0;
265
266 #[test]
267 fn rebate_identities_hold_exactly() {
268 use BarrierDirection::*;
269 let rebate = 5.0;
270 for (dir, h) in [(Down, 85.0), (Up, 115.0)] {
271 let ki = barrier_rebate_value(S, h, rebate, R, Q, SIG, T, dir, KnockType::In,
273 RebateTiming::AtExpiry);
274 let ko_exp = barrier_rebate_value(S, h, rebate, R, Q, SIG, T, dir, KnockType::Out,
275 RebateTiming::AtExpiry);
276 assert!((ki + ko_exp - rebate * (-R * T).exp()).abs() < 1e-12, "{dir:?}");
277 let ko_hit = barrier_rebate_value(S, h, rebate, R, Q, SIG, T, dir, KnockType::Out,
279 RebateTiming::AtHit);
280 assert!(ko_hit > ko_exp, "{dir:?}: {ko_hit} vs {ko_exp}");
281 let hit0 = barrier_rebate_value(S, h, rebate, 0.0, Q, SIG, T, dir, KnockType::Out,
283 RebateTiming::AtHit);
284 let exp0 = barrier_rebate_value(S, h, rebate, 0.0, Q, SIG, T, dir, KnockType::Out,
285 RebateTiming::AtExpiry);
286 assert!((hit0 - exp0).abs() < 1e-10, "{dir:?}: {hit0} vs {exp0}");
287 }
288 let far = barrier_rebate_value(S, 1e4, rebate, R, Q, SIG, T, Up, KnockType::Out,
290 RebateTiming::AtHit);
291 assert!(far < 1e-8, "{far}");
292 let sure = barrier_rebate_value(S, 1e4, rebate, R, Q, SIG, T, Up, KnockType::In,
293 RebateTiming::AtExpiry);
294 assert!((sure - rebate * (-R * T).exp()).abs() < 1e-8);
295 let now = barrier_rebate_value(100.0, 100.0, rebate, R, Q, SIG, T, Up, KnockType::Out,
297 RebateTiming::AtHit);
298 assert!((now - rebate).abs() < 1e-12);
299 }
300
301 #[test]
302 fn rebate_at_hit_matches_a_first_touch_monte_carlo() {
303 use crate::core::montecarlo::path_rng;
304 use rand::Rng;
305 let (h, rebate) = (115.0, 10.0);
306 let analytic = barrier_rebate_value(S, h, rebate, R, Q, SIG, T,
307 BarrierDirection::Up, KnockType::Out, RebateTiming::AtHit);
308 let (paths, steps) = (60_000, 2000);
311 let dt = T / steps as f64;
312 let drift = (R - Q - 0.5 * SIG * SIG) * dt;
313 let vol = SIG * dt.sqrt();
314 let mut sum = 0.0;
315 for i in 0..paths {
316 let mut rng = path_rng(97, i as u64);
317 let mut spot = S;
318 for step in 1..=steps {
319 let z: f64 = rng.sample(rand_distr::StandardNormal);
320 spot *= (drift + vol * z).exp();
321 if spot >= h {
322 sum += rebate * (-R * step as f64 * dt).exp();
323 break;
324 }
325 }
326 }
327 let mc = sum / paths as f64;
328 assert!(mc < analytic + 0.02, "discrete monitoring should undercount");
329 assert!((mc - analytic).abs() < 0.20, "mc {mc} vs analytic {analytic}");
330 }
331
332 #[test]
333 fn double_barrier_limits_reproduce_single_barriers() {
334 for pc in [PutOrCall::Call, PutOrCall::Put] {
335 for k in [90.0, 100.0, 110.0] {
336 let dko = double_barrier_price(S, k, 10.0, 120.0, R, Q, SIG, T,
338 KnockType::Out, pc);
339 let uo = barrier_price(S, k, 120.0, R, Q, SIG, T,
340 BarrierDirection::Up, KnockType::Out, pc);
341 assert!((dko - uo).abs() < 1e-6, "{pc:?} K={k}: {dko} vs UO {uo}");
342 let dko2 = double_barrier_price(S, k, 80.0, 1000.0, R, Q, SIG, T,
344 KnockType::Out, pc);
345 let down_out = barrier_price(S, k, 80.0, R, Q, SIG, T,
346 BarrierDirection::Down, KnockType::Out, pc);
347 assert!((dko2 - down_out).abs() < 1e-6, "{pc:?} K={k}: {dko2} vs DO {down_out}");
348 let wide = double_barrier_price(S, k, 10.0, 1000.0, R, Q, SIG, T,
350 KnockType::Out, pc);
351 let vanilla = crate::equity::blackscholes::bs_price(S, k, R, Q, SIG, T, pc);
352 assert!((wide - vanilla).abs() < 1e-6, "{pc:?} K={k}");
353 }
354 }
355 }
356
357 #[test]
358 fn double_barrier_parity_and_bounds() {
359 for pc in [PutOrCall::Call, PutOrCall::Put] {
360 let (l, u, k) = (85.0, 120.0, 100.0);
361 let out = double_barrier_price(S, k, l, u, R, Q, SIG, T, KnockType::Out, pc);
362 let inn = double_barrier_price(S, k, l, u, R, Q, SIG, T, KnockType::In, pc);
363 let vanilla = crate::equity::blackscholes::bs_price(S, k, R, Q, SIG, T, pc);
364 assert!((out + inn - vanilla).abs() < 1e-10, "{pc:?} parity");
365 let uo = barrier_price(S, k, u, R, Q, SIG, T,
367 BarrierDirection::Up, KnockType::Out, pc);
368 let down_out = barrier_price(S, k, l, R, Q, SIG, T,
369 BarrierDirection::Down, KnockType::Out, pc);
370 assert!(out <= uo + 1e-10 && out <= down_out + 1e-10, "{pc:?} bounds");
371 assert!(out > 0.0);
372 }
373 }
374
375 #[test]
376 fn double_barrier_matches_a_dense_monte_carlo() {
377 use crate::core::montecarlo::path_rng;
378 use rand::Rng;
379 let (l, u, k) = (85.0, 120.0, 100.0);
380 for pc in [PutOrCall::Call, PutOrCall::Put] {
381 let analytic = double_barrier_price(S, k, l, u, R, Q, SIG, T, KnockType::Out, pc);
382 let (paths, steps) = (60_000, 2000);
383 let dt = T / steps as f64;
384 let drift = (R - Q - 0.5 * SIG * SIG) * dt;
385 let vol = SIG * dt.sqrt();
386 let mut sum = 0.0;
387 for i in 0..paths {
388 let mut rng = path_rng(13, i as u64);
389 let mut spot = S;
390 let mut alive = true;
391 for _ in 0..steps {
392 let z: f64 = rng.sample(rand_distr::StandardNormal);
393 spot *= (drift + vol * z).exp();
394 if spot <= l || spot >= u {
395 alive = false;
396 break;
397 }
398 }
399 if alive {
400 sum += match pc {
401 PutOrCall::Call => (spot - k).max(0.0),
402 PutOrCall::Put => (k - spot).max(0.0),
403 };
404 }
405 }
406 let mc = (-R * T).exp() * sum / paths as f64;
407 assert!(mc > analytic - 0.02, "{pc:?}: {mc} vs {analytic}");
409 assert!((mc - analytic).abs() < 0.25, "{pc:?}: mc {mc} vs analytic {analytic}");
410 }
411 }
412
413 #[test]
414 fn matches_independent_oracle_goldens() {
415 use BarrierDirection::*;
416 use KnockType::*;
417 use PutOrCall::*;
418 let cases = [
420 (Down, In, Call, 90.0, 4.5095197744),
421 (Down, Out, Call, 90.0, 8.5107614943),
422 (Down, In, Put, 90.0, 10.0710164338),
423 (Down, Out, Put, 90.0, 0.0523399543),
424 (Up, In, Call, 120.0, 12.5974705742),
425 (Up, Out, Call, 120.0, 0.4228106946),
426 (Up, In, Put, 120.0, 1.4297711810),
427 (Up, Out, Put, 120.0, 8.6935852071),
428 ];
429 for (direction, knock, pc, h, expected) in cases {
430 let price = barrier_price(S, 100.0, h, R, Q, SIG, T, direction, knock, pc);
431 assert!(
432 (price - expected).abs() < 1e-8,
433 "{direction:?} {knock:?} {pc:?} H={h}: {price} vs {expected}"
434 );
435 }
436 }
437
438 #[test]
439 fn in_plus_out_equals_vanilla() {
440 for pc in [PutOrCall::Call, PutOrCall::Put] {
441 for k in [90.0, 100.0, 110.0] {
442 for (direction, h) in [
443 (BarrierDirection::Down, 80.0),
444 (BarrierDirection::Down, 99.0),
445 (BarrierDirection::Up, 101.0),
446 (BarrierDirection::Up, 130.0),
447 ] {
448 let vanilla = bs_price(S, k, R, Q, SIG, T, pc);
449 let ki = barrier_price(S, k, h, R, Q, SIG, T, direction, KnockType::In, pc);
450 let ko = barrier_price(S, k, h, R, Q, SIG, T, direction, KnockType::Out, pc);
451 assert!(
452 (ki + ko - vanilla).abs() < 1e-10,
453 "{pc:?} K={k} {direction:?} H={h}: {ki} + {ko} != {vanilla}"
454 );
455 }
456 }
457 }
458 }
459
460 #[test]
461 fn far_barriers_reduce_to_vanilla_or_zero() {
462 let vanilla_call = bs_price(S, 100.0, R, Q, SIG, T, PutOrCall::Call);
463 let ko = barrier_price(S, 100.0, 1e-4, R, Q, SIG, T, BarrierDirection::Down, KnockType::Out, PutOrCall::Call);
465 let ki = barrier_price(S, 100.0, 1e-4, R, Q, SIG, T, BarrierDirection::Down, KnockType::In, PutOrCall::Call);
466 assert!((ko - vanilla_call).abs() < 1e-9);
467 assert!(ki.abs() < 1e-9);
468 let ko_up = barrier_price(S, 100.0, 1e6, R, Q, SIG, T, BarrierDirection::Up, KnockType::Out, PutOrCall::Call);
469 assert!((ko_up - vanilla_call).abs() < 1e-9);
470 }
471
472 #[test]
473 fn already_knocked_positions() {
474 let vanilla = bs_price(S, 100.0, R, Q, SIG, T, PutOrCall::Call);
475 let ko = barrier_price(S, 100.0, 100.0, R, Q, SIG, T, BarrierDirection::Down, KnockType::Out, PutOrCall::Call);
477 let ki = barrier_price(S, 100.0, 100.0, R, Q, SIG, T, BarrierDirection::Down, KnockType::In, PutOrCall::Call);
478 assert_eq!(ko, 0.0);
479 assert!((ki - vanilla).abs() < 1e-12);
480 }
481
482 #[test]
483 fn up_out_call_with_strike_above_barrier_is_worthless() {
484 let price = barrier_price(
486 S, 110.0, 105.0, R, Q, SIG, T,
487 BarrierDirection::Up, KnockType::Out, PutOrCall::Call,
488 );
489 assert!(price.abs() < 1e-12, "{price}");
490 }
491}