Skip to main content

rustyqlib/equity/
barrier.rs

1//! Analytic pricing of continuously monitored barrier options
2//! (Reiner-Rubinstein 1991), all eight types: up/down x in/out x call/put,
3//! without rebate.
4//!
5//! Implemented as a pure function of the market inputs so Greeks can be
6//! taken by bumping arguments, and so the formulas can be validated
7//! independently of the option object (in-out parity, vanilla limits,
8//! Monte Carlo agreement).
9
10use crate::core::trade::PutOrCall;
11use crate::core::utils::N;
12use super::blackscholes::bs_price;
13
14/// Which side the barrier sits on relative to the spot at inception.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum BarrierDirection {
17    Up,
18    Down,
19}
20
21/// Knock-in options come alive when the barrier is touched; knock-out
22/// options die.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum KnockType {
25    In,
26    Out,
27}
28
29/// Reiner-Rubinstein price of a European barrier option (no rebate).
30///
31/// If the spot is already at or beyond the barrier the option is treated as
32/// knocked: an `Out` option is worthless, an `In` option is the vanilla.
33#[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 * N(phi * x1) - phi * k * df_r * N(phi * x1 - phi * st);
71    let b = phi * s * df_q * N(phi * x2) - phi * k * df_r * N(phi * x2 - phi * st);
72    let c = phi * s * df_q * hs.powf(2.0 * (mu + 1.0)) * N(eta * y1)
73        - phi * k * df_r * hs.powf(2.0 * mu) * N(eta * y1 - eta * st);
74    let d = phi * s * df_q * hs.powf(2.0 * (mu + 1.0)) * N(eta * y2)
75        - phi * k * df_r * hs.powf(2.0 * mu) * N(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        // in-out parity (no rebate): out = vanilla - in
87        KnockType::Out => bs_price(s, k, r, q, sigma, t, put_or_call) - knock_in,
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    const S: f64 = 100.0;
96    const R: f64 = 0.05;
97    const Q: f64 = 0.02;
98    const SIG: f64 = 0.3;
99    const T: f64 = 1.0;
100
101    #[test]
102    fn matches_independent_oracle_goldens() {
103        use BarrierDirection::*;
104        use KnockType::*;
105        use PutOrCall::*;
106        // values from an independently coded Reiner-Rubinstein implementation
107        let cases = [
108            (Down, In, Call, 90.0, 4.5095197744),
109            (Down, Out, Call, 90.0, 8.5107614943),
110            (Down, In, Put, 90.0, 10.0710164338),
111            (Down, Out, Put, 90.0, 0.0523399543),
112            (Up, In, Call, 120.0, 12.5974705742),
113            (Up, Out, Call, 120.0, 0.4228106946),
114            (Up, In, Put, 120.0, 1.4297711810),
115            (Up, Out, Put, 120.0, 8.6935852071),
116        ];
117        for (direction, knock, pc, h, expected) in cases {
118            let price = barrier_price(S, 100.0, h, R, Q, SIG, T, direction, knock, pc);
119            assert!(
120                (price - expected).abs() < 1e-8,
121                "{direction:?} {knock:?} {pc:?} H={h}: {price} vs {expected}"
122            );
123        }
124    }
125
126    #[test]
127    fn in_plus_out_equals_vanilla() {
128        for pc in [PutOrCall::Call, PutOrCall::Put] {
129            for k in [90.0, 100.0, 110.0] {
130                for (direction, h) in [
131                    (BarrierDirection::Down, 80.0),
132                    (BarrierDirection::Down, 99.0),
133                    (BarrierDirection::Up, 101.0),
134                    (BarrierDirection::Up, 130.0),
135                ] {
136                    let vanilla = bs_price(S, k, R, Q, SIG, T, pc);
137                    let ki = barrier_price(S, k, h, R, Q, SIG, T, direction, KnockType::In, pc);
138                    let ko = barrier_price(S, k, h, R, Q, SIG, T, direction, KnockType::Out, pc);
139                    assert!(
140                        (ki + ko - vanilla).abs() < 1e-10,
141                        "{pc:?} K={k} {direction:?} H={h}: {ki} + {ko} != {vanilla}"
142                    );
143                }
144            }
145        }
146    }
147
148    #[test]
149    fn far_barriers_reduce_to_vanilla_or_zero() {
150        let vanilla_call = bs_price(S, 100.0, R, Q, SIG, T, PutOrCall::Call);
151        // barrier so far away it is never hit: out = vanilla, in = 0
152        let ko = barrier_price(S, 100.0, 1e-4, R, Q, SIG, T, BarrierDirection::Down, KnockType::Out, PutOrCall::Call);
153        let ki = barrier_price(S, 100.0, 1e-4, R, Q, SIG, T, BarrierDirection::Down, KnockType::In, PutOrCall::Call);
154        assert!((ko - vanilla_call).abs() < 1e-9);
155        assert!(ki.abs() < 1e-9);
156        let ko_up = barrier_price(S, 100.0, 1e6, R, Q, SIG, T, BarrierDirection::Up, KnockType::Out, PutOrCall::Call);
157        assert!((ko_up - vanilla_call).abs() < 1e-9);
158    }
159
160    #[test]
161    fn already_knocked_positions() {
162        let vanilla = bs_price(S, 100.0, R, Q, SIG, T, PutOrCall::Call);
163        // spot at the barrier counts as touched
164        let ko = barrier_price(S, 100.0, 100.0, R, Q, SIG, T, BarrierDirection::Down, KnockType::Out, PutOrCall::Call);
165        let ki = barrier_price(S, 100.0, 100.0, R, Q, SIG, T, BarrierDirection::Down, KnockType::In, PutOrCall::Call);
166        assert_eq!(ko, 0.0);
167        assert!((ki - vanilla).abs() < 1e-12);
168    }
169
170    #[test]
171    fn up_out_call_with_strike_above_barrier_is_worthless() {
172        // any payoff requires S_T > K >= H, which forces a knock
173        let price = barrier_price(
174            S, 110.0, 105.0, R, Q, SIG, T,
175            BarrierDirection::Up, KnockType::Out, PutOrCall::Call,
176        );
177        assert!(price.abs() < 1e-12, "{price}");
178    }
179}