1use crate::core::solvers::Solver1d;
25use crate::core::trade::PutOrCall;
26use crate::core::utils::{norm_pdf, ContractStyle, norm_cdf};
27use crate::equity::blackscholes::bs_price;
28use crate::equity::vanilla_option::EquityOption;
29
30const CRIT_TOL: f64 = 1e-6;
33const CRIT_MAX_ITER: usize = 100;
34
35pub fn price(s: f64, k: f64, r: f64, q: f64, sigma: f64, t: f64, put_or_call: PutOrCall) -> f64 {
40 let intrinsic = match put_or_call {
41 PutOrCall::Call => (s - k).max(0.0),
42 PutOrCall::Put => (k - s).max(0.0),
43 };
44 if t <= 0.0 || sigma <= 0.0 {
45 return intrinsic;
46 }
47 let b = r - q;
48 let euro = bs_price(s, k, r, q, sigma, t, put_or_call);
49
50 match put_or_call {
51 PutOrCall::Call => {
52 if b >= r {
54 return euro;
55 }
56 let s_star = critical_call(k, r, b, sigma, t);
57 if s >= s_star {
58 return intrinsic;
59 }
60 let q2 = quadratic_root(r, b, sigma, t, true);
61 let d1 = d1_of(s_star, k, b, sigma, t);
62 let a2 = (s_star / q2) * (1.0 - ((b - r) * t).exp() * norm_cdf(d1));
63 euro + a2 * (s / s_star).powf(q2)
64 }
65 PutOrCall::Put => {
66 let s_star = critical_put(k, r, b, sigma, t);
67 if s <= s_star {
68 return intrinsic;
69 }
70 let q1 = quadratic_root(r, b, sigma, t, false);
71 let d1 = d1_of(s_star, k, b, sigma, t);
72 let a1 = -(s_star / q1) * (1.0 - ((b - r) * t).exp() * norm_cdf(-d1));
73 euro + a1 * (s / s_star).powf(q1)
74 }
75 }
76}
77
78pub fn early_exercise_premium(
81 s: f64,
82 k: f64,
83 r: f64,
84 q: f64,
85 sigma: f64,
86 t: f64,
87 put_or_call: PutOrCall,
88) -> f64 {
89 price(s, k, r, q, sigma, t, put_or_call) - bs_price(s, k, r, q, sigma, t, put_or_call)
90}
91
92fn d1_of(s: f64, k: f64, b: f64, sigma: f64, t: f64) -> f64 {
93 ((s / k).ln() + (b + 0.5 * sigma * sigma) * t) / (sigma * t.sqrt())
94}
95
96fn quadratic_root(r: f64, b: f64, sigma: f64, t: f64, call: bool) -> f64 {
99 let n = 2.0 * b / (sigma * sigma);
100 let kf = 2.0 * r / (sigma * sigma * (1.0 - (-r * t).exp()));
101 let disc = ((n - 1.0).powi(2) + 4.0 * kf).sqrt();
102 if call {
103 (-(n - 1.0) + disc) / 2.0
104 } else {
105 (-(n - 1.0) - disc) / 2.0
106 }
107}
108
109fn critical_call(k: f64, r: f64, b: f64, sigma: f64, t: f64) -> f64 {
114 let n = 2.0 * b / (sigma * sigma);
115 let m = 2.0 * r / (sigma * sigma);
116 let q2u = (-(n - 1.0) + ((n - 1.0).powi(2) + 4.0 * m).sqrt()) / 2.0;
117 let su = k / (1.0 - 1.0 / q2u); let h2 = -(b * t + 2.0 * sigma * t.sqrt()) * k / (su - k);
119 let seed = k + (su - k) * (1.0 - h2.exp());
120
121 let q2 = quadratic_root(r, b, sigma, t, true);
122 let sqt = sigma * t.sqrt();
123 let rhs = |si: f64| {
124 let d1 = d1_of(si, k, b, sigma, t);
125 bs_price(si, k, r, r - b, sigma, t, PutOrCall::Call)
126 + (1.0 - ((b - r) * t).exp() * norm_cdf(d1)) * si / q2
127 };
128 let bi = |si: f64| {
130 let d1 = d1_of(si, k, b, sigma, t);
131 ((b - r) * t).exp() * norm_cdf(d1) * (1.0 - 1.0 / q2)
132 + (1.0 - ((b - r) * t).exp() * norm_pdf(d1) / sqt) / q2
133 };
134 Solver1d::new(CRIT_TOL * k, CRIT_MAX_ITER)
135 .newton_raphson(|si| (si - k) - rhs(si), |si| 1.0 - bi(si), seed)
136 .x
137}
138
139fn critical_put(k: f64, r: f64, b: f64, sigma: f64, t: f64) -> f64 {
142 let n = 2.0 * b / (sigma * sigma);
143 let m = 2.0 * r / (sigma * sigma);
144 let q1u = (-(n - 1.0) - ((n - 1.0).powi(2) + 4.0 * m).sqrt()) / 2.0;
145 let su = k / (1.0 - 1.0 / q1u);
146 let h1 = (b * t - 2.0 * sigma * t.sqrt()) * k / (k - su);
147 let seed = su + (k - su) * h1.exp();
148
149 let q1 = quadratic_root(r, b, sigma, t, false);
150 let sqt = sigma * t.sqrt();
151 let rhs = |si: f64| {
152 let d1 = d1_of(si, k, b, sigma, t);
153 bs_price(si, k, r, r - b, sigma, t, PutOrCall::Put)
154 - (1.0 - ((b - r) * t).exp() * norm_cdf(-d1)) * si / q1
155 };
156 let bi = |si: f64| {
157 let d1 = d1_of(si, k, b, sigma, t);
158 -((b - r) * t).exp() * norm_cdf(-d1) * (1.0 - 1.0 / q1)
159 - (1.0 + ((b - r) * t).exp() * norm_pdf(-d1) / sqt) / q1
160 };
161 Solver1d::new(CRIT_TOL * k, CRIT_MAX_ITER)
162 .newton_raphson(|si| (k - si) - rhs(si), |si| -1.0 - bi(si), seed)
163 .x
164}
165
166fn reprice(option: &EquityOption, d_spot: f64, d_vol: f64, d_rate: f64, d_maturity: f64) -> f64 {
172 let s = option.effective_spot() + d_spot;
173 let k = option.base.strike_price;
174 let r = option.risk_free_rate() + d_rate;
175 let q = option.carry_yield();
176 let sigma = option.volatility() + d_vol;
177 let t = (option.time_to_maturity() + d_maturity).max(1e-8);
178 let pc = *option.payoff.put_or_call();
179 match option.payoff.exercise_style() {
180 ContractStyle::American => price(s, k, r, q, sigma, t, pc),
181 ContractStyle::European => bs_price(s, k, r, q, sigma, t, pc),
183 ContractStyle::Bermudan(_) => {
185 unreachable!("Bermudan exercise is rejected on the BAW engine before pricing")
186 }
187 }
188}
189
190pub fn npv(option: &EquityOption) -> f64 {
191 reprice(option, 0.0, 0.0, 0.0, 0.0)
192}
193
194pub fn critical_spot(option: &EquityOption) -> f64 {
196 let (k, r, b, sigma, t) = (
197 option.base.strike_price,
198 option.risk_free_rate(),
199 option.risk_free_rate() - option.carry_yield(),
200 option.volatility(),
201 option.time_to_maturity(),
202 );
203 match option.payoff.put_or_call() {
204 PutOrCall::Call => critical_call(k, r, b, sigma, t),
205 PutOrCall::Put => critical_put(k, r, b, sigma, t),
206 }
207}
208
209pub fn price_with(option: &EquityOption, d_spot: f64, d_vol: f64, d_rate: f64, d_time: f64) -> f64 {
213 reprice(option, d_spot, d_vol, d_rate, -d_time)
214}
215
216pub(crate) struct SpotKernel {
228 k: f64,
229 r: f64,
230 q: f64,
231 sigma: f64,
232 t: f64,
233 pc: PutOrCall,
234 american: bool,
235 premium: Option<(f64, f64, f64)>,
238}
239
240impl SpotKernel {
241 pub(crate) fn new(option: &EquityOption, d_vol: f64, d_rate: f64, d_maturity: f64) -> Self {
245 let k = option.base.strike_price;
246 let r = option.risk_free_rate() + d_rate;
247 let q = option.carry_yield();
248 let sigma = option.volatility() + d_vol;
249 let t = (option.time_to_maturity() + d_maturity).max(1e-8);
250 let pc = *option.payoff.put_or_call();
251 let american = matches!(option.payoff.exercise_style(), ContractStyle::American);
252 let b = r - q;
253 let premium = if !american || sigma <= 0.0 {
254 None
255 } else {
256 match pc {
257 PutOrCall::Call if b >= r => None,
259 PutOrCall::Call => {
260 let s_star = critical_call(k, r, b, sigma, t);
261 let q2 = quadratic_root(r, b, sigma, t, true);
262 let d1 = d1_of(s_star, k, b, sigma, t);
263 let a2 = (s_star / q2) * (1.0 - ((b - r) * t).exp() * norm_cdf(d1));
264 Some((s_star, q2, a2))
265 }
266 PutOrCall::Put => {
267 let s_star = critical_put(k, r, b, sigma, t);
268 let q1 = quadratic_root(r, b, sigma, t, false);
269 let d1 = d1_of(s_star, k, b, sigma, t);
270 let a1 = -(s_star / q1) * (1.0 - ((b - r) * t).exp() * norm_cdf(-d1));
271 Some((s_star, q1, a1))
272 }
273 }
274 };
275 SpotKernel { k, r, q, sigma, t, pc, american, premium }
276 }
277
278 pub(crate) fn value(&self, s: f64) -> f64 {
281 let intrinsic = match self.pc {
282 PutOrCall::Call => (s - self.k).max(0.0),
283 PutOrCall::Put => (self.k - s).max(0.0),
284 };
285 if self.american && self.sigma <= 0.0 {
289 return intrinsic;
290 }
291 let euro = bs_price(s, self.k, self.r, self.q, self.sigma, self.t, self.pc);
292 if !self.american {
293 return euro;
294 }
295 match self.premium {
296 None => euro,
297 Some((s_star, exponent, coefficient)) => {
298 let exercised = match self.pc {
299 PutOrCall::Call => s >= s_star,
300 PutOrCall::Put => s <= s_star,
301 };
302 if exercised {
303 intrinsic
304 } else {
305 euro + coefficient * (s / s_star).powf(exponent)
306 }
307 }
308 }
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315
316 #[test]
317 fn golden_values_match_reference() {
318 let p = price(100.0, 100.0, 0.05, 0.0, 0.20, 1.0, PutOrCall::Put);
320 assert!((p - 6.09762).abs() < 1e-4, "put {p}");
321 let c = price(100.0, 100.0, 0.10, 0.10, 0.25, 0.5, PutOrCall::Call);
323 assert!((c - 6.80134).abs() < 1e-4, "call {c}");
324 }
325
326 #[test]
327 fn non_dividend_call_equals_european() {
328 for s in [80.0, 100.0, 120.0] {
330 let a = price(s, 100.0, 0.05, 0.0, 0.30, 1.0, PutOrCall::Call);
331 let e = bs_price(s, 100.0, 0.05, 0.0, 0.30, 1.0, PutOrCall::Call);
332 assert!((a - e).abs() < 1e-10, "s={s} amer {a} euro {e}");
333 }
334 }
335
336 #[test]
337 fn premium_is_non_negative_and_bounded_by_intrinsic() {
338 for &pc in &[PutOrCall::Call, PutOrCall::Put] {
339 for s in [70.0, 85.0, 100.0, 115.0, 130.0] {
340 let a = price(s, 100.0, 0.08, 0.04, 0.25, 0.75, pc);
341 let e = bs_price(s, 100.0, 0.08, 0.04, 0.25, 0.75, pc);
342 let intrinsic = match pc {
343 PutOrCall::Call => (s - 100.0).max(0.0),
344 PutOrCall::Put => (100.0 - s).max(0.0),
345 };
346 assert!(a >= e - 1e-9, "american {a} below european {e}");
347 assert!(a >= intrinsic - 1e-9, "american {a} below intrinsic {intrinsic}");
348 }
349 }
350 }
351
352 #[test]
353 fn deep_in_the_money_put_is_intrinsic() {
354 let p = price(60.0, 100.0, 0.10, 0.0, 0.20, 0.5, PutOrCall::Put);
356 assert!((p - 40.0).abs() < 1e-6, "{p}");
357 }
358
359 fn crr(pc: PutOrCall, s: f64, k: f64, r: f64, b: f64, v: f64, t: f64, steps: usize) -> f64 {
361 let dt = t / steps as f64;
362 let u = (v * dt.sqrt()).exp();
363 let d = 1.0 / u;
364 let p = ((b * dt).exp() - d) / (u - d);
365 let disc = (-r * dt).exp();
366 let intrinsic = |sp: f64| match pc {
367 PutOrCall::Call => (sp - k).max(0.0),
368 PutOrCall::Put => (k - sp).max(0.0),
369 };
370 let mut v_nodes: Vec<f64> = (0..=steps)
371 .map(|j| intrinsic(s * u.powi(j as i32) * d.powi((steps - j) as i32)))
372 .collect();
373 for i in (0..steps).rev() {
374 for j in 0..=i {
375 let cont = disc * (p * v_nodes[j + 1] + (1.0 - p) * v_nodes[j]);
376 let sp = s * u.powi(j as i32) * d.powi((i - j) as i32);
377 v_nodes[j] = cont.max(intrinsic(sp));
378 }
379 }
380 v_nodes[0]
381 }
382
383 #[test]
384 fn engine_prices_and_greeks_match_binomial() {
385 use crate::core::traits::Instrument;
386 use crate::equity::builder::EquityOptionBuilder;
387 use crate::equity::utils::Engine;
388 use chrono::NaiveDate;
389
390 let build = |engine: Engine| {
391 EquityOptionBuilder::new()
392 .symbol("ACME")
393 .spot(100.0)
394 .strike(100.0)
395 .flat_vol(0.25)
396 .flat_rate(0.08)
397 .dividend_yield(0.04)
398 .valuation_date(NaiveDate::from_ymd_opt(2026, 1, 1).unwrap())
399 .maturity_date(NaiveDate::from_ymd_opt(2026, 7, 2).unwrap())
400 .american()
401 .vanilla(PutOrCall::Put)
402 .engine(engine)
403 .build().expect("option must build")
404 };
405 let euro = EquityOptionBuilder::new()
406 .symbol("ACME")
407 .spot(100.0)
408 .strike(100.0)
409 .flat_vol(0.25)
410 .flat_rate(0.08)
411 .dividend_yield(0.04)
412 .valuation_date(NaiveDate::from_ymd_opt(2026, 1, 1).unwrap())
413 .maturity_date(NaiveDate::from_ymd_opt(2026, 7, 2).unwrap())
414 .vanilla(PutOrCall::Put)
415 .engine(Engine::BlackScholes)
416 .build().expect("option must build");
417
418 let baw_opt = build(Engine::BaroneAdesiWhaley);
419 let tree = build(Engine::Binomial);
420 let fd = build(Engine::FiniteDifference);
421
422 assert!((baw_opt.npv() - tree.npv()).abs() < 0.05,
424 "baw {} vs tree {}", baw_opt.npv(), tree.npv());
425 assert!(baw_opt.npv() > euro.npv(), "baw {} not above euro {}", baw_opt.npv(), euro.npv());
427 assert!(baw_opt.delta() < 0.0 && baw_opt.delta() > -1.0);
431 assert!(baw_opt.gamma() > 0.0);
432 assert!(baw_opt.vega() > 0.0);
433 assert!((baw_opt.delta() - fd.delta()).abs() < 0.01,
434 "baw delta {} vs fd {}", baw_opt.delta(), fd.delta());
435 assert!((baw_opt.gamma() - fd.gamma()).abs() < 0.01,
436 "baw gamma {} vs fd {}", baw_opt.gamma(), fd.gamma());
437 }
438
439 #[test]
440 fn tracks_binomial_within_a_few_cents() {
441 let cases = [
443 (PutOrCall::Put, 100.0, 100.0, 0.05, 0.0, 0.20, 1.0),
444 (PutOrCall::Put, 100.0, 100.0, 0.10, 0.0, 0.25, 0.5),
445 (PutOrCall::Call, 110.0, 100.0, 0.10, 0.10, 0.25, 0.5),
446 (PutOrCall::Put, 95.0, 100.0, 0.08, 0.03, 0.30, 0.25),
447 (PutOrCall::Call, 100.0, 100.0, 0.06, 0.09, 0.20, 1.0),
448 ];
449 for (pc, s, k, r, q, v, t) in cases {
450 let baw = price(s, k, r, q, v, t, pc);
451 let tree = crr(pc, s, k, r, r - q, v, t, 3000);
452 assert!(
453 (baw - tree).abs() < 0.05,
454 "{pc:?} s={s}: baw {baw} vs tree {tree}"
455 );
456 }
457 }
458}