1use libm::{exp, log};
2use std::f64::consts::{PI, SQRT_2};
3use std::{io, thread};
4use crate::core::quotes::Quote;
5use chrono::{Datelike, Local, NaiveDate};
6use crate::core::utils::{ContractStyle, dN, N};
9use crate::core::trade::{PutOrCall, Transection};
10use super::asian::{self, AsianStrikeType, AveragingType};
11use super::barrier;
12use super::vanila_option::{AsianPayoff, BarrierPayoff, BinaryPayoff, BinaryType, EquityOption, EquityOptionBase, VanillaPayoff};
13use super::utils::{Engine, PayoffType, Payoff, LongShort};
14use crate::core::curves::{Compounding, YieldCurve};
15use crate::core::daycount::DayCountConvention;
16use crate::core::vols::VolSurface;
17use super::super::core::traits::{Instrument,Greeks};
18
19pub struct BlackScholesPricer;
20impl BlackScholesPricer {
21 pub fn new() -> Self {
22 BlackScholesPricer
23 }
24 pub fn npv(&self, bsd_option: &EquityOption) -> f64 {
25 assert!(bsd_option.time_to_maturity() >= 0.0, "Option is expired or negative time");
27 assert!(bsd_option.base.underlying_price.value >= 0.0, "Negative underlying price not allowed");
28 if bsd_option.base.is_futures_option() {
29 return self.npv_black76(bsd_option);
30 }
31 match &bsd_option.payoff.payoff_kind() {
32 PayoffType::Vanilla => self.npv_vanilla(bsd_option),
33 PayoffType::Binary => self.npv_binary(bsd_option),
34 PayoffType::Barrier => self.npv_barrier(bsd_option),
35 PayoffType::Asian => self.npv_asian(bsd_option),
36 PayoffType::ForwardStart => self.npv_forward_start(bsd_option),
37 _ => {0.0}
38 }
39 }
40 pub fn delta(&self, bsd_option: &EquityOption) -> f64 {
41 assert!(bsd_option.time_to_maturity() >= 0.0, "Option is expired or negative time");
43 assert!(bsd_option.base.underlying_price.value >= 0.0, "Negative underlying price not allowed");
44 if bsd_option.base.is_futures_option() {
45 return self.delta_black76(bsd_option);
46 }
47 match &bsd_option.payoff.payoff_kind() {
48 PayoffType::Vanilla => self.delta_vanilla(bsd_option),
49 PayoffType::Binary => self.delta_binary(bsd_option),
50 PayoffType::Barrier => self.delta_barrier(bsd_option),
51 PayoffType::Asian => self.delta_asian(bsd_option),
52 PayoffType::ForwardStart => self.delta_forward_start(bsd_option),
53 _ => {0.0}
54 }
55 }
56 pub fn gamma(&self, bsd_option: &EquityOption) -> f64 {
57 if bsd_option.base.is_futures_option() {
58 return self.gamma_black76(bsd_option);
59 }
60 match &bsd_option.payoff.payoff_kind() {
61 PayoffType::Vanilla => self.gamma_vanilla(bsd_option),
62 PayoffType::Binary => self.gamma_binary(bsd_option),
63 PayoffType::Barrier => self.gamma_barrier(bsd_option),
64 PayoffType::Asian => self.gamma_asian(bsd_option),
65 PayoffType::ForwardStart => self.gamma_forward_start(bsd_option),
66 _ => {0.0}
67 }
68 }
69 pub fn vega(&self, bsd_option: &EquityOption) -> f64 {
70 if bsd_option.base.is_futures_option() {
71 return self.vega_black76(bsd_option);
72 }
73 match &bsd_option.payoff.payoff_kind() {
74 PayoffType::Vanilla => self.vega_vanilla(bsd_option),
75 PayoffType::Binary => self.vega_binary(bsd_option),
76 PayoffType::Barrier => self.vega_barrier(bsd_option),
77 PayoffType::Asian => self.vega_asian(bsd_option),
78 PayoffType::ForwardStart => self.vega_forward_start(bsd_option),
79 _ => {0.0}
80 }
81 }
82 pub fn theta(&self, bsd_option: &EquityOption) -> f64 {
83 if bsd_option.base.is_futures_option() {
84 return self.theta_black76(bsd_option);
85 }
86 match &bsd_option.payoff.payoff_kind() {
87 PayoffType::Vanilla => self.theta_vanilla(bsd_option),
88 PayoffType::Binary => self.theta_binary(bsd_option),
89 PayoffType::Barrier => self.theta_barrier(bsd_option),
90 PayoffType::Asian => self.theta_asian(bsd_option),
91 PayoffType::ForwardStart => self.theta_forward_start(bsd_option),
92 _ => {0.0}
93 }
94 }
95 pub fn rho(&self, bsd_option: &EquityOption) -> f64 {
96 if bsd_option.base.is_futures_option() {
97 return self.rho_black76(bsd_option);
98 }
99 match &bsd_option.payoff.payoff_kind() {
100 PayoffType::Vanilla => self.rho_vanilla(bsd_option),
101 PayoffType::Binary => self.rho_binary(bsd_option),
102 PayoffType::Barrier => self.rho_barrier(bsd_option),
103 PayoffType::Asian => self.rho_asian(bsd_option),
104 PayoffType::ForwardStart => self.rho_forward_start(bsd_option),
105 _ => {0.0}
106 }
107 }
108 fn black76_inputs(bsd_option: &EquityOption)
113 -> (f64, f64, f64, f64, f64, PutOrCall, crate::equity::black76::FuturesSettlement)
114 {
115 let f = bsd_option.base.underlying_price.value();
116 let k = bsd_option.base.strike_price;
117 let r = bsd_option.base.risk_free_rate();
118 let t = bsd_option.time_to_maturity();
119 let sigma = bsd_option.base.vol_surface.vol(k, f, t);
120 let settlement = bsd_option
121 .base
122 .futures_settlement
123 .expect("black76 pricer called on a non-futures option");
124 (f, k, r, sigma, t, *bsd_option.payoff.put_or_call(), settlement)
125 }
126 fn npv_black76(&self, o: &EquityOption) -> f64 {
127 let (f, k, r, sig, t, pc, s) = Self::black76_inputs(o);
128 crate::equity::black76::price(f, k, r, sig, t, pc, s)
129 }
130 fn delta_black76(&self, o: &EquityOption) -> f64 {
131 let (f, k, r, sig, t, pc, s) = Self::black76_inputs(o);
132 crate::equity::black76::delta(f, k, r, sig, t, pc, s)
133 }
134 fn gamma_black76(&self, o: &EquityOption) -> f64 {
135 let (f, k, r, sig, t, _pc, s) = Self::black76_inputs(o);
136 crate::equity::black76::gamma(f, k, r, sig, t, s)
137 }
138 fn vega_black76(&self, o: &EquityOption) -> f64 {
139 let (f, k, r, sig, t, _pc, s) = Self::black76_inputs(o);
140 crate::equity::black76::vega(f, k, r, sig, t, s)
141 }
142 fn theta_black76(&self, o: &EquityOption) -> f64 {
143 let (f, k, r, sig, t, pc, s) = Self::black76_inputs(o);
144 crate::equity::black76::theta(f, k, r, sig, t, pc, s)
145 }
146 fn rho_black76(&self, o: &EquityOption) -> f64 {
147 let (f, k, r, sig, t, pc, s) = Self::black76_inputs(o);
148 crate::equity::black76::rho(f, k, r, sig, t, pc, s)
149 }
150 fn npv_vanilla(&self, bsd_option: &EquityOption) -> f64 {
151
152 let n_d1 = N(bsd_option.base.d1());
153 let n_d2 = N(bsd_option.base.d2());
154 let df_d = exp(-bsd_option.base.carry_yield() * bsd_option.time_to_maturity());
155 let df_r = bsd_option.base.maturity_discount_factor();
156 match bsd_option.payoff.put_or_call() {
157 PutOrCall::Call => {bsd_option.base.effective_spot()*n_d1 *df_d
158 -bsd_option.base.strike_price*n_d2*df_r
159 }
160 PutOrCall::Put => {bsd_option.base.strike_price*N(-bsd_option.base.d2())*df_r-
161 bsd_option.base.effective_spot()*N(-bsd_option.base.d1()) *df_d
162 }
163
164 }
165 }
166 fn delta_vanilla(&self, bsd_option: &EquityOption) -> f64 {
167 let n_d1 = N(bsd_option.base.d1());
169 let df_d = exp(-bsd_option.base.carry_yield() * bsd_option.time_to_maturity());
170
171 match bsd_option.payoff.put_or_call() {
172 PutOrCall::Call => {n_d1 * df_d }
173 PutOrCall::Put => {(n_d1-1.0) * df_d }
174 }
175 }
176 fn gamma_vanilla(&self, bsd_option: &EquityOption) -> f64 {
177 let dn_d1 = dN(bsd_option.base.d1());
179 let df_d = exp(-bsd_option.base.carry_yield() * bsd_option.time_to_maturity());
180 let var_sqrt = bsd_option.base.volatility() * (bsd_option.time_to_maturity().sqrt());
181 dn_d1 * df_d / (bsd_option.base.effective_spot() * var_sqrt)
182 }
183 fn vega_vanilla(&self, bsd_option: &EquityOption) -> f64 {
184 let dn_d1 = dN(bsd_option.base.d1());
186 let df_d = exp(-bsd_option.base.carry_yield() * bsd_option.time_to_maturity());
187 let df_S = bsd_option.base.effective_spot() * df_d;
188 let vega = df_S * dn_d1 * bsd_option.time_to_maturity().sqrt();
189 vega
190 }
191 fn theta_vanilla(&self, bsd_option: &EquityOption) -> f64 {
192 let q = bsd_option.base.carry_yield();
195 let r = bsd_option.base.risk_free_rate();
196 let k = bsd_option.base.strike_price;
197 let dn_d1 = dN(bsd_option.base.d1());
198 let n_d1 = N(bsd_option.base.d1());
199 let n_d2 = N(bsd_option.base.d2());
200 let df_d = exp(-q * bsd_option.time_to_maturity());
201 let df_r = bsd_option.base.maturity_discount_factor();
202 let df_S = bsd_option.base.effective_spot() * df_d;
203 let t1 = -df_S * dn_d1 * bsd_option.base.volatility()
204 / (2.0 * bsd_option.time_to_maturity().sqrt());
205
206 match bsd_option.payoff.put_or_call() {
207 PutOrCall::Call => {
208 t1 + q * df_S * n_d1 - r * k * df_r * n_d2
209 }
210 PutOrCall::Put => {
211 t1 - q * df_S * N(-bsd_option.base.d1()) + r * k * df_r * N(-bsd_option.base.d2())
212 }
213 }
214 }
215 fn rho_vanilla(&self, bsd_option: &EquityOption) -> f64 {
216 let n_d2 = N(bsd_option.base.d2());
218 let df_r = bsd_option.base.maturity_discount_factor();
219 let r1 = bsd_option.time_to_maturity()*bsd_option.base.strike_price;
220 match bsd_option.payoff.put_or_call() {
221 PutOrCall::Call => {
222 r1*n_d2*df_r
223 }
224 PutOrCall::Put => {-r1*N(-bsd_option.base.d2())*df_r
225 }
226
227 }
228 }
229
230 fn binary_details(bsd_option: &EquityOption) -> (BinaryType, f64) {
238 let payoff = bsd_option
239 .payoff
240 .as_any()
241 .downcast_ref::<BinaryPayoff>()
242 .expect("payoff of kind Binary must be a BinaryPayoff");
243 (payoff.binary_type, payoff.cash)
244 }
245
246 fn npv_binary(&self, bsd_option: &EquityOption) -> f64 {
247 let (binary_type, cash) = Self::binary_details(bsd_option);
248 let df_r = bsd_option.base.maturity_discount_factor();
249 let df_q = exp(-bsd_option.base.carry_yield() * bsd_option.time_to_maturity());
250 let s = bsd_option.base.effective_spot();
251 match (binary_type, bsd_option.payoff.put_or_call()) {
252 (BinaryType::CashOrNothing, PutOrCall::Call) => cash * df_r * N(bsd_option.base.d2()),
253 (BinaryType::CashOrNothing, PutOrCall::Put) => cash * df_r * N(-bsd_option.base.d2()),
254 (BinaryType::AssetOrNothing, PutOrCall::Call) => s * df_q * N(bsd_option.base.d1()),
255 (BinaryType::AssetOrNothing, PutOrCall::Put) => s * df_q * N(-bsd_option.base.d1()),
256 }
257 }
258 fn delta_binary(&self, bsd_option: &EquityOption) -> f64 {
259 let (binary_type, cash) = Self::binary_details(bsd_option);
260 let t = bsd_option.time_to_maturity();
261 let sigma = bsd_option.base.volatility();
262 let s = bsd_option.base.effective_spot();
263 let vol_sqrt_t = sigma * t.sqrt();
264 match binary_type {
265 BinaryType::CashOrNothing => {
266 let df_r = bsd_option.base.maturity_discount_factor();
268 let delta_call = cash * df_r * dN(bsd_option.base.d2()) / (s * vol_sqrt_t);
269 match bsd_option.payoff.put_or_call() {
270 PutOrCall::Call => delta_call,
271 PutOrCall::Put => -delta_call,
272 }
273 }
274 BinaryType::AssetOrNothing => {
275 let df_q = exp(-bsd_option.base.dividend_yield * t);
277 let d1 = bsd_option.base.d1();
278 match bsd_option.payoff.put_or_call() {
279 PutOrCall::Call => df_q * (N(d1) + dN(d1) / vol_sqrt_t),
280 PutOrCall::Put => df_q * (N(-d1) - dN(d1) / vol_sqrt_t),
281 }
282 }
283 }
284 }
285 fn gamma_binary(&self, bsd_option: &EquityOption) -> f64 {
286 let (binary_type, cash) = Self::binary_details(bsd_option);
287 let t = bsd_option.time_to_maturity();
288 let sigma = bsd_option.base.volatility();
289 let s = bsd_option.base.effective_spot();
290 let vol_sqrt_t = sigma * t.sqrt();
291 let gamma_call = match binary_type {
292 BinaryType::CashOrNothing => {
293 let df_r = bsd_option.base.maturity_discount_factor();
295 -cash * df_r * dN(bsd_option.base.d2()) * bsd_option.base.d1()
296 / (s * s * sigma * sigma * t)
297 }
298 BinaryType::AssetOrNothing => {
299 let df_q = exp(-bsd_option.base.dividend_yield * t);
301 let d1 = bsd_option.base.d1();
302 df_q * dN(d1) * (1.0 - d1 / vol_sqrt_t) / (s * vol_sqrt_t)
303 }
304 };
305 match bsd_option.payoff.put_or_call() {
306 PutOrCall::Call => gamma_call,
307 PutOrCall::Put => -gamma_call,
308 }
309 }
310 fn vega_binary(&self, bsd_option: &EquityOption) -> f64 {
311 let (binary_type, cash) = Self::binary_details(bsd_option);
312 let t = bsd_option.time_to_maturity();
313 let sigma = bsd_option.base.volatility();
314 let s = bsd_option.base.effective_spot();
315 let vega_call = match binary_type {
316 BinaryType::CashOrNothing => {
317 let df_r = bsd_option.base.maturity_discount_factor();
319 -cash * df_r * dN(bsd_option.base.d2()) * bsd_option.base.d1() / sigma
320 }
321 BinaryType::AssetOrNothing => {
322 let df_q = exp(-bsd_option.base.dividend_yield * t);
324 -s * df_q * dN(bsd_option.base.d1()) * bsd_option.base.d2() / sigma
325 }
326 };
327 match bsd_option.payoff.put_or_call() {
328 PutOrCall::Call => vega_call,
329 PutOrCall::Put => -vega_call,
330 }
331 }
332 fn theta_binary(&self, bsd_option: &EquityOption) -> f64 {
333 let (binary_type, cash) = Self::binary_details(bsd_option);
334 let r = bsd_option.base.risk_free_rate();
335 let q = bsd_option.base.carry_yield();
336 let t = bsd_option.time_to_maturity();
337 let sigma = bsd_option.base.volatility();
338 let s = bsd_option.base.effective_spot();
339 match binary_type {
340 BinaryType::CashOrNothing => {
341 let df_r = bsd_option.base.maturity_discount_factor();
343 let d2 = bsd_option.base.d2();
344 let dd2_dt = (r - q - 0.5 * sigma * sigma) / (sigma * t.sqrt()) - d2 / (2.0 * t);
345 match bsd_option.payoff.put_or_call() {
346 PutOrCall::Call => cash * (r * df_r * N(d2) - df_r * dN(d2) * dd2_dt),
347 PutOrCall::Put => cash * (r * df_r * N(-d2) + df_r * dN(d2) * dd2_dt),
348 }
349 }
350 BinaryType::AssetOrNothing => {
351 let df_q = exp(-q * t);
353 let d1 = bsd_option.base.d1();
354 let dd1_dt = (r - q + 0.5 * sigma * sigma) / (sigma * t.sqrt()) - d1 / (2.0 * t);
355 match bsd_option.payoff.put_or_call() {
356 PutOrCall::Call => q * s * df_q * N(d1) - s * df_q * dN(d1) * dd1_dt,
357 PutOrCall::Put => q * s * df_q * N(-d1) + s * df_q * dN(d1) * dd1_dt,
358 }
359 }
360 }
361 }
362 fn rho_binary(&self, bsd_option: &EquityOption) -> f64 {
363 let (binary_type, cash) = Self::binary_details(bsd_option);
364 let t = bsd_option.time_to_maturity();
365 let sigma = bsd_option.base.volatility();
366 let s = bsd_option.base.effective_spot();
367 match binary_type {
368 BinaryType::CashOrNothing => {
369 let df_r = bsd_option.base.maturity_discount_factor();
370 let d2 = bsd_option.base.d2();
371 match bsd_option.payoff.put_or_call() {
372 PutOrCall::Call => cash * (-t * df_r * N(d2) + df_r * dN(d2) * t.sqrt() / sigma),
373 PutOrCall::Put => cash * (-t * df_r * N(-d2) - df_r * dN(d2) * t.sqrt() / sigma),
374 }
375 }
376 BinaryType::AssetOrNothing => {
377 let df_q = exp(-bsd_option.base.dividend_yield * t);
379 let rho_call = s * df_q * dN(bsd_option.base.d1()) * t.sqrt() / sigma;
380 match bsd_option.payoff.put_or_call() {
381 PutOrCall::Call => rho_call,
382 PutOrCall::Put => -rho_call,
383 }
384 }
385 }
386 }
387
388 fn barrier_price_with(
395 bsd_option: &EquityOption,
396 ds: f64,
397 dsigma: f64,
398 dr: f64,
399 dt_shift: f64,
400 ) -> f64 {
401 let payoff = bsd_option
402 .payoff
403 .as_any()
404 .downcast_ref::<BarrierPayoff>()
405 .expect("payoff of kind Barrier must be a BarrierPayoff");
406 barrier::barrier_price(
407 bsd_option.base.effective_spot() + ds,
408 bsd_option.base.strike_price,
409 payoff.barrier,
410 bsd_option.base.risk_free_rate() + dr,
411 bsd_option.base.carry_yield(),
412 bsd_option.base.volatility() + dsigma,
413 bsd_option.time_to_maturity() + dt_shift,
414 payoff.direction,
415 payoff.knock,
416 *bsd_option.payoff.put_or_call(),
417 )
418 }
419 fn npv_barrier(&self, bsd_option: &EquityOption) -> f64 {
420 Self::barrier_price_with(bsd_option, 0.0, 0.0, 0.0, 0.0)
421 }
422 fn delta_barrier(&self, bsd_option: &EquityOption) -> f64 {
423 let h = bsd_option.base.underlying_price.value() * 1e-4;
424 (Self::barrier_price_with(bsd_option, h, 0.0, 0.0, 0.0)
425 - Self::barrier_price_with(bsd_option, -h, 0.0, 0.0, 0.0))
426 / (2.0 * h)
427 }
428 fn gamma_barrier(&self, bsd_option: &EquityOption) -> f64 {
429 let h = bsd_option.base.underlying_price.value() * 1e-3;
430 (Self::barrier_price_with(bsd_option, h, 0.0, 0.0, 0.0)
431 - 2.0 * Self::barrier_price_with(bsd_option, 0.0, 0.0, 0.0, 0.0)
432 + Self::barrier_price_with(bsd_option, -h, 0.0, 0.0, 0.0))
433 / (h * h)
434 }
435 fn vega_barrier(&self, bsd_option: &EquityOption) -> f64 {
436 let h = 1e-4;
437 (Self::barrier_price_with(bsd_option, 0.0, h, 0.0, 0.0)
438 - Self::barrier_price_with(bsd_option, 0.0, -h, 0.0, 0.0))
439 / (2.0 * h)
440 }
441 fn theta_barrier(&self, bsd_option: &EquityOption) -> f64 {
442 let h = (1.0 / 365.0_f64).min(0.5 * bsd_option.time_to_maturity());
443 -(Self::barrier_price_with(bsd_option, 0.0, 0.0, 0.0, h)
444 - Self::barrier_price_with(bsd_option, 0.0, 0.0, 0.0, -h))
445 / (2.0 * h)
446 }
447 fn rho_barrier(&self, bsd_option: &EquityOption) -> f64 {
448 let h = 1e-5;
449 (Self::barrier_price_with(bsd_option, 0.0, 0.0, h, 0.0)
450 - Self::barrier_price_with(bsd_option, 0.0, 0.0, -h, 0.0))
451 / (2.0 * h)
452 }
453
454 fn asian_price_with(
461 bsd_option: &EquityOption,
462 ds: f64,
463 dsigma: f64,
464 dr: f64,
465 dt_shift: f64,
466 ) -> f64 {
467 let payoff = bsd_option
468 .payoff
469 .as_any()
470 .downcast_ref::<AsianPayoff>()
471 .expect("payoff of kind Asian must be an AsianPayoff");
472 if payoff.strike_type == AsianStrikeType::FloatingStrike {
473 panic!(
474 "Floating-strike Asian options have no analytic pricer; \
475 use the MonteCarlo engine"
476 );
477 }
478 let s = bsd_option.base.effective_spot() + ds;
479 let k = bsd_option.base.strike_price;
480 let r = bsd_option.base.risk_free_rate() + dr;
481 let q = bsd_option.base.carry_yield();
482 let sigma = bsd_option.base.volatility() + dsigma;
483 let t = bsd_option.time_to_maturity() + dt_shift;
484 let pc = *bsd_option.payoff.put_or_call();
485 match payoff.averaging {
486 AveragingType::Geometric => {
487 asian::geometric_asian_price(s, k, r, q, sigma, t, None, pc)
488 }
489 AveragingType::Arithmetic => asian::turnbull_wakeman_price(s, k, r, q, sigma, t, pc),
490 }
491 }
492 fn npv_asian(&self, bsd_option: &EquityOption) -> f64 {
493 Self::asian_price_with(bsd_option, 0.0, 0.0, 0.0, 0.0)
494 }
495 fn delta_asian(&self, bsd_option: &EquityOption) -> f64 {
496 let h = bsd_option.base.underlying_price.value() * 1e-4;
497 (Self::asian_price_with(bsd_option, h, 0.0, 0.0, 0.0)
498 - Self::asian_price_with(bsd_option, -h, 0.0, 0.0, 0.0))
499 / (2.0 * h)
500 }
501 fn gamma_asian(&self, bsd_option: &EquityOption) -> f64 {
502 let h = bsd_option.base.underlying_price.value() * 1e-3;
503 (Self::asian_price_with(bsd_option, h, 0.0, 0.0, 0.0)
504 - 2.0 * Self::asian_price_with(bsd_option, 0.0, 0.0, 0.0, 0.0)
505 + Self::asian_price_with(bsd_option, -h, 0.0, 0.0, 0.0))
506 / (h * h)
507 }
508 fn vega_asian(&self, bsd_option: &EquityOption) -> f64 {
509 let h = 1e-4;
510 (Self::asian_price_with(bsd_option, 0.0, h, 0.0, 0.0)
511 - Self::asian_price_with(bsd_option, 0.0, -h, 0.0, 0.0))
512 / (2.0 * h)
513 }
514 fn theta_asian(&self, bsd_option: &EquityOption) -> f64 {
515 let h = (1.0 / 365.0_f64).min(0.5 * bsd_option.time_to_maturity());
516 -(Self::asian_price_with(bsd_option, 0.0, 0.0, 0.0, h)
517 - Self::asian_price_with(bsd_option, 0.0, 0.0, 0.0, -h))
518 / (2.0 * h)
519 }
520 fn rho_asian(&self, bsd_option: &EquityOption) -> f64 {
521 let h = 1e-5;
522 (Self::asian_price_with(bsd_option, 0.0, 0.0, h, 0.0)
523 - Self::asian_price_with(bsd_option, 0.0, 0.0, -h, 0.0))
524 / (2.0 * h)
525 }
526
527 fn forward_start_price_with(
530 bsd_option: &EquityOption,
531 ds: f64,
532 dsigma: f64,
533 dr: f64,
534 dt_shift: f64,
535 ) -> f64 {
536 let payoff = bsd_option
537 .payoff
538 .as_any()
539 .downcast_ref::<crate::equity::forward_start_option::ForwardStartPayoff>()
540 .expect("payoff of kind ForwardStart must be a ForwardStartPayoff");
541 let t = bsd_option.time_to_maturity() + dt_shift;
542 crate::equity::forward_start_option::forward_start_price(
543 bsd_option.base.effective_spot() + ds,
544 payoff.strike_fraction,
545 bsd_option.base.risk_free_rate() + dr,
546 bsd_option.base.carry_yield(),
547 bsd_option.base.volatility() + dsigma,
548 payoff.start_fraction * t,
549 t,
550 *bsd_option.payoff.put_or_call(),
551 )
552 }
553 fn npv_forward_start(&self, bsd_option: &EquityOption) -> f64 {
554 Self::forward_start_price_with(bsd_option, 0.0, 0.0, 0.0, 0.0)
555 }
556 fn delta_forward_start(&self, bsd_option: &EquityOption) -> f64 {
557 let h = bsd_option.base.underlying_price.value() * 1e-4;
558 (Self::forward_start_price_with(bsd_option, h, 0.0, 0.0, 0.0)
559 - Self::forward_start_price_with(bsd_option, -h, 0.0, 0.0, 0.0))
560 / (2.0 * h)
561 }
562 fn gamma_forward_start(&self, bsd_option: &EquityOption) -> f64 {
563 let h = bsd_option.base.underlying_price.value() * 1e-3;
564 (Self::forward_start_price_with(bsd_option, h, 0.0, 0.0, 0.0)
565 - 2.0 * Self::forward_start_price_with(bsd_option, 0.0, 0.0, 0.0, 0.0)
566 + Self::forward_start_price_with(bsd_option, -h, 0.0, 0.0, 0.0))
567 / (h * h)
568 }
569 fn vega_forward_start(&self, bsd_option: &EquityOption) -> f64 {
570 let h = 1e-4;
571 (Self::forward_start_price_with(bsd_option, 0.0, h, 0.0, 0.0)
572 - Self::forward_start_price_with(bsd_option, 0.0, -h, 0.0, 0.0))
573 / (2.0 * h)
574 }
575 fn theta_forward_start(&self, bsd_option: &EquityOption) -> f64 {
576 let h = (1.0 / 365.0_f64).min(0.25 * bsd_option.time_to_maturity());
577 -(Self::forward_start_price_with(bsd_option, 0.0, 0.0, 0.0, h)
578 - Self::forward_start_price_with(bsd_option, 0.0, 0.0, 0.0, -h))
579 / (2.0 * h)
580 }
581 fn rho_forward_start(&self, bsd_option: &EquityOption) -> f64 {
582 let h = 1e-5;
583 (Self::forward_start_price_with(bsd_option, 0.0, 0.0, h, 0.0)
584 - Self::forward_start_price_with(bsd_option, 0.0, 0.0, -h, 0.0))
585 / (2.0 * h)
586 }
587
588}
589
590
591pub fn bs_price(s: f64, k: f64, r: f64, q: f64, sigma: f64, t: f64, put_or_call: PutOrCall) -> f64 {
594 if t <= 0.0 || sigma <= 0.0 {
595 return match put_or_call {
596 PutOrCall::Call => (s * exp(-q * t) - k * exp(-r * t)).max(0.0),
597 PutOrCall::Put => (k * exp(-r * t) - s * exp(-q * t)).max(0.0),
598 };
599 }
600 let sqrt_t = t.sqrt();
601 let d1 = ((s / k).ln() + (r - q + 0.5 * sigma * sigma) * t) / (sigma * sqrt_t);
602 let d2 = d1 - sigma * sqrt_t;
603 match put_or_call {
604 PutOrCall::Call => s * exp(-q * t) * N(d1) - k * exp(-r * t) * N(d2),
605 PutOrCall::Put => k * exp(-r * t) * N(-d2) - s * exp(-q * t) * N(-d1),
606 }
607}
608
609pub fn bs_vega(s: f64, k: f64, r: f64, q: f64, sigma: f64, t: f64) -> f64 {
611 let sqrt_t = t.sqrt();
612 let d1 = ((s / k).ln() + (r - q + 0.5 * sigma * sigma) * t) / (sigma * sqrt_t);
613 s * exp(-q * t) * dN(d1) * sqrt_t
614}
615
616const IMPLIED_VOL_MIN: f64 = 1e-4;
617const IMPLIED_VOL_MAX: f64 = 5.0;
618
619pub fn implied_vol_from_price(
626 s: f64,
627 k: f64,
628 r: f64,
629 q: f64,
630 t: f64,
631 target: f64,
632 put_or_call: PutOrCall,
633) -> Result<f64, String> {
634 if t <= 0.0 {
635 return Err("option is expired".to_string());
636 }
637 let lower_bound = bs_price(s, k, r, q, 0.0, t, put_or_call);
638 let upper_bound = match put_or_call {
639 PutOrCall::Call => s * exp(-q * t),
640 PutOrCall::Put => k * exp(-r * t),
641 };
642 if target < lower_bound - 1e-12 || target > upper_bound + 1e-12 {
643 return Err(format!(
644 "price {target} violates arbitrage bounds [{lower_bound}, {upper_bound}]"
645 ));
646 }
647
648 let (mut lo, mut hi) = (IMPLIED_VOL_MIN, IMPLIED_VOL_MAX);
649 if bs_price(s, k, r, q, lo, t, put_or_call) > target {
650 return Ok(lo); }
652 if bs_price(s, k, r, q, hi, t, put_or_call) < target {
653 return Err(format!("implied vol above {IMPLIED_VOL_MAX}"));
654 }
655
656 let mut sigma = 0.5_f64.min(hi).max(lo);
657 let tol = 1e-12 * target.max(1.0);
658 for _ in 0..100 {
659 let diff = bs_price(s, k, r, q, sigma, t, put_or_call) - target;
660 if diff.abs() < tol {
661 return Ok(sigma);
662 }
663 if diff > 0.0 {
664 hi = sigma;
665 } else {
666 lo = sigma;
667 }
668 let vega = bs_vega(s, k, r, q, sigma, t);
669 let newton = sigma - diff / vega;
670 sigma = if vega > 1e-12 && newton > lo && newton < hi {
671 newton
672 } else {
673 0.5 * (lo + hi)
674 };
675 if hi - lo < 1e-14 {
676 return Ok(sigma);
677 }
678 }
679 Ok(sigma)
680}
681
682pub fn option_pricing() {
683 println!("Welcome to the Black-Scholes Option pricer.");
684 print!(">>");
685 println!(" What is the current price of the underlying asset?");
686 print!(">>");
687 let mut curr_price = String::new();
688 io::stdin()
689 .read_line(&mut curr_price)
690 .expect("Failed to read line");
691 println!(" Do you want a call option ('C') or a put option ('P') ?");
692 print!(">>");
693 let mut side_input = String::new();
694 io::stdin()
695 .read_line(&mut side_input)
696 .expect("Failed to read line");
697 let side: PutOrCall;
698 match side_input.trim() {
699 "C" | "c" | "Call" | "call" => side = PutOrCall::Call,
700 "P" | "p" | "Put" | "put" => side = PutOrCall::Put,
701 _ => panic!("Invalide side argument! Side has to be either 'C' or 'P'."),
702 }
703 println!("Stike price:");
704 print!(">>");
705 let mut strike = String::new();
706 io::stdin()
707 .read_line(&mut strike)
708 .expect("Failed to read line");
709 println!("Expected annualized volatility in %:");
710 println!("E.g.: Enter 50% chance as 0.50 ");
711 print!(">>");
712 let mut vol = String::new();
713 io::stdin()
714 .read_line(&mut vol)
715 .expect("Failed to read line");
716
717 println!("Risk-free rate in %:");
718 print!(">>");
719 let mut rf = String::new();
720 io::stdin().read_line(&mut rf).expect("Failed to read line");
721 println!(" Maturity date in YYYY-MM-DD format:");
722
723 let mut expiry = String::new();
724 println!("E.g.: Enter 2020-12-31 for 31st December 2020");
725 print!(">>");
726 io::stdin()
727 .read_line(&mut expiry)
728 .expect("Failed to read line");
729 println!("{:?}", expiry.trim());
730 let _d = expiry.trim();
731 let future_date = NaiveDate::parse_from_str(&_d, "%Y-%m-%d").expect("Invalid date format");
732 println!("Dividend yield on this stock:");
734 print!(">>");
735 let mut div = String::new();
736 io::stdin()
737 .read_line(&mut div)
738 .expect("Failed to read line");
739
740 let valuation_date = Local::now().date_naive();
741 let discount_curve = YieldCurve::flat(
742 rf.trim().parse::<f64>().unwrap(),
743 valuation_date,
744 DayCountConvention::Act365,
745 Compounding::Continuous,
746 )
747 .expect("Invalid risk free rate");
748 let vol_surface = VolSurface::flat(
749 vol.trim().parse::<f64>().unwrap(),
750 valuation_date,
751 DayCountConvention::Act365,
752 )
753 .expect("Invalid volatility");
754 let curr_quote = Quote::new( curr_price.trim().parse::<f64>().unwrap());
755 let option = EquityOptionBase {
756
757 symbol:"ABC".to_string(),
758 currency: None,
759 exchange:None,
760 name: None,
761 cusip: None,
762 isin: None,
763 settlement_type: Some("ABC".to_string()),
764 entry_price: 0.0,
765 long_short: LongShort::LONG,
766 underlying_price: curr_quote,
767 current_price: Quote::new(0.0),
768 strike_price: strike.trim().parse::<f64>().unwrap(),
769 vol_surface,
770 maturity_date: future_date,
771 discount_curve,
772 dividend_yield: div.trim().parse::<f64>().unwrap(),
773 borrow_cost: 0.0,
774 cash_dividends: vec![],
775 futures_settlement: None,
776 valuation_date,
777 multiplier: 1.0,
778 };
779 let payoff = Box::new(VanillaPayoff{put_or_call:side,
781 exercise_style:ContractStyle::European});
782 let option = EquityOption {
783 base: option,
784 payoff:payoff,
785 engine:Engine::BlackScholes,
786 mc: crate::equity::montecarlo::MonteCarloConfig::default(),
787 fd: crate::equity::finite_difference::FdConfig::default(),
788 heston: None
789 };
790 println!("Theoretical Price ${}", option.npv());
791 println!("Premium at risk ${}", option.get_premium_at_risk());
792 println!("Delta {}", option.delta());
793 println!("Gamma {}", option.gamma());
794 println!("Vega {}", option.vega() * 0.01);
795 println!("Theta {}", option.theta() * (1.0 / 365.0));
796 println!("Rho {}", option.rho() * 0.01);
797 let mut wait = String::new();
798 io::stdin()
799 .read_line(&mut wait)
800 .expect("Failed to read line");
801}
802pub fn implied_volatility(){}
803#[cfg(test)]
890mod tests {
891 use assert_approx_eq::assert_approx_eq;
892 use super::*;
893 use crate::core::curves::{Compounding, InterpolationMethod, Tenor, YieldCurve};
894 use crate::core::daycount::DayCountConvention;
895 use crate::core::utils::ContractStyle;
896
897 fn test_option_with(payoff: Box<dyn Payoff>, curve: YieldCurve) -> EquityOption {
899 let valuation_date = NaiveDate::from_ymd_opt(2026, 1, 1).unwrap();
900 let base = EquityOptionBase {
901 symbol: "TEST".to_string(),
902 currency: None,
903 exchange: None,
904 name: None,
905 cusip: None,
906 isin: None,
907 settlement_type: None,
908 underlying_price: Quote::new(100.0),
909 current_price: Quote::new(0.0),
910 strike_price: 100.0,
911 dividend_yield: 0.0,
912 borrow_cost: 0.0,
913 cash_dividends: vec![],
914 futures_settlement: None,
915 vol_surface: VolSurface::flat(0.3, valuation_date, DayCountConvention::Act365)
916 .unwrap(),
917 maturity_date: NaiveDate::from_ymd_opt(2027, 1, 1).unwrap(),
918 valuation_date,
919 discount_curve: curve,
920 entry_price: 0.0,
921 long_short: LongShort::LONG,
922 multiplier: 1.0,
923 };
924 EquityOption {
925 base,
926 payoff,
927 engine: Engine::BlackScholes,
928 mc: crate::equity::montecarlo::MonteCarloConfig::default(),
929 fd: crate::equity::finite_difference::FdConfig::default(),
930 heston: None,
931 }
932 }
933
934 fn test_option(put_or_call: PutOrCall, curve: YieldCurve) -> EquityOption {
935 test_option_with(
936 Box::new(VanillaPayoff { put_or_call, exercise_style: ContractStyle::European }),
937 curve,
938 )
939 }
940
941 fn binary_option_of(
942 put_or_call: PutOrCall,
943 binary_type: BinaryType,
944 cash: f64,
945 ) -> EquityOption {
946 test_option_with(
947 Box::new(BinaryPayoff {
948 put_or_call,
949 exercise_style: ContractStyle::European,
950 binary_type,
951 cash,
952 }),
953 flat_5pct(),
954 )
955 }
956
957 fn binary_option(put_or_call: PutOrCall) -> EquityOption {
958 binary_option_of(put_or_call, BinaryType::CashOrNothing, 1.0)
959 }
960
961 fn flat_5pct() -> YieldCurve {
962 YieldCurve::flat(
963 0.05,
964 NaiveDate::from_ymd_opt(2026, 1, 1).unwrap(),
965 DayCountConvention::Act365,
966 Compounding::Continuous,
967 )
968 .unwrap()
969 }
970
971 #[test]
973 fn golden_call_npv_and_greeks() {
974 let option = test_option(PutOrCall::Call, flat_5pct());
975 assert_approx_eq!(option.npv(), 14.2312547860, 1e-8);
976 assert_approx_eq!(option.delta(), 0.6242517279, 1e-8);
977 assert_approx_eq!(option.gamma(), 0.0126477644, 1e-8);
978 assert_approx_eq!(option.vega(), 37.9432933117, 1e-8);
979 assert_approx_eq!(option.theta(), -8.1011898970, 1e-8);
980 assert_approx_eq!(option.rho(), 48.1939180046, 1e-8);
981 }
982
983 #[test]
984 fn golden_put_npv_and_greeks() {
985 let option = test_option(PutOrCall::Put, flat_5pct());
986 assert_approx_eq!(option.npv(), 9.3541972361, 1e-8);
987 assert_approx_eq!(option.delta(), -0.3757482721, 1e-8);
988 assert_approx_eq!(option.gamma(), 0.0126477644, 1e-8);
989 assert_approx_eq!(option.vega(), 37.9432933117, 1e-8);
990 assert_approx_eq!(option.theta(), -3.3450427745, 1e-8);
991 assert_approx_eq!(option.rho(), -46.9290244455, 1e-8);
992 }
993
994 #[test]
995 fn put_call_parity() {
996 let call = test_option(PutOrCall::Call, flat_5pct());
997 let put = test_option(PutOrCall::Put, flat_5pct());
998 let s = call.base.underlying_price.value();
999 let k_df = call.base.strike_price * call.base.maturity_discount_factor();
1000 assert_approx_eq!(call.npv() - put.npv(), s - k_df, 1e-10);
1001 }
1002
1003 #[test]
1005 fn golden_binary_call_npv_and_greeks() {
1006 let option = binary_option(PutOrCall::Call);
1007 assert_approx_eq!(option.npv(), 0.4819391800, 1e-8);
1008 assert_approx_eq!(option.delta(), 0.0126477644, 1e-8);
1009 assert_approx_eq!(option.gamma(), -0.0001335042, 1e-8);
1010 assert_approx_eq!(option.vega(), -0.4005125405, 1e-8);
1011 assert_approx_eq!(option.theta(), 0.0209350179, 1e-8);
1012 assert_approx_eq!(option.rho(), 0.7828372637, 1e-8);
1013 }
1014
1015 #[test]
1016 fn golden_binary_put_npv_and_greeks() {
1017 let option = binary_option(PutOrCall::Put);
1018 assert_approx_eq!(option.npv(), 0.4692902445, 1e-8);
1019 assert_approx_eq!(option.delta(), -0.0126477644, 1e-8);
1020 assert_approx_eq!(option.gamma(), 0.0001335042, 1e-8);
1021 assert_approx_eq!(option.vega(), 0.4005125405, 1e-8);
1022 assert_approx_eq!(option.theta(), 0.0266264533, 1e-8);
1023 assert_approx_eq!(option.rho(), -1.7340666882, 1e-8);
1024 }
1025
1026 #[test]
1027 fn binary_call_plus_put_equals_discount_factor() {
1028 let call = binary_option(PutOrCall::Call);
1029 let put = binary_option(PutOrCall::Put);
1030 assert_approx_eq!(call.npv() + put.npv(), call.base.maturity_discount_factor(), 1e-12);
1031 }
1032
1033 #[test]
1034 fn cash_amount_scales_cash_or_nothing_linearly() {
1035 let unit = binary_option(PutOrCall::Call);
1036 let sized = binary_option_of(PutOrCall::Call, BinaryType::CashOrNothing, 1000.0);
1037 assert_approx_eq!(sized.npv(), 1000.0 * unit.npv(), 1e-9);
1038 assert_approx_eq!(sized.delta(), 1000.0 * unit.delta(), 1e-9);
1039 assert_approx_eq!(sized.vega(), 1000.0 * unit.vega(), 1e-9);
1040 }
1041
1042 #[test]
1045 fn golden_asset_or_nothing_call_npv_and_greeks() {
1046 let mut option = binary_option_of(PutOrCall::Call, BinaryType::AssetOrNothing, 0.0);
1047 option.base.dividend_yield = 0.02;
1048 assert_approx_eq!(option.npv(), 58.6851146135, 1e-8);
1049 assert_approx_eq!(option.delta(), 1.8502230631, 1e-8);
1050 assert_approx_eq!(option.gamma(), 0.0021056199, 1e-8);
1051 assert_approx_eq!(option.vega(), 6.3168595850, 1e-8);
1052 assert_approx_eq!(option.theta(), -3.5639423965, 1e-8);
1053 assert_approx_eq!(option.rho(), 126.3371917001, 1e-8);
1054 }
1055
1056 #[test]
1057 fn golden_asset_or_nothing_put_npv_and_greeks() {
1058 let mut option = binary_option_of(PutOrCall::Put, BinaryType::AssetOrNothing, 0.0);
1059 option.base.dividend_yield = 0.02;
1060 assert_approx_eq!(option.npv(), 39.3347527172, 1e-8);
1061 assert_approx_eq!(option.delta(), -0.8700243898, 1e-8);
1062 assert_approx_eq!(option.gamma(), -0.0021056199, 1e-8);
1063 assert_approx_eq!(option.vega(), -6.3168595850, 1e-8);
1064 assert_approx_eq!(option.theta(), 5.5243397431, 1e-8);
1065 assert_approx_eq!(option.rho(), -126.3371917001, 1e-8);
1066 }
1067
1068 #[test]
1069 fn asset_call_plus_put_equals_forward_leg() {
1070 let call = binary_option_of(PutOrCall::Call, BinaryType::AssetOrNothing, 0.0);
1071 let put = binary_option_of(PutOrCall::Put, BinaryType::AssetOrNothing, 0.0);
1072 assert_approx_eq!(call.npv() + put.npv(), 100.0, 1e-10);
1074 }
1075
1076 #[test]
1081 fn asset_digital_replicated_by_call_plus_cash_digitals() {
1082 let k = 100.0;
1083 let asset = binary_option_of(PutOrCall::Call, BinaryType::AssetOrNothing, 0.0);
1084 let vanilla = test_option(PutOrCall::Call, flat_5pct());
1085 let cash = binary_option_of(PutOrCall::Call, BinaryType::CashOrNothing, k);
1086
1087 assert_approx_eq!(asset.npv(), vanilla.npv() + cash.npv(), 1e-10);
1088 assert_approx_eq!(asset.delta(), vanilla.delta() + cash.delta(), 1e-10);
1089 assert_approx_eq!(asset.gamma(), vanilla.gamma() + cash.gamma(), 1e-10);
1090 assert_approx_eq!(asset.vega(), vanilla.vega() + cash.vega(), 1e-10);
1091 assert_approx_eq!(asset.theta(), vanilla.theta() + cash.theta(), 1e-10);
1092 assert_approx_eq!(asset.rho(), vanilla.rho() + cash.rho(), 1e-10);
1093 }
1094
1095 #[test]
1098 fn asset_digital_replication_holds_across_engines() {
1099 let k = 100.0;
1100 let priced = |engine: Engine, payoff: Box<dyn Payoff>| {
1101 let mut option = test_option_with(payoff, flat_5pct());
1102 option.engine = engine.clone();
1103 option.npv()
1104 };
1105 let asset_payoff = || -> Box<dyn Payoff> {
1106 Box::new(BinaryPayoff {
1107 put_or_call: PutOrCall::Call,
1108 exercise_style: ContractStyle::European,
1109 binary_type: BinaryType::AssetOrNothing,
1110 cash: 0.0,
1111 })
1112 };
1113 let cash_payoff = || -> Box<dyn Payoff> {
1114 Box::new(BinaryPayoff {
1115 put_or_call: PutOrCall::Call,
1116 exercise_style: ContractStyle::European,
1117 binary_type: BinaryType::CashOrNothing,
1118 cash: k,
1119 })
1120 };
1121 let vanilla_payoff = || -> Box<dyn Payoff> {
1122 Box::new(VanillaPayoff {
1123 put_or_call: PutOrCall::Call,
1124 exercise_style: ContractStyle::European,
1125 })
1126 };
1127 for (engine, tol) in [
1128 (Engine::FiniteDifference, 0.01),
1129 (Engine::Binomial, 0.05),
1130 (Engine::MonteCarlo, 0.05),
1131 ] {
1132 let asset = priced(engine.clone(), asset_payoff());
1133 let replicated =
1134 priced(engine.clone(), vanilla_payoff()) + priced(engine.clone(), cash_payoff());
1135 assert!(
1136 (asset - replicated).abs() < tol,
1137 "{engine:?}: asset={asset} replicated={replicated}"
1138 );
1139 }
1140 }
1141
1142 #[test]
1143 fn asset_digital_matches_analytic_across_engines() {
1144 let analytic = binary_option_of(PutOrCall::Call, BinaryType::AssetOrNothing, 0.0).npv();
1145 for (engine, tol) in [
1146 (Engine::FiniteDifference, 0.05),
1147 (Engine::Binomial, 2.0), (Engine::MonteCarlo, 0.05),
1149 ] {
1150 let mut option = binary_option_of(PutOrCall::Call, BinaryType::AssetOrNothing, 0.0);
1151 option.engine = engine.clone();
1152 let value = option.npv();
1153 assert!(
1154 (value - analytic).abs() < tol,
1155 "{engine:?}: {value} vs analytic {analytic}"
1156 );
1157 }
1158 }
1159
1160 #[test]
1163 fn finite_difference_matches_analytic_vanilla() {
1164 for pc in [PutOrCall::Call, PutOrCall::Put] {
1165 let mut option = test_option(pc, flat_5pct());
1166 let analytic = option.npv();
1167 option.engine = Engine::FiniteDifference;
1168 let fd = option.npv();
1169 assert!(
1170 (fd - analytic).abs() < 0.01,
1171 "{pc:?}: fd={fd} analytic={analytic}"
1172 );
1173 }
1174 }
1175
1176 #[test]
1177 fn finite_difference_matches_analytic_binary() {
1178 for pc in [PutOrCall::Call, PutOrCall::Put] {
1179 let mut option = binary_option(pc);
1180 let analytic = option.npv();
1181 option.engine = Engine::FiniteDifference;
1182 let fd = option.npv();
1183 assert!(
1184 (fd - analytic).abs() < 0.002,
1185 "{pc:?}: fd={fd} analytic={analytic}"
1186 );
1187 }
1188 }
1189
1190 #[test]
1191 fn binomial_matches_analytic_vanilla_and_binary() {
1192 for pc in [PutOrCall::Call, PutOrCall::Put] {
1193 let mut vanilla = test_option(pc, flat_5pct());
1194 let analytic = vanilla.npv();
1195 vanilla.engine = Engine::Binomial;
1196 let tree = vanilla.npv();
1197 assert!((tree - analytic).abs() < 0.02, "vanilla {pc:?}: tree={tree} bs={analytic}");
1198
1199 let mut binary = binary_option(pc);
1200 let analytic = binary.npv();
1201 binary.engine = Engine::Binomial;
1202 let tree = binary.npv();
1203 assert!((tree - analytic).abs() < 0.02, "binary {pc:?}: tree={tree} bs={analytic}");
1204 }
1205 }
1206
1207 #[test]
1208 fn monte_carlo_matches_analytic_vanilla_and_binary() {
1209 for pc in [PutOrCall::Call, PutOrCall::Put] {
1211 let mut vanilla = test_option(pc, flat_5pct());
1212 let analytic = vanilla.npv();
1213 vanilla.engine = Engine::MonteCarlo;
1214 let mc = vanilla.npv();
1215 assert!((mc - analytic).abs() < 0.02, "vanilla {pc:?}: mc={mc} bs={analytic}");
1216
1217 let mut binary = binary_option(pc);
1218 let analytic = binary.npv();
1219 binary.engine = Engine::MonteCarlo;
1220 let mc = binary.npv();
1221 assert!((mc - analytic).abs() < 0.005, "binary {pc:?}: mc={mc} bs={analytic}");
1222 }
1223 }
1224
1225 #[test]
1226 fn monte_carlo_sobol_beats_default_tolerance_and_is_reproducible() {
1227 let mut option = test_option(PutOrCall::Call, flat_5pct());
1228 option.engine = Engine::MonteCarlo;
1229 let first = option.npv();
1230 let second = option.npv();
1231 assert_eq!(first, second, "deterministic sampler must reproduce exactly");
1232 assert!((first - 14.2312547860).abs() < 0.02, "sobol mc = {first}");
1233 }
1234
1235 #[test]
1236 fn monte_carlo_path_wise_starts_at_spot_and_schemes_converge() {
1237 let analytic = test_option(PutOrCall::Call, flat_5pct()).npv();
1240 for scheme in ["exact", "euler", "milstein"] {
1241 let mut option = test_option(PutOrCall::Call, flat_5pct());
1242 option.engine = Engine::MonteCarlo;
1243 option.mc.scheme = scheme.parse().unwrap();
1244 option.mc.time_steps = 252;
1245 option.mc.paths = 50_000;
1246 let mc = option.npv();
1247 assert!(
1248 (mc - analytic).abs() < 0.35,
1249 "{scheme}: mc={mc} analytic={analytic}"
1250 );
1251 }
1252 }
1253
1254 #[test]
1255 fn monte_carlo_greeks_match_analytic() {
1256 let mut option = test_option(PutOrCall::Call, flat_5pct());
1257 option.engine = Engine::MonteCarlo;
1258 assert!((option.delta() - 0.6242517279).abs() < 0.01, "delta {}", option.delta());
1260 assert!((option.gamma() - 0.0126477644).abs() < 0.003, "gamma {}", option.gamma());
1261 assert!((option.vega() - 37.9432933117).abs() < 1.0, "vega {}", option.vega());
1262 assert!((option.theta() - -8.1011898970).abs() < 0.5, "theta {}", option.theta());
1263 assert!((option.rho() - 48.1939180046).abs() < 0.5, "rho {}", option.rho());
1264 }
1265
1266 #[test]
1267 fn lsmc_american_put_close_to_tree_and_dominates_european() {
1268 let european = test_option(PutOrCall::Put, flat_5pct()).npv();
1269 let mut tree_option = test_option_with(
1270 Box::new(VanillaPayoff {
1271 put_or_call: PutOrCall::Put,
1272 exercise_style: ContractStyle::American,
1273 }),
1274 flat_5pct(),
1275 );
1276 tree_option.engine = Engine::Binomial;
1277 let tree = tree_option.npv();
1278
1279 let mut lsmc_option = test_option_with(
1280 Box::new(VanillaPayoff {
1281 put_or_call: PutOrCall::Put,
1282 exercise_style: ContractStyle::American,
1283 }),
1284 flat_5pct(),
1285 );
1286 lsmc_option.engine = Engine::MonteCarlo;
1287 lsmc_option.mc.paths = 20_000;
1288 let lsmc = lsmc_option.npv();
1289
1290 assert!(lsmc > european, "lsmc {lsmc} must exceed european {european}");
1293 assert!((lsmc - tree).abs() < 0.25, "lsmc={lsmc} tree={tree}");
1294 }
1295
1296 #[test]
1299 fn implied_vol_round_trips_across_strikes_and_vols() {
1300 let (s, r, q) = (100.0, 0.05, 0.02);
1301 for pc in [PutOrCall::Call, PutOrCall::Put] {
1302 for k in [50.0, 80.0, 100.0, 120.0, 200.0] {
1303 for vol in [0.05, 0.2, 0.6, 1.5] {
1304 for t in [0.05, 0.5, 2.0] {
1305 let price = bs_price(s, k, r, q, vol, t, pc);
1306 if price - bs_price(s, k, r, q, 0.0, t, pc) < 1e-10 {
1308 continue;
1309 }
1310 let iv = implied_vol_from_price(s, k, r, q, t, price, pc).unwrap();
1311 assert!(
1315 (iv - vol).abs() < 1e-5,
1316 "{pc:?} K={k} vol={vol} t={t}: recovered {iv}"
1317 );
1318 }
1319 }
1320 }
1321 }
1322 }
1323
1324 #[test]
1325 fn implied_vol_rejects_arbitrage_violating_prices() {
1326 assert!(implied_vol_from_price(100.0, 80.0, 0.05, 0.0, 1.0, 10.0, PutOrCall::Call)
1328 .is_err());
1329 assert!(implied_vol_from_price(100.0, 100.0, 0.05, 0.0, 1.0, 101.0, PutOrCall::Call)
1331 .is_err());
1332 }
1333
1334 fn smile_vol(k: f64, base: f64) -> f64 {
1338 base - 0.001 * (k - 100.0)
1339 }
1340
1341 fn quoted_option(
1342 k: f64,
1343 maturity: NaiveDate,
1344 market_price: f64,
1345 ) -> Box<EquityOption> {
1346 let mut option = test_option(PutOrCall::Call, flat_5pct());
1347 option.base.strike_price = k;
1348 option.base.maturity_date = maturity;
1349 option.base.current_price = Quote::new(market_price);
1350 Box::new(option)
1351 }
1352
1353 fn build_surface_from_quotes() -> crate::core::vols::VolSurface {
1354 let valuation = NaiveDate::from_ymd_opt(2026, 1, 1).unwrap();
1355 let maturities = [
1356 (NaiveDate::from_ymd_opt(2026, 7, 2).unwrap(), 0.23),
1357 (NaiveDate::from_ymd_opt(2027, 1, 1).unwrap(), 0.25),
1358 ];
1359 let mut quotes = Vec::new();
1360 for (maturity, base) in maturities {
1361 let t = (maturity - valuation).num_days() as f64 / 365.0;
1362 for i in 0..13 {
1363 let k = 70.0 + 5.0 * i as f64;
1364 let vol = smile_vol(k, base);
1365 let price = bs_price(100.0, k, 0.05, 0.0, vol, t, PutOrCall::Call);
1366 quotes.push(quoted_option(k, maturity, price));
1367 }
1368 }
1369 crate::equity::vol_surface::build_implied_vol_surface("es).unwrap()
1370 }
1371
1372 #[test]
1373 fn implied_surface_recovers_input_vols() {
1374 let surface = build_surface_from_quotes();
1375 for (t, base) in [(182.0 / 365.0, 0.23), (1.0, 0.25)] {
1377 for k in [70.0, 85.0, 100.0, 115.0, 130.0] {
1378 let vol = surface.vol(k, 100.0, t);
1379 assert!(
1380 (vol - smile_vol(k, base)).abs() < 1e-7,
1381 "K={k} t={t}: {vol} vs {}",
1382 smile_vol(k, base)
1383 );
1384 }
1385 }
1386 }
1387
1388 fn local_vol_option(surface: crate::core::vols::VolSurface, k: f64) -> EquityOption {
1389 let mut option = test_option(PutOrCall::Call, flat_5pct());
1390 option.base.strike_price = k;
1391 option.base.vol_surface = surface;
1392 option.engine = Engine::MonteCarlo;
1393 option.mc.model = crate::equity::montecarlo::McModel::LocalVol;
1394 option.mc.paths = 20_000;
1395 option
1396 }
1397
1398 #[test]
1399 fn local_vol_prices_back_vanilla_from_calibrated_surface() {
1400 let surface = build_surface_from_quotes();
1403 for k in [90.0, 100.0, 110.0] {
1404 let expected = bs_price(100.0, k, 0.05, 0.0, smile_vol(k, 0.25), 1.0, PutOrCall::Call);
1405 let lv_price = local_vol_option(surface.clone(), k).npv();
1406 assert!(
1407 (lv_price - expected).abs() < 0.3,
1408 "K={k}: local vol {lv_price} vs BS {expected}"
1409 );
1410 }
1411 }
1412
1413 #[test]
1414 fn local_vol_flat_surface_reproduces_black_scholes() {
1415 let valuation = NaiveDate::from_ymd_opt(2026, 1, 1).unwrap();
1416 let surface =
1417 crate::core::vols::VolSurface::flat(0.3, valuation, DayCountConvention::Act365)
1418 .unwrap();
1419 let expected = 14.2312547860; let lv_price = local_vol_option(surface, 100.0).npv();
1421 assert!((lv_price - expected).abs() < 0.3, "{lv_price} vs {expected}");
1422 }
1423
1424 #[test]
1425 fn local_vol_term_structure_reproduces_terminal_implied() {
1426 let valuation = NaiveDate::from_ymd_opt(2026, 1, 1).unwrap();
1430 let surface = crate::core::vols::VolSurface::from_strike_smiles(
1431 &[Tenor::YearFraction(0.5), Tenor::YearFraction(1.0)],
1432 &[vec![(100.0, 0.20)], vec![(100.0, 0.25)]],
1433 valuation,
1434 DayCountConvention::Act365,
1435 )
1436 .unwrap();
1437 let expected = bs_price(100.0, 100.0, 0.05, 0.0, 0.25, 1.0, PutOrCall::Call);
1438 let lv_price = local_vol_option(surface, 100.0).npv();
1439 assert!((lv_price - expected).abs() < 0.3, "{lv_price} vs {expected}");
1440 }
1441
1442 fn barrier_option(
1445 put_or_call: PutOrCall,
1446 direction: crate::equity::barrier::BarrierDirection,
1447 knock: crate::equity::barrier::KnockType,
1448 barrier: f64,
1449 ) -> EquityOption {
1450 let mut option = test_option_with(
1451 Box::new(BarrierPayoff {
1452 put_or_call,
1453 exercise_style: ContractStyle::European,
1454 direction,
1455 knock,
1456 barrier,
1457 }),
1458 flat_5pct(),
1459 );
1460 option.base.dividend_yield = 0.02; option
1462 }
1463
1464 #[test]
1465 fn golden_barrier_prices_all_eight_types() {
1466 use crate::equity::barrier::{BarrierDirection::*, KnockType::*};
1467 let cases = [
1470 (Down, In, PutOrCall::Call, 90.0, 4.5095197744),
1471 (Down, Out, PutOrCall::Call, 90.0, 8.5107614943),
1472 (Down, In, PutOrCall::Put, 90.0, 10.0710164338),
1473 (Down, Out, PutOrCall::Put, 90.0, 0.0523399543),
1474 (Up, In, PutOrCall::Call, 120.0, 12.5974705742),
1475 (Up, Out, PutOrCall::Call, 120.0, 0.4228106946),
1476 (Up, In, PutOrCall::Put, 120.0, 1.4297711810),
1477 (Up, Out, PutOrCall::Put, 120.0, 8.6935852071),
1478 ];
1479 for (direction, knock, pc, h, expected) in cases {
1480 let option = barrier_option(pc, direction, knock, h);
1481 assert_approx_eq!(option.npv(), expected, 1e-8);
1482 }
1483 }
1484
1485 #[test]
1486 fn barrier_greeks_satisfy_in_out_parity() {
1487 use crate::equity::barrier::{BarrierDirection::*, KnockType::*};
1488 let ki = barrier_option(PutOrCall::Call, Down, In, 90.0);
1490 let ko = barrier_option(PutOrCall::Call, Down, Out, 90.0);
1491 let mut vanilla = test_option(PutOrCall::Call, flat_5pct());
1492 vanilla.base.dividend_yield = 0.02;
1493 assert_approx_eq!(ki.npv() + ko.npv(), vanilla.npv(), 1e-10);
1494 assert_approx_eq!(ki.delta() + ko.delta(), vanilla.delta(), 1e-5);
1495 assert_approx_eq!(ki.gamma() + ko.gamma(), vanilla.gamma(), 1e-4);
1496 assert_approx_eq!(ki.vega() + ko.vega(), vanilla.vega(), 1e-4);
1497 assert_approx_eq!(ki.theta() + ko.theta(), vanilla.theta(), 1e-4);
1498 assert_approx_eq!(ki.rho() + ko.rho(), vanilla.rho(), 1e-4);
1499 }
1500
1501 #[test]
1502 fn monte_carlo_barrier_matches_analytic() {
1503 use crate::equity::barrier::{BarrierDirection::*, KnockType::*};
1504 let cases = [
1505 (Down, Out, PutOrCall::Call, 90.0),
1506 (Down, In, PutOrCall::Put, 90.0),
1507 (Up, Out, PutOrCall::Put, 120.0),
1508 (Up, In, PutOrCall::Call, 110.0),
1509 ];
1510 for (direction, knock, pc, h) in cases {
1511 let analytic = barrier_option(pc, direction, knock, h).npv();
1512 let mut option = barrier_option(pc, direction, knock, h);
1513 option.engine = Engine::MonteCarlo;
1514 option.mc.paths = 50_000;
1515 let mc = option.npv();
1516 assert!(
1517 (mc - analytic).abs() < 0.3,
1518 "{direction:?} {knock:?} {pc:?} H={h}: mc={mc} analytic={analytic}"
1519 );
1520 }
1521 }
1522
1523 fn asian_option(
1526 put_or_call: PutOrCall,
1527 averaging: crate::equity::asian::AveragingType,
1528 strike_type: crate::equity::asian::AsianStrikeType,
1529 ) -> EquityOption {
1530 let mut option = test_option_with(
1531 Box::new(AsianPayoff {
1532 put_or_call,
1533 exercise_style: ContractStyle::European,
1534 averaging,
1535 strike_type,
1536 }),
1537 flat_5pct(),
1538 );
1539 option.base.dividend_yield = 0.02; option
1541 }
1542
1543 #[test]
1544 fn golden_asian_analytic_prices() {
1545 use crate::equity::asian::{AsianStrikeType::*, AveragingType::*};
1546 let geo = asian_option(PutOrCall::Call, Geometric, FixedStrike);
1548 assert_approx_eq!(geo.npv(), 6.953600, 1e-5);
1549 let arith = asian_option(PutOrCall::Call, Arithmetic, FixedStrike);
1550 assert_approx_eq!(arith.npv(), 7.409272, 1e-5);
1551 }
1552
1553 #[test]
1554 fn geometric_asian_mc_matches_discrete_closed_form() {
1555 use crate::equity::asian::{AsianStrikeType::*, AveragingType::*};
1556 let mut option = asian_option(PutOrCall::Call, Geometric, FixedStrike);
1557 option.engine = Engine::MonteCarlo;
1558 option.mc.paths = 50_000;
1559 let mc = option.npv(); let closed = crate::equity::asian::geometric_asian_price(
1561 100.0, 100.0, 0.05, 0.02, 0.3, 1.0, Some(100), PutOrCall::Call,
1562 );
1563 assert!((mc - closed).abs() < 0.15, "mc={mc} closed={closed}");
1564 }
1565
1566 #[test]
1567 fn arithmetic_asian_cv_mc_close_to_turnbull_wakeman() {
1568 use crate::equity::asian::{AsianStrikeType::*, AveragingType::*};
1569 let analytic = asian_option(PutOrCall::Call, Arithmetic, FixedStrike).npv();
1570 let mut option = asian_option(PutOrCall::Call, Arithmetic, FixedStrike);
1571 option.engine = Engine::MonteCarlo;
1572 option.mc.paths = 50_000;
1573 let mc = option.npv(); assert!((mc - analytic).abs() < 0.08, "cv-mc={mc} tw={analytic}");
1578 }
1579
1580 #[test]
1581 fn arithmetic_average_dominates_geometric_on_same_paths() {
1582 use crate::equity::asian::{AsianStrikeType::*, AveragingType::*};
1583 let price_mc = |averaging| {
1584 let mut option = asian_option(PutOrCall::Call, averaging, FixedStrike);
1585 option.engine = Engine::MonteCarlo;
1586 option.mc.paths = 20_000;
1587 option.mc.scheme = crate::equity::montecarlo::DiscretizationScheme::Euler;
1590 option.mc.time_steps = 100;
1591 option.npv()
1592 };
1593 assert!(price_mc(Arithmetic) > price_mc(Geometric), "AM-GM inequality");
1594 }
1595
1596 #[test]
1597 fn floating_strike_asian_prices_on_mc_only() {
1598 use crate::equity::asian::{AsianStrikeType::*, AveragingType::*};
1599 let mut option = asian_option(PutOrCall::Call, Arithmetic, FloatingStrike);
1600 option.engine = Engine::MonteCarlo;
1601 option.mc.paths = 20_000;
1602 let price = option.npv();
1603 let vanilla = test_option(PutOrCall::Call, flat_5pct()).npv();
1605 assert!(price > 0.0 && price < vanilla, "{price}");
1606 }
1607
1608 #[test]
1609 #[should_panic(expected = "no analytic pricer")]
1610 fn analytic_engine_rejects_floating_strike_asian() {
1611 use crate::equity::asian::{AsianStrikeType::*, AveragingType::*};
1612 asian_option(PutOrCall::Call, Arithmetic, FloatingStrike).npv();
1613 }
1614
1615 #[test]
1618 fn fd_grid_greeks_match_analytic_for_european() {
1619 let mut option = test_option(PutOrCall::Call, flat_5pct());
1620 option.engine = Engine::FiniteDifference;
1621 assert!((option.delta() - 0.6242517279).abs() < 1e-3, "delta {}", option.delta());
1622 assert!((option.gamma() - 0.0126477644).abs() < 1e-4, "gamma {}", option.gamma());
1623 assert!((option.theta() - -8.1011898970).abs() < 0.03, "theta {}", option.theta());
1624 assert!((option.vega() - 37.9432933117).abs() < 0.05, "vega {}", option.vega());
1625 assert!((option.rho() - 48.1939180046).abs() < 0.05, "rho {}", option.rho());
1626 }
1627
1628 #[test]
1629 fn fd_american_put_greeks_differ_from_european_correctly() {
1630 let mut american = test_option_with(
1631 Box::new(VanillaPayoff {
1632 put_or_call: PutOrCall::Put,
1633 exercise_style: ContractStyle::American,
1634 }),
1635 flat_5pct(),
1636 );
1637 american.engine = Engine::FiniteDifference;
1638 let european_delta = -0.3757482721; assert!(
1642 american.delta() < european_delta,
1643 "american delta {} vs european {european_delta}",
1644 american.delta()
1645 );
1646 assert!(american.npv() > test_option(PutOrCall::Put, flat_5pct()).npv());
1647 }
1648
1649 #[test]
1650 fn fd_brennan_schwartz_american_matches_tree() {
1651 let mut fd = test_option_with(
1652 Box::new(VanillaPayoff {
1653 put_or_call: PutOrCall::Put,
1654 exercise_style: ContractStyle::American,
1655 }),
1656 flat_5pct(),
1657 );
1658 fd.engine = Engine::FiniteDifference;
1659 let mut tree = test_option_with(
1660 Box::new(VanillaPayoff {
1661 put_or_call: PutOrCall::Put,
1662 exercise_style: ContractStyle::American,
1663 }),
1664 flat_5pct(),
1665 );
1666 tree.engine = Engine::Binomial;
1667 assert!((fd.npv() - tree.npv()).abs() < 0.02, "fd={} tree={}", fd.npv(), tree.npv());
1668 }
1669
1670 #[test]
1671 fn fd_barrier_matches_reiner_rubinstein() {
1672 use crate::equity::barrier::{BarrierDirection::*, KnockType::*};
1673 for (direction, knock, pc, h) in [
1674 (Down, Out, PutOrCall::Call, 90.0),
1675 (Down, In, PutOrCall::Call, 90.0),
1676 (Up, Out, PutOrCall::Put, 120.0),
1677 (Up, In, PutOrCall::Put, 120.0),
1678 ] {
1679 let analytic = barrier_option(pc, direction, knock, h).npv();
1680 let mut option = barrier_option(pc, direction, knock, h);
1681 option.engine = Engine::FiniteDifference;
1682 let fd = option.npv();
1683 assert!(
1684 (fd - analytic).abs() < 0.02,
1685 "{direction:?} {knock:?} {pc:?} H={h}: fd={fd} analytic={analytic}"
1686 );
1687 }
1688 }
1689
1690 #[test]
1691 fn fd_barrier_in_out_parity_on_grid() {
1692 use crate::equity::barrier::{BarrierDirection::*, KnockType::*};
1693 let mut ki = barrier_option(PutOrCall::Call, Down, In, 90.0);
1694 let mut ko = barrier_option(PutOrCall::Call, Down, Out, 90.0);
1695 ki.engine = Engine::FiniteDifference;
1696 ko.engine = Engine::FiniteDifference;
1697 let mut vanilla = test_option(PutOrCall::Call, flat_5pct());
1698 vanilla.base.dividend_yield = 0.02;
1699 vanilla.engine = Engine::FiniteDifference;
1700 assert!((ki.npv() + ko.npv() - vanilla.npv()).abs() < 1e-9);
1701 assert!((ki.delta() + ko.delta() - vanilla.delta()).abs() < 1e-9);
1702 }
1703
1704 #[test]
1705 fn fd_local_vol_flat_surface_matches_black_scholes() {
1706 let mut option = test_option(PutOrCall::Call, flat_5pct());
1707 option.engine = Engine::FiniteDifference;
1708 option.mc.model = crate::equity::montecarlo::McModel::LocalVol;
1709 assert_approx_eq!(option.npv(), 14.2312547860, 5e-3);
1711 }
1712
1713 #[test]
1714 fn fd_grid_is_configurable() {
1715 let mut coarse = test_option(PutOrCall::Call, flat_5pct());
1716 coarse.engine = Engine::FiniteDifference;
1717 coarse.fd.spot_steps = 100;
1718 coarse.fd.time_steps = 50;
1719 assert!((coarse.npv() - 14.2312547860).abs() < 0.02, "{}", coarse.npv());
1721 }
1722
1723 #[test]
1726 fn qmc_path_wise_prices_accurately() {
1727 let mut option = test_option(PutOrCall::Call, flat_5pct());
1729 option.engine = Engine::MonteCarlo;
1730 option.mc.time_steps = 64;
1731 option.mc.paths = 20_000;
1732 let qmc = option.npv();
1733 assert!((qmc - 14.2312547860).abs() < 0.05, "qmc path-wise {qmc}");
1734 }
1735
1736 #[test]
1737 fn mc_stats_reports_consistent_standard_error() {
1738 let mut option = test_option(PutOrCall::Call, flat_5pct());
1739 option.engine = Engine::MonteCarlo;
1740 option.mc.sampler = crate::equity::montecarlo::Sampler::PseudoRandom;
1741 let stats = crate::equity::montecarlo::npv_with_stats(&option);
1742 assert!(stats.std_err > 0.0 && stats.std_err < 1.0);
1743 assert!(stats.paths == 100_000 && stats.steps == 1);
1744 assert!(
1747 (stats.pv - 14.2312547860).abs() < 5.0 * stats.std_err,
1748 "pv={} stderr={}",
1749 stats.pv,
1750 stats.std_err
1751 );
1752 }
1753
1754 #[test]
1755 fn parallel_paths_are_bit_reproducible() {
1756 for sampler in
1757 [crate::equity::montecarlo::Sampler::Sobol, crate::equity::montecarlo::Sampler::PseudoRandom]
1758 {
1759 let mut option = test_option(PutOrCall::Call, flat_5pct());
1760 option.engine = Engine::MonteCarlo;
1761 option.mc.sampler = sampler;
1762 option.mc.time_steps = 32;
1763 option.mc.paths = 30_000;
1764 assert_eq!(option.npv(), option.npv());
1765 }
1766 }
1767
1768 fn heston_option(payoff: Box<dyn Payoff>) -> EquityOption {
1771 let mut option = test_option_with(payoff, flat_5pct());
1772 option.base.dividend_yield = 0.02;
1773 option.mc.model = crate::equity::montecarlo::McModel::Heston;
1774 option.heston = Some(crate::equity::heston::HestonParams {
1775 v0: 0.09,
1776 kappa: 2.0,
1777 theta: 0.09,
1778 vol_of_vol: 0.4,
1779 rho: -0.7,
1780 });
1781 option
1782 }
1783
1784 fn heston_vanilla(pc: PutOrCall) -> EquityOption {
1785 heston_option(Box::new(VanillaPayoff {
1786 put_or_call: pc,
1787 exercise_style: ContractStyle::European,
1788 }))
1789 }
1790
1791 #[test]
1792 fn heston_mc_matches_semi_analytic() {
1793 for pc in [PutOrCall::Call, PutOrCall::Put] {
1794 let analytic = heston_vanilla(pc).npv();
1795 let mut mc = heston_vanilla(pc);
1796 mc.engine = Engine::MonteCarlo;
1797 mc.mc.paths = 50_000;
1798 let mc_price = mc.npv();
1799 assert!(
1801 (mc_price - analytic).abs() < 0.15,
1802 "{pc:?}: mc={mc_price} analytic={analytic}"
1803 );
1804 }
1805 }
1806
1807 #[test]
1808 fn heston_binary_mc_matches_semi_analytic() {
1809 let payoff = || -> Box<dyn Payoff> {
1810 Box::new(BinaryPayoff {
1811 put_or_call: PutOrCall::Call,
1812 exercise_style: ContractStyle::European,
1813 binary_type: BinaryType::CashOrNothing,
1814 cash: 1.0,
1815 })
1816 };
1817 let analytic = heston_option(payoff()).npv();
1818 let mut mc = heston_option(payoff());
1819 mc.engine = Engine::MonteCarlo;
1820 mc.mc.paths = 50_000;
1821 assert!((mc.npv() - analytic).abs() < 0.01, "mc={} analytic={analytic}", mc.npv());
1822 }
1823
1824 #[test]
1825 fn heston_greeks_are_consistent() {
1826 let call = heston_vanilla(PutOrCall::Call);
1827 let put = heston_vanilla(PutOrCall::Put);
1828 let dfq = (-0.02_f64).exp();
1830 assert!((call.delta() - put.delta() - dfq).abs() < 1e-4);
1831 assert!((call.gamma() - put.gamma()).abs() < 1e-6);
1833 assert!((call.vega() - put.vega()).abs() < 1e-4);
1834 assert!(call.vega() > 0.0);
1835 }
1836
1837 #[test]
1838 fn heston_barrier_and_asian_price_on_mc() {
1839 use crate::equity::barrier::{BarrierDirection::*, KnockType::*};
1841 let vanilla = {
1842 let mut o = heston_vanilla(PutOrCall::Call);
1843 o.engine = Engine::MonteCarlo;
1844 o.mc.paths = 20_000;
1845 o.npv()
1846 };
1847 let mut ko = heston_option(Box::new(BarrierPayoff {
1848 put_or_call: PutOrCall::Call,
1849 exercise_style: ContractStyle::European,
1850 direction: Down,
1851 knock: Out,
1852 barrier: 90.0,
1853 }));
1854 ko.engine = Engine::MonteCarlo;
1855 ko.mc.paths = 20_000;
1856 let ko_price = ko.npv();
1857 assert!(ko_price > 0.0 && ko_price < vanilla, "ko={ko_price} vanilla={vanilla}");
1858
1859 let mut asian = heston_option(Box::new(AsianPayoff {
1860 put_or_call: PutOrCall::Call,
1861 exercise_style: ContractStyle::European,
1862 averaging: crate::equity::asian::AveragingType::Arithmetic,
1863 strike_type: crate::equity::asian::AsianStrikeType::FixedStrike,
1864 }));
1865 asian.engine = Engine::MonteCarlo;
1866 asian.mc.paths = 20_000;
1867 let asian_price = asian.npv();
1868 assert!(asian_price > 0.0 && asian_price < vanilla);
1869 }
1870
1871 #[test]
1874 fn borrow_cost_is_equivalent_to_extra_dividend_yield() {
1875 for engine in [Engine::BlackScholes, Engine::FiniteDifference, Engine::MonteCarlo] {
1876 let mut with_borrow = test_option(PutOrCall::Call, flat_5pct());
1877 with_borrow.base.dividend_yield = 0.01;
1878 with_borrow.base.borrow_cost = 0.03;
1879 with_borrow.engine = engine.clone();
1880 let mut with_yield = test_option(PutOrCall::Call, flat_5pct());
1881 with_yield.base.dividend_yield = 0.04;
1882 with_yield.engine = engine.clone();
1883 assert!(
1884 (with_borrow.npv() - with_yield.npv()).abs() < 1e-12,
1885 "{engine:?}: borrow {} vs yield {}",
1886 with_borrow.npv(),
1887 with_yield.npv()
1888 );
1889 }
1890 }
1891
1892 fn dividend_paying_option(pc: PutOrCall) -> EquityOption {
1893 let mut option = test_option(pc, flat_5pct());
1894 option.base.cash_dividends =
1895 vec![(NaiveDate::from_ymd_opt(2026, 7, 1).unwrap(), 3.0)];
1896 option
1897 }
1898
1899 #[test]
1900 fn cash_dividend_prices_as_escrowed_spot_analytically() {
1901 let option = dividend_paying_option(PutOrCall::Call);
1902 let t_div = (NaiveDate::from_ymd_opt(2026, 7, 1).unwrap()
1903 - NaiveDate::from_ymd_opt(2026, 1, 1).unwrap())
1904 .num_days() as f64
1905 / 365.0;
1906 let s_eff = 100.0 - 3.0 * (-0.05 * t_div).exp();
1907 assert!((option.base.effective_spot() - s_eff).abs() < 1e-10);
1908 let expected = bs_price(s_eff, 100.0, 0.05, 0.0, 0.3, 1.0, PutOrCall::Call);
1909 assert_approx_eq!(option.npv(), expected, 1e-10);
1910 }
1911
1912 #[test]
1913 fn put_call_parity_with_dividends_and_borrow() {
1914 let mut call = dividend_paying_option(PutOrCall::Call);
1915 let mut put = dividend_paying_option(PutOrCall::Put);
1916 call.base.borrow_cost = 0.02;
1917 put.base.borrow_cost = 0.02;
1918 let parity = call.base.effective_spot() * (-call.base.carry_yield()).exp()
1919 - 100.0 * (-0.05_f64).exp();
1920 assert_approx_eq!(call.npv() - put.npv(), parity, 1e-10);
1921 }
1922
1923 #[test]
1924 fn mc_dividend_jumps_close_to_escrowed_analytic() {
1925 let analytic = dividend_paying_option(PutOrCall::Call).npv();
1929 let mut mc = dividend_paying_option(PutOrCall::Call);
1930 mc.engine = Engine::MonteCarlo;
1931 mc.mc.time_steps = 100;
1932 mc.mc.paths = 50_000;
1933 assert!((mc.npv() - analytic).abs() < 0.3, "mc={} analytic={analytic}", mc.npv());
1934 }
1935
1936 #[test]
1937 fn fd_dividend_jump_condition_consistent_with_mc_jump_model() {
1938 let mut fd = dividend_paying_option(PutOrCall::Call);
1943 fd.engine = Engine::FiniteDifference;
1944 let mut mc = dividend_paying_option(PutOrCall::Call);
1945 mc.engine = Engine::MonteCarlo;
1946 mc.mc.time_steps = 100;
1947 mc.mc.paths = 50_000;
1948 assert!((fd.npv() - mc.npv()).abs() < 0.1, "fd={} mc={}", fd.npv(), mc.npv());
1949 let escrowed = dividend_paying_option(PutOrCall::Call).npv();
1950 assert!((fd.npv() - escrowed).abs() < 0.3, "fd={} escrowed={escrowed}", fd.npv());
1951 }
1952
1953 #[test]
1954 fn forward_price_reflects_borrow_and_cash_dividends() {
1955 let mut option = dividend_paying_option(PutOrCall::Call);
1956 option.base.borrow_cost = 0.02;
1957 let expected = option.base.effective_spot() * ((0.05 - 0.02) * 1.0_f64).exp();
1958 assert_approx_eq!(option.base.forward_price(), expected, 1e-10);
1959 }
1960
1961 #[test]
1962 fn cash_dividend_with_carry_discounts_at_net_carry() {
1963 let carry = 0.03;
1968 let mut option = dividend_paying_option(PutOrCall::Call);
1969 option.base.borrow_cost = carry;
1970 let (r, s, d, t) = (0.05, 100.0, 3.0, 1.0);
1971 let t_div = (NaiveDate::from_ymd_opt(2026, 7, 1).unwrap()
1972 - NaiveDate::from_ymd_opt(2026, 1, 1).unwrap())
1973 .num_days() as f64
1974 / 365.0;
1975
1976 let s_eff = s - d * (-(r - carry) * t_div).exp();
1977 assert_approx_eq!(option.base.effective_spot(), s_eff, 1e-10);
1978
1979 let jump_forward = s_eff * ((r - carry) * t).exp();
1980 assert_approx_eq!(option.base.forward_price(), jump_forward, 1e-10);
1981 }
1982
1983 #[test]
1984 fn net_carry_discounting_flows_through_to_price_and_stays_near_jump_engines() {
1985 let carry = 0.03;
1995 let mut analytic = dividend_paying_option(PutOrCall::Call);
1996 analytic.base.borrow_cost = carry;
1997 let a = analytic.npv();
1998
1999 let expected =
2000 bs_price(analytic.base.effective_spot(), 100.0, 0.05, carry, 0.3, 1.0, PutOrCall::Call);
2001 assert_approx_eq!(a, expected, 1e-10);
2002
2003 let mut fd = dividend_paying_option(PutOrCall::Call);
2004 fd.base.borrow_cost = carry;
2005 fd.engine = Engine::FiniteDifference;
2006 assert!((a - fd.npv()).abs() < 0.2, "analytic {a} vs fd {}", fd.npv());
2007 }
2008
2009 fn futures_option(
2012 pc: PutOrCall,
2013 settlement: crate::equity::black76::FuturesSettlement,
2014 ) -> EquityOption {
2015 crate::equity::builder::EquityOptionBuilder::new()
2016 .symbol("FUT")
2017 .spot(100.0) .strike(100.0)
2019 .flat_vol(0.30)
2020 .flat_rate(0.05)
2021 .valuation_date(NaiveDate::from_ymd_opt(2026, 1, 1).unwrap())
2022 .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
2023 .vanilla(pc)
2024 .on_future(settlement)
2025 .engine(Engine::BlackScholes)
2026 .build()
2027 }
2028
2029 #[test]
2030 fn black76_option_api_matches_closed_form() {
2031 use crate::equity::black76::FuturesSettlement::*;
2032 for (settlement, gold) in [(Discounted, 11.34202064), (Margined, 11.92353847)] {
2033 let option = futures_option(PutOrCall::Call, settlement);
2034 assert_approx_eq!(option.npv(), gold, 1e-7);
2035 }
2036 let call = futures_option(PutOrCall::Call, Discounted);
2038 assert_approx_eq!(call.delta(), 0.53232482, 1e-7);
2039 assert_approx_eq!(call.rho(), -11.34202064, 1e-6);
2040 }
2041
2042 #[test]
2043 fn margined_futures_option_has_zero_rho_and_exceeds_discounted() {
2044 use crate::equity::black76::FuturesSettlement::*;
2045 let disc = futures_option(PutOrCall::Call, Discounted).npv();
2046 let marg = futures_option(PutOrCall::Call, Margined);
2047 assert_eq!(marg.rho(), 0.0);
2048 assert!(marg.npv() > disc);
2049 assert_approx_eq!(marg.npv(), disc * (0.05_f64).exp(), 1e-9);
2050 }
2051
2052 #[test]
2053 fn black76_on_the_forward_equals_spot_black_scholes() {
2054 let (s, q, r, t): (f64, f64, f64, f64) = (100.0, 0.02, 0.05, 1.0);
2057 let fwd = s * ((r - q) * t).exp();
2058 let futures_opt = crate::equity::builder::EquityOptionBuilder::new()
2059 .spot(fwd)
2060 .strike(100.0)
2061 .flat_vol(0.30)
2062 .flat_rate(r)
2063 .valuation_date(NaiveDate::from_ymd_opt(2026, 1, 1).unwrap())
2064 .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
2065 .vanilla(PutOrCall::Call)
2066 .on_future(crate::equity::black76::FuturesSettlement::Discounted)
2067 .build();
2068 let spot_opt = crate::equity::builder::EquityOptionBuilder::new()
2069 .spot(s)
2070 .strike(100.0)
2071 .flat_vol(0.30)
2072 .flat_rate(r)
2073 .dividend_yield(q)
2074 .valuation_date(NaiveDate::from_ymd_opt(2026, 1, 1).unwrap())
2075 .maturity_date(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap())
2076 .vanilla(PutOrCall::Call)
2077 .build();
2078 assert_approx_eq!(futures_opt.npv(), spot_opt.npv(), 1e-10);
2079 }
2080
2081 #[test]
2082 fn put_call_parity_on_futures_both_styles() {
2083 use crate::equity::black76::FuturesSettlement::*;
2084 for (settlement, df) in [(Discounted, (-0.05_f64).exp()), (Margined, 1.0)] {
2085 let c = futures_option(PutOrCall::Call, settlement).npv();
2086 let p = futures_option(PutOrCall::Put, settlement).npv();
2087 assert_approx_eq!(c - p, df * (100.0 - 100.0), 1e-10);
2089 }
2090 }
2091
2092 #[test]
2093 #[should_panic(expected = "Options on futures (Black-76) price on the Analytical engine only")]
2094 fn futures_option_rejects_non_analytic_engine() {
2095 let mut option =
2096 futures_option(PutOrCall::Call, crate::equity::black76::FuturesSettlement::Discounted);
2097 option.engine = Engine::MonteCarlo;
2098 option.npv();
2099 }
2100
2101 fn forward_start_option(pc: PutOrCall) -> EquityOption {
2104 test_option_with(
2105 Box::new(crate::equity::forward_start_option::ForwardStartPayoff {
2106 put_or_call: pc,
2107 exercise_style: ContractStyle::European,
2108 strike_fraction: 1.0,
2109 start_fraction: 0.5,
2110 }),
2111 flat_5pct(),
2112 )
2113 }
2114
2115 #[test]
2116 fn forward_start_analytic_matches_monte_carlo() {
2117 let analytic = forward_start_option(PutOrCall::Call).npv();
2118 let mut mc = forward_start_option(PutOrCall::Call);
2119 mc.engine = Engine::MonteCarlo;
2120 mc.mc.paths = 50_000;
2121 assert!((mc.npv() - analytic).abs() < 0.15, "mc={} analytic={analytic}", mc.npv());
2122 }
2123
2124 #[test]
2125 fn forward_start_heston_degenerates_to_black_scholes() {
2126 let bs = forward_start_option(PutOrCall::Call).npv();
2127 let mut heston = forward_start_option(PutOrCall::Call);
2128 heston.engine = Engine::MonteCarlo;
2129 heston.mc.model = crate::equity::montecarlo::McModel::Heston;
2130 heston.mc.paths = 50_000;
2131 heston.heston = Some(crate::equity::heston::HestonParams {
2132 v0: 0.09,
2133 kappa: 1.0,
2134 theta: 0.09,
2135 vol_of_vol: 1e-3,
2136 rho: 0.0,
2137 });
2138 assert!((heston.npv() - bs).abs() < 0.2, "heston={} bs={bs}", heston.npv());
2139 }
2140
2141 fn autocall_note(autocall_barrier: f64, protection_barrier: f64, coupon: f64) -> EquityOption {
2144 let mut option = test_option_with(
2145 Box::new(crate::equity::autocallable::AutocallablePayoff {
2146 exercise_style: ContractStyle::European,
2147 autocall_barrier,
2148 protection_barrier,
2149 coupon,
2150 observations: 4,
2151 notional: 100.0,
2152 initial_fixing: 100.0,
2153 }),
2154 flat_5pct(),
2155 );
2156 option.engine = Engine::MonteCarlo;
2157 option.mc.paths = 20_000;
2158 option
2159 }
2160
2161 #[test]
2162 fn autocall_that_always_calls_pays_coupon_at_first_observation() {
2163 let note = autocall_note(1e-9, 50.0, 5.0);
2165 let stats = crate::equity::montecarlo::npv_with_stats(¬e);
2166 let expected = 105.0 * (-0.05 * 0.25_f64).exp();
2167 assert_approx_eq!(stats.pv, expected, 1e-9);
2168 assert!(stats.std_err < 1e-6, "deterministic payoff: stderr {}", stats.std_err);
2170 }
2171
2172 #[test]
2173 fn autocall_never_called_with_full_protection_is_a_zero_coupon_bond() {
2174 let note = autocall_note(1e12, 1e-9, 5.0);
2175 let expected = 100.0 * (-0.05_f64).exp();
2176 assert_approx_eq!(note.npv(), expected, 1e-9);
2177 }
2178
2179 #[test]
2180 fn autocall_full_downside_is_the_discounted_forward() {
2181 let note = autocall_note(1e12, 1e12, 0.0);
2184 assert!((note.npv() - 100.0).abs() < 0.3, "{}", note.npv());
2185 }
2186
2187 #[test]
2188 fn autocall_value_increases_with_coupon_and_lower_protection() {
2189 let base = autocall_note(105.0, 70.0, 5.0).npv();
2190 assert!(autocall_note(105.0, 70.0, 8.0).npv() > base, "higher coupon");
2191 assert!(autocall_note(105.0, 50.0, 5.0).npv() > base, "lower knock-in barrier");
2192 }
2193
2194 #[test]
2195 fn autocall_prices_under_local_vol() {
2196 let gbm = autocall_note(105.0, 70.0, 5.0).npv();
2198 let mut lv = autocall_note(105.0, 70.0, 5.0);
2199 lv.mc.model = crate::equity::montecarlo::McModel::LocalVol;
2200 assert!((lv.npv() - gbm).abs() < 0.5, "lv={} gbm={gbm}", lv.npv());
2201 }
2202
2203 #[test]
2204 #[should_panic(expected = "Autocallables price on the MonteCarlo engine only")]
2205 fn analytic_engine_rejects_autocallables() {
2206 let mut note = autocall_note(105.0, 70.0, 5.0);
2207 note.engine = Engine::BlackScholes;
2208 note.npv();
2209 }
2210
2211 #[test]
2212 #[should_panic(expected = "only barriers price on the FD")]
2213 fn fd_engine_rejects_forward_start() {
2214 let mut option = forward_start_option(PutOrCall::Call);
2215 option.engine = Engine::FiniteDifference;
2216 option.npv();
2217 }
2218
2219 #[test]
2220 #[should_panic(expected = "Heston model is supported on the Analytical and MonteCarlo")]
2221 fn fd_engine_rejects_heston() {
2222 let mut option = heston_vanilla(PutOrCall::Call);
2223 option.engine = Engine::FiniteDifference;
2224 option.npv();
2225 }
2226
2227 #[test]
2228 #[should_panic(expected = "not supported on the Binomial engine")]
2229 fn tree_engine_rejects_barrier_options() {
2230 use crate::equity::barrier::{BarrierDirection::*, KnockType::*};
2231 let mut option = barrier_option(PutOrCall::Call, Down, Out, 90.0);
2232 option.engine = Engine::Binomial;
2233 option.npv();
2234 }
2235
2236 #[test]
2237 #[should_panic(expected = "Analytical engine cannot price American")]
2238 fn analytic_engine_rejects_american_exercise() {
2239 let option = test_option_with(
2240 Box::new(VanillaPayoff {
2241 put_or_call: PutOrCall::Put,
2242 exercise_style: ContractStyle::American,
2243 }),
2244 flat_5pct(),
2245 );
2246 option.npv();
2247 }
2248
2249 #[test]
2250 fn american_put_fd_and_tree_agree_and_dominate_european() {
2251 let european_put = test_option(PutOrCall::Put, flat_5pct()).npv();
2252 let american = |engine: Engine| {
2253 let mut option = test_option_with(
2254 Box::new(VanillaPayoff {
2255 put_or_call: PutOrCall::Put,
2256 exercise_style: ContractStyle::American,
2257 }),
2258 flat_5pct(),
2259 );
2260 option.engine = engine;
2261 option.npv()
2262 };
2263 let fd = american(Engine::FiniteDifference);
2264 let tree = american(Engine::Binomial);
2265 assert!(fd > european_put, "american {fd} must exceed european {european_put}");
2266 assert!(tree > european_put);
2267 assert!((fd - tree).abs() < 0.02, "fd={fd} tree={tree}");
2268 }
2269
2270 #[test]
2271 fn smile_surface_prices_with_interpolated_vol() {
2272 let valuation_date = NaiveDate::from_ymd_opt(2026, 1, 1).unwrap();
2276 let surface = crate::core::vols::VolSurface::from_strike_grid(
2277 &[Tenor::YearFraction(1.0), Tenor::YearFraction(2.0)],
2278 &[90.0, 100.0, 110.0],
2279 &[vec![0.32, 0.30, 0.28], vec![0.36, 0.34, 0.32]],
2280 valuation_date,
2281 DayCountConvention::Act365,
2282 )
2283 .unwrap();
2284 let mut option = test_option(PutOrCall::Call, flat_5pct());
2285 option.base.vol_surface = surface;
2286 assert_approx_eq!(option.base.volatility(), 0.30, 1e-14);
2287 assert_approx_eq!(option.npv(), 14.2312547860, 1e-8);
2288 assert_approx_eq!(option.vega(), 37.9432933117, 1e-8);
2289 option.base.strike_price = 95.0;
2291 assert_approx_eq!(option.base.volatility(), 0.31, 1e-14);
2292 }
2293
2294 #[test]
2295 fn implied_vol_recovers_input_vol() {
2296 let mut option = test_option(PutOrCall::Call, flat_5pct());
2297 let target_price = option.npv(); option.base.vol_surface = crate::core::vols::VolSurface::flat(
2300 0.6,
2301 option.base.valuation_date,
2302 DayCountConvention::Act365,
2303 )
2304 .unwrap();
2305 let iv = option.imp_vol(target_price);
2306 assert_approx_eq!(iv, 0.30, 1e-10);
2307 }
2308
2309 #[test]
2310 fn zero_curve_prices_off_maturity_pillar() {
2311 let curve = YieldCurve::from_zero_rates(
2314 &[Tenor::YearFraction(0.5), Tenor::YearFraction(1.0), Tenor::YearFraction(2.0)],
2315 &[0.02, 0.05, 0.07],
2316 NaiveDate::from_ymd_opt(2026, 1, 1).unwrap(),
2317 DayCountConvention::Act365,
2318 Compounding::Continuous,
2319 InterpolationMethod::LogLinearDf,
2320 )
2321 .unwrap();
2322 let option = test_option(PutOrCall::Call, curve);
2323 assert_approx_eq!(option.npv(), 14.2312547860, 1e-8);
2324 assert_approx_eq!(option.base.risk_free_rate(), 0.05, 1e-12);
2325 }
2326}