1use std::sync::Arc;
2use chrono::NaiveDate;
3use crate::equity::{baw,bjerksund_stensland,binomial,finite_difference,greeks,montecarlo};
4use crate::core::curves::{Compounding, YieldCurve};
5use crate::core::vols::VolSurface;
6use crate::equity::asian::{AsianStrikeType, AveragingType};
7use crate::equity::barrier::{BarrierDirection, KnockType};
8use crate::equity::builder::EquityOptionBuilder;
9use crate::equity::heston;
10use super::super::core::quotes::Quote;
11use super::super::core::traits::Instrument;
12use super::blackscholes;
13use crate::equity::utils::{Engine, Model, Payoff, PayoffType, PricingEngine, LongShort};
14use crate::core::trade::PutOrCall;
15use crate::core::utils::ContractStyle;
16use blackscholes::BlackScholesPricer;
17use crate::core::data_models::EquityOptionData;
18use crate::core::errors::RustyQLibError;
19use crate::core::results::PricingResult;
20
21#[derive(Debug, Clone)]
22pub struct VanillaPayoff {
23 pub put_or_call: PutOrCall,
24 pub exercise_style: ContractStyle,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum BinaryType {
30 CashOrNothing,
32 AssetOrNothing,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum LookbackType {
40 FloatingStrike,
41 FixedStrike,
42}
43
44#[derive(Debug, Clone)]
49pub struct LookbackPayoff {
50 pub put_or_call: PutOrCall,
51 pub exercise_style: ContractStyle,
52 pub lookback_type: LookbackType,
53}
54
55impl Payoff for LookbackPayoff {
56 fn payoff(&self, _spot: f64, _strike: f64) -> f64 {
58 0.0
59 }
60 fn path_payoff(&self, path: &[f64], strike: f64) -> f64 {
61 let terminal = *path.last().expect("empty path");
62 let max = path.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
63 let min = path.iter().cloned().fold(f64::INFINITY, f64::min);
64 match (self.lookback_type, &self.put_or_call) {
65 (LookbackType::FloatingStrike, PutOrCall::Call) => terminal - min,
66 (LookbackType::FloatingStrike, PutOrCall::Put) => max - terminal,
67 (LookbackType::FixedStrike, PutOrCall::Call) => (max - strike).max(0.0),
68 (LookbackType::FixedStrike, PutOrCall::Put) => (strike - min).max(0.0),
69 }
70 }
71 fn path_payoff_var<'t>(
72 &self,
73 path: &[crate::core::aad::Var<'t>],
74 strike: f64,
75 ) -> Option<crate::core::aad::Var<'t>> {
76 let terminal = *path.last().expect("empty path");
77 let mut max = path[0];
78 let mut min = path[0];
79 for s in &path[1..] {
80 max = max.max(*s);
81 min = min.min(*s);
82 }
83 Some(match (self.lookback_type, &self.put_or_call) {
84 (LookbackType::FloatingStrike, PutOrCall::Call) => terminal - min,
85 (LookbackType::FloatingStrike, PutOrCall::Put) => max - terminal,
86 (LookbackType::FixedStrike, PutOrCall::Call) => (max - strike).maxf(0.0),
87 (LookbackType::FixedStrike, PutOrCall::Put) => (strike - min).maxf(0.0),
88 })
89 }
90 fn is_path_dependent(&self) -> bool {
91 true
92 }
93 fn payoff_kind(&self) -> PayoffType {
94 PayoffType::Lookback
95 }
96 fn put_or_call(&self) -> &PutOrCall {
97 &self.put_or_call
98 }
99 fn exercise_style(&self) -> &ContractStyle {
100 &self.exercise_style
101 }
102 fn as_any(&self) -> &dyn std::any::Any {
103 self
104 }
105 fn clone_box(&self) -> Box<dyn Payoff> {
106 Box::new(self.clone())
107 }
108}
109
110#[derive(Debug, Clone)]
111pub struct BinaryPayoff {
112 pub put_or_call: PutOrCall,
113 pub exercise_style: ContractStyle,
114 pub binary_type: BinaryType,
115 pub cash: f64,
117}
118#[derive(Debug, Clone)]
119pub struct BarrierPayoff {
120 pub put_or_call: PutOrCall,
121 pub exercise_style: ContractStyle,
122 pub direction: BarrierDirection,
123 pub knock: KnockType,
124 pub barrier: f64,
125 pub barrier2: Option<f64>,
128 pub rebate: f64,
131 pub rebate_at_hit: bool,
134}
135
136impl Payoff for BarrierPayoff {
142 fn payoff(&self, spot: f64, strike: f64) -> f64 {
143 match &self.put_or_call {
144 PutOrCall::Call => (spot - strike).max(0.0),
145 PutOrCall::Put => (strike - spot).max(0.0),
146 }
147 }
148 fn path_payoff(&self, path: &[f64], strike: f64) -> f64 {
149 let crossed = match self.barrier2 {
150 Some(b2) => {
151 let (lo, hi) = (self.barrier.min(b2), self.barrier.max(b2));
152 path.iter().any(|&s| s <= lo || s >= hi)
153 }
154 None => path.iter().any(|&s| match self.direction {
155 BarrierDirection::Up => s >= self.barrier,
156 BarrierDirection::Down => s <= self.barrier,
157 }),
158 };
159 let alive = match self.knock {
160 KnockType::Out => !crossed,
161 KnockType::In => crossed,
162 };
163 let rebate = match self.knock {
166 KnockType::Out if crossed => self.rebate,
167 KnockType::In if !crossed => self.rebate,
168 _ => 0.0,
169 };
170 let payoff = if alive {
171 self.payoff(*path.last().expect("empty path"), strike)
172 } else {
173 0.0
174 };
175 payoff + rebate
176 }
177 fn is_path_dependent(&self) -> bool {
178 true
179 }
180 fn payoff_kind(&self) -> PayoffType {
181 PayoffType::Barrier
182 }
183 fn put_or_call(&self) -> &PutOrCall {
184 &self.put_or_call
185 }
186 fn exercise_style(&self) -> &ContractStyle {
187 &self.exercise_style
188 }
189 fn as_any(&self) -> &dyn std::any::Any {
190 self
191 }
192 fn clone_box(&self) -> Box<dyn Payoff> {
193 Box::new(self.clone())
194 }
195}
196#[derive(Debug, Clone)]
197pub struct AsianPayoff {
198 pub put_or_call: PutOrCall,
199 pub exercise_style: ContractStyle,
200 pub averaging: AveragingType,
201 pub strike_type: AsianStrikeType,
202}
203
204impl Payoff for AsianPayoff {
209 fn payoff(&self, spot: f64, strike: f64) -> f64 {
212 match &self.put_or_call {
213 PutOrCall::Call => (spot - strike).max(0.0),
214 PutOrCall::Put => (strike - spot).max(0.0),
215 }
216 }
217 fn path_payoff(&self, path: &[f64], strike: f64) -> f64 {
218 let n = path.len() as f64;
219 let average = match self.averaging {
220 AveragingType::Arithmetic => path.iter().sum::<f64>() / n,
221 AveragingType::Geometric => (path.iter().map(|s| s.ln()).sum::<f64>() / n).exp(),
222 };
223 let terminal = *path.last().expect("empty path");
224 let (long_leg, short_leg) = match self.strike_type {
225 AsianStrikeType::FixedStrike => (average, strike),
226 AsianStrikeType::FloatingStrike => (terminal, average),
227 };
228 match &self.put_or_call {
229 PutOrCall::Call => (long_leg - short_leg).max(0.0),
230 PutOrCall::Put => (short_leg - long_leg).max(0.0),
231 }
232 }
233 fn path_payoff_var<'t>(
234 &self,
235 path: &[crate::core::aad::Var<'t>],
236 strike: f64,
237 ) -> Option<crate::core::aad::Var<'t>> {
238 let n = path.len() as f64;
239 let average = match self.averaging {
240 AveragingType::Arithmetic => {
241 let mut sum = path[0];
242 for s in &path[1..] {
243 sum = sum + *s;
244 }
245 sum / n
246 }
247 AveragingType::Geometric => {
248 let mut sum = path[0].ln();
249 for s in &path[1..] {
250 sum = sum + s.ln();
251 }
252 (sum / n).exp()
253 }
254 };
255 let terminal = *path.last().expect("empty path");
256 Some(match (self.strike_type, &self.put_or_call) {
257 (AsianStrikeType::FixedStrike, PutOrCall::Call) => (average - strike).maxf(0.0),
258 (AsianStrikeType::FixedStrike, PutOrCall::Put) => (strike - average).maxf(0.0),
259 (AsianStrikeType::FloatingStrike, PutOrCall::Call) => (terminal - average).maxf(0.0),
260 (AsianStrikeType::FloatingStrike, PutOrCall::Put) => (average - terminal).maxf(0.0),
261 })
262 }
263 fn is_path_dependent(&self) -> bool {
264 true
265 }
266 fn payoff_kind(&self) -> PayoffType {
267 PayoffType::Asian
268 }
269 fn put_or_call(&self) -> &PutOrCall {
270 &self.put_or_call
271 }
272 fn exercise_style(&self) -> &ContractStyle {
273 &self.exercise_style
274 }
275 fn as_any(&self) -> &dyn std::any::Any {
276 self
277 }
278 fn clone_box(&self) -> Box<dyn Payoff> {
279 Box::new(self.clone())
280 }
281}
282impl Payoff for VanillaPayoff {
283 fn payoff(&self, spot: f64, strike: f64) -> f64 {
284 match &self.put_or_call {
285 PutOrCall::Call => (spot - strike).max(0.0),
286 PutOrCall::Put => (strike - spot).max(0.0),
287 }
288 }
289 fn path_payoff_var<'t>(
290 &self,
291 path: &[crate::core::aad::Var<'t>],
292 strike: f64,
293 ) -> Option<crate::core::aad::Var<'t>> {
294 let terminal = *path.last().expect("empty path");
295 Some(match self.put_or_call {
296 PutOrCall::Call => (terminal - strike).maxf(0.0),
297 PutOrCall::Put => (strike - terminal).maxf(0.0),
298 })
299 }
300 fn payoff_kind(&self) -> PayoffType {
301 PayoffType::Vanilla
302 }
303 fn put_or_call(&self) -> &PutOrCall {
304 &self.put_or_call
305 }
306 fn exercise_style(&self) -> &ContractStyle {
307 &self.exercise_style
308 }
309 fn as_any(&self) -> &dyn std::any::Any {
310 self
311 }
312 fn clone_box(&self) -> Box<dyn Payoff> {
313 Box::new(self.clone())
314 }
315}
316
317impl Payoff for BinaryPayoff {
320 fn payoff(&self, spot: f64, strike: f64) -> f64 {
321 let in_the_money = match &self.put_or_call {
322 PutOrCall::Call => spot > strike,
323 PutOrCall::Put => spot < strike,
324 };
325 if !in_the_money {
326 return 0.0;
327 }
328 match self.binary_type {
329 BinaryType::CashOrNothing => self.cash,
330 BinaryType::AssetOrNothing => spot,
331 }
332 }
333 fn payoff_kind(&self) -> PayoffType {
334 PayoffType::Binary
335 }
336 fn put_or_call(&self) -> &PutOrCall {
337 &self.put_or_call
338 }
339 fn exercise_style(&self) -> &ContractStyle {
340 &self.exercise_style
341 }
342 fn as_any(&self) -> &dyn std::any::Any {
343 self
344 }
345 fn clone_box(&self) -> Box<dyn Payoff> {
346 Box::new(self.clone())
347 }
348}
349
350#[derive(Debug, Clone)]
354pub struct EquityOptionBase {
355 pub symbol: String,
356 pub currency: Option<String>,
357 pub exchange: Option<String>,
358 pub name: Option<String>,
359 pub cusip: Option<String>,
360 pub isin: Option<String>,
361 pub settlement_type: Option<String>,
362
363 pub strike_price: f64,
364 pub maturity_date: NaiveDate,
365 pub futures_settlement: Option<crate::equity::black76::FuturesSettlement>,
370 pub multiplier: f64,
371
372 pub current_price: Quote,
374 pub entry_price: f64,
375 pub long_short: LongShort,
376}
377
378#[derive(Debug, Clone)]
384pub struct EquityMarketData {
385 pub valuation_date: NaiveDate,
387 pub spot: Quote,
388 pub dividend_yield: f64,
389 pub borrow_cost: f64,
392 pub cash_dividends: Vec<(NaiveDate, f64)>,
397 pub vol_surface: Arc<VolSurface>,
403 pub discount_curve: Arc<YieldCurve>,
407}
408
409#[derive(Debug)]
410pub struct EquityOption {
411 pub base: EquityOptionBase,
413 pub market: EquityMarketData,
415 pub payoff: Box<dyn Payoff>,
416 pub engine: PricingEngine,
418 pub model: Model,
421}
422
423impl Clone for EquityOption {
426 fn clone(&self) -> Self {
427 EquityOption {
428 base: self.base.clone(),
429 market: self.market.clone(),
430 payoff: self.payoff.clone_box(),
431 engine: self.engine,
432 model: self.model,
433 }
434 }
435}
436
437impl EquityOption {
438 pub(crate) fn mc_cfg(&self) -> &montecarlo::MonteCarloConfig {
441 match &self.engine {
442 PricingEngine::MonteCarlo(cfg) => cfg,
443 _ => unreachable!("Monte Carlo code path reached on a non-MC engine"),
444 }
445 }
446
447 pub(crate) fn fd_cfg(&self) -> &finite_difference::FdConfig {
448 match &self.engine {
449 PricingEngine::FiniteDifference(cfg) => cfg,
450 _ => unreachable!("finite-difference code path reached on a non-FD engine"),
451 }
452 }
453
454 pub(crate) fn heston_params(&self) -> &crate::equity::heston::HestonParams {
457 match &self.model {
458 Model::Heston(hp) => hp,
459 _ => unreachable!("Heston code path reached on a non-Heston model"),
460 }
461 }
462
463 pub(crate) fn lattice_cfg(&self) -> &crate::core::lattice::LatticeConfig {
464 match &self.engine {
465 PricingEngine::Binomial(cfg) => cfg,
466 _ => unreachable!("lattice code path reached on a non-Binomial engine"),
467 }
468 }
469
470 #[cfg(test)]
473 pub(crate) fn mc_cfg_mut(&mut self) -> &mut montecarlo::MonteCarloConfig {
474 match &mut self.engine {
475 PricingEngine::MonteCarlo(cfg) => cfg,
476 _ => unreachable!("Monte Carlo code path reached on a non-MC engine"),
477 }
478 }
479
480 #[cfg(test)]
481 pub(crate) fn fd_cfg_mut(&mut self) -> &mut finite_difference::FdConfig {
482 match &mut self.engine {
483 PricingEngine::FiniteDifference(cfg) => cfg,
484 _ => unreachable!("finite-difference code path reached on a non-FD engine"),
485 }
486 }
487}
488impl EquityOption {
489
490 pub fn from_json(data: &EquityOptionData) -> Box<EquityOption> {
494 Self::try_from_json(data).unwrap_or_else(|e| panic!("{e}"))
495 }
496
497 pub fn try_from_json(data: &EquityOptionData) -> Result<Box<EquityOption>, RustyQLibError> {
505 let valuation_date =
506 crate::core::data_models::parse_valuation_date(data.base.valuation_date.as_deref())?;
507 let maturity_date = NaiveDate::parse_from_str(&data.maturity, "%Y-%m-%d")
508 .map_err(|_| RustyQLibError::invalid_input(
509 "maturity",
510 format!("invalid date '{}' (expected YYYY-MM-DD)", data.maturity),
511 ))?;
512 let payoff_type = data.payoff_type.parse::<PayoffType>()
513 .map_err(|_| RustyQLibError::invalid_input(
514 "payoff_type",
515 format!("unknown payoff_type '{}'", data.payoff_type),
516 ))?;
517 let strike_price = match payoff_type {
518 PayoffType::ForwardStart | PayoffType::Autocallable => {
520 data.strike_price.unwrap_or(0.0)
521 }
522 _ => data.strike_price.ok_or_else(|| RustyQLibError::invalid_input(
523 "strike_price",
524 "strike_price is required for this payoff",
525 ))?,
526 };
527
528 let mut builder = EquityOptionBuilder::new()
529 .symbol(&data.base.symbol)
530 .spot(data.base.underlying_price)
531 .strike(strike_price)
532 .valuation_date(valuation_date)
533 .maturity_date(maturity_date)
534 .dividend_yield(data.dividend.unwrap_or(0.0))
535 .borrow_cost(data.base.borrow_cost.unwrap_or(0.0));
536
537 builder = match &data.discount_curve {
539 Some(input) => builder.discount_curve(YieldCurve::from_input(input, valuation_date)?),
540 None => builder.flat_rate(data.base.risk_free_rate.unwrap_or(0.0)),
541 };
542 builder = match &data.vol_surface {
543 Some(input) => builder.vol_surface(VolSurface::from_input(input, valuation_date)?),
544 None => builder.flat_vol(data.volatility.ok_or_else(|| {
545 RustyQLibError::invalid_input(
546 "volatility",
547 "either volatility or vol_surface must be provided",
548 )
549 })?),
550 };
551 for d in data.cash_dividends.as_deref().unwrap_or(&[]) {
552 let date = NaiveDate::parse_from_str(&d.date, "%Y-%m-%d")
553 .map_err(|_| RustyQLibError::invalid_input(
554 "cash_dividends",
555 format!("invalid dividend date '{}' (expected YYYY-MM-DD)", d.date),
556 ))?;
557 builder = builder.cash_dividend(date, d.amount);
558 }
559 if let Some(s) = data.futures_settlement.as_deref() {
560 let settlement = s
561 .parse::<crate::equity::black76::FuturesSettlement>()
562 .map_err(|_| RustyQLibError::invalid_input(
563 "futures_settlement",
564 format!("invalid futures_settlement '{s}' (use 'discounted' or 'margined')"),
565 ))?;
566 builder = builder.on_future(settlement);
567 }
568
569 builder = match data.exercise_style.as_deref().unwrap_or("European").trim() {
571 "American" | "american" => builder.american(),
572 "Bermudan" | "bermudan" => {
573 let dates = data.exercise_dates.as_deref().ok_or_else(|| {
574 RustyQLibError::invalid_input(
575 "exercise_dates",
576 "exercise_dates is required when exercise_style is Bermudan",
577 )
578 })?;
579 builder.bermudan(parse_date_list("exercise_dates", dates)?)
580 }
581 _ => builder,
583 };
584
585 let side = match data.put_or_call.trim() {
586 "C" | "c" | "Call" | "call" => PutOrCall::Call,
587 "P" | "p" | "Put" | "put" => PutOrCall::Put,
588 other => return Err(RustyQLibError::invalid_input(
589 "put_or_call",
590 format!("invalid side '{other}' (use 'C' or 'P')"),
591 )),
592 };
593
594 builder = match payoff_type {
596 PayoffType::Accumulator => {
599 return Err(RustyQLibError::invalid_input(
600 "payoff_type",
601 "accumulators are built through EquityOptionBuilder::accumulator, \
602 not JSON contract data",
603 ));
604 }
605 PayoffType::Vanilla => builder.vanilla(side),
606 PayoffType::Binary => {
607 let binary_type = match data
608 .binary_type
609 .as_deref()
610 .unwrap_or("cash")
611 .trim()
612 .to_lowercase()
613 .as_str()
614 {
615 "cash" | "cash_or_nothing" | "cash-or-nothing" => BinaryType::CashOrNothing,
616 "asset" | "asset_or_nothing" | "asset-or-nothing" => BinaryType::AssetOrNothing,
617 other => return Err(RustyQLibError::invalid_input(
618 "binary_type",
619 format!("invalid binary_type '{other}' (use 'cash' or 'asset')"),
620 )),
621 };
622 builder.binary(side, binary_type, data.cash_amount.unwrap_or(1.0))
623 }
624 PayoffType::Lookback => {
625 let lookback_type = match data
626 .lookback_type
627 .as_deref()
628 .unwrap_or("floating")
629 .trim()
630 .to_lowercase()
631 .as_str()
632 {
633 "floating" | "floating_strike" => LookbackType::FloatingStrike,
634 "fixed" | "fixed_strike" => LookbackType::FixedStrike,
635 other => return Err(RustyQLibError::invalid_input(
636 "lookback_type",
637 format!("invalid lookback_type '{other}' (use 'floating' or 'fixed')"),
638 )),
639 };
640 builder.lookback(side, lookback_type)
641 }
642 PayoffType::Barrier => {
643 let barrier = data
644 .barrier_level
645 .ok_or_else(|| RustyQLibError::invalid_input(
646 "barrier_level",
647 "barrier_level is required for barrier options",
648 ))?;
649 let (direction, knock) = match data
650 .barrier_type
651 .as_deref()
652 .unwrap_or("")
653 .trim()
654 .to_lowercase()
655 .as_str()
656 {
657 "up_in" | "up-in" | "ui" => (BarrierDirection::Up, KnockType::In),
658 "up_out" | "up-out" | "uo" => (BarrierDirection::Up, KnockType::Out),
659 "down_in" | "down-in" | "di" => (BarrierDirection::Down, KnockType::In),
660 "down_out" | "down-out" | "do" => (BarrierDirection::Down, KnockType::Out),
661 other => return Err(RustyQLibError::invalid_input(
662 "barrier_type",
663 format!("barrier_type must be up_in/up_out/down_in/down_out, got '{other}'"),
664 )),
665 };
666 let b = match data.barrier_level2 {
669 Some(b2) => builder.double_barrier(
670 side,
671 knock,
672 barrier.min(b2),
673 barrier.max(b2),
674 ),
675 None => builder.barrier(side, direction, knock, barrier),
676 };
677 b.barrier_rebate(
678 data.rebate.unwrap_or(0.0),
679 data.rebate_at_hit.unwrap_or(false),
680 )
681 }
682 PayoffType::Asian => {
683 let averaging = match data
684 .averaging_type
685 .as_deref()
686 .unwrap_or("arithmetic")
687 .trim()
688 .to_lowercase()
689 .as_str()
690 {
691 "arithmetic" | "arith" => AveragingType::Arithmetic,
692 "geometric" | "geo" => AveragingType::Geometric,
693 other => return Err(RustyQLibError::invalid_input(
694 "averaging_type",
695 format!("averaging_type must be arithmetic or geometric, got '{other}'"),
696 )),
697 };
698 let strike_type = match data
699 .asian_strike_type
700 .as_deref()
701 .unwrap_or("fixed")
702 .trim()
703 .to_lowercase()
704 .as_str()
705 {
706 "fixed" | "average_price" => AsianStrikeType::FixedStrike,
707 "floating" | "average_strike" => AsianStrikeType::FloatingStrike,
708 other => return Err(RustyQLibError::invalid_input(
709 "asian_strike_type",
710 format!("asian_strike_type must be fixed or floating, got '{other}'"),
711 )),
712 };
713 builder.asian(side, averaging, strike_type)
714 }
715 PayoffType::ForwardStart => {
716 let start_date_str = data
717 .forward_start_date
718 .as_ref()
719 .ok_or_else(|| RustyQLibError::invalid_input(
720 "forward_start_date",
721 "forward_start_date is required for forward-start options",
722 ))?;
723 let start_date = NaiveDate::parse_from_str(start_date_str, "%Y-%m-%d")
724 .map_err(|_| RustyQLibError::invalid_input(
725 "forward_start_date",
726 format!("invalid date '{start_date_str}' (expected YYYY-MM-DD)"),
727 ))?;
728 if !(start_date > valuation_date && start_date < maturity_date) {
729 return Err(RustyQLibError::invalid_input(
730 "forward_start_date",
731 "forward_start_date must lie between valuation and maturity",
732 ));
733 }
734 let start_fraction = (start_date - valuation_date).num_days() as f64
735 / (maturity_date - valuation_date).num_days() as f64;
736 builder.forward_start(
737 side,
738 data.strike_fraction.unwrap_or(1.0),
739 start_fraction,
740 )
741 }
742 PayoffType::Autocallable => {
743 let autocall_barrier = data
744 .autocall_barrier
745 .ok_or_else(|| RustyQLibError::invalid_input(
746 "autocall_barrier",
747 "autocall_barrier is required for autocallables",
748 ))?;
749 let protection_barrier = data
750 .protection_barrier
751 .ok_or_else(|| RustyQLibError::invalid_input(
752 "protection_barrier",
753 "protection_barrier is required for autocallables",
754 ))?;
755 let coupon = data.autocall_coupon.unwrap_or(0.0);
756 let observations = data.autocall_observations.unwrap_or(4).max(1);
757 let notional = data.notional.unwrap_or(100.0);
758 let mut b = match data.coupon_barrier {
761 Some(coupon_barrier) => builder.phoenix(
762 autocall_barrier,
763 coupon_barrier,
764 protection_barrier,
765 coupon,
766 observations,
767 notional,
768 data.coupon_memory.unwrap_or(false),
769 ),
770 None => builder.autocallable(
771 autocall_barrier,
772 protection_barrier,
773 coupon,
774 observations,
775 notional,
776 ),
777 };
778 if let Some(dates) = data.autocall_observation_dates.as_deref() {
779 b = b.autocall_observation_dates(parse_date_list(
780 "autocall_observation_dates",
781 dates,
782 )?);
783 }
784 b
785 }
786 };
787
788 let engine_kind = match data.pricer.as_ref().map_or("Analytical", |v| v).trim() {
790 "Analytical" | "analytical" | "bs" => Engine::BlackScholes,
791 "MonteCarlo" | "montecarlo" | "MC" | "mc" => Engine::MonteCarlo,
792 "Binomial" | "binomial" | "bino" => Engine::Binomial,
793 "FiniteDifference" | "finitdifference" | "FD" | "fd" => Engine::FiniteDifference,
794 "BaroneAdesiWhaley" | "baw" | "BAW" => Engine::BaroneAdesiWhaley,
795 "BjerksundStensland" | "bjerksund_stensland" | "bs2002" | "BS2002" => {
796 Engine::BjerksundStensland
797 }
798 other => {
799 return Err(RustyQLibError::invalid_input(
800 "pricer",
801 format!(
802 "unknown pricer '{other}' (use Analytical, MonteCarlo, Binomial, \
803 FiniteDifference, BAW or BS2002)"
804 ),
805 ));
806 }
807 };
808 builder = match &engine_kind {
809 Engine::MonteCarlo => {
810 builder.mc_config(montecarlo::MonteCarloConfig::from_data(data)?)
811 }
812 Engine::FiniteDifference => {
813 builder.fd_config(finite_difference::FdConfig::from_data(data))
814 }
815 Engine::Binomial => {
816 let defaults = crate::core::lattice::LatticeConfig::default();
817 builder.lattice_config(crate::core::lattice::LatticeConfig {
818 tree_type: match data.tree_type.as_deref() {
819 Some(s) => s.parse()?,
820 None => defaults.tree_type,
821 },
822 steps: data.tree_steps.unwrap_or(defaults.steps),
823 term_structure: data.tree_term_structure.unwrap_or(false),
824 })
825 }
826 _ => builder,
827 };
828 builder = builder
829 .engine(engine_kind)
830 .model(Model::from_contract(data.mc_model.as_deref(), data.heston)?);
831
832 let mut option = builder.build()?;
833
834 option.base.currency = data.base.currency.clone();
836 option.base.exchange = data.base.exchange.clone();
837 option.base.name = data.base.name.clone();
838 option.base.cusip = data.base.cusip.clone();
839 option.base.isin = data.base.isin.clone();
840 option.base.settlement_type = data.base.settlement_type.clone();
841 option.base.multiplier = data.multiplier.unwrap_or(1.0);
842 option.base.current_price = Quote::new(data.current_price.unwrap_or(0.0));
843 option.base.entry_price = data.entry_price.unwrap_or(0.0);
844 Ok(Box::new(option))
845 }
846}
847
848fn parse_date_list(field: &str, dates: &[String]) -> Result<Vec<NaiveDate>, RustyQLibError> {
852 dates
853 .iter()
854 .map(|s| {
855 NaiveDate::parse_from_str(s, "%Y-%m-%d").map_err(|_| {
856 RustyQLibError::invalid_input(
857 field,
858 format!("invalid date '{s}' (expected YYYY-MM-DD)"),
859 )
860 })
861 })
862 .collect()
863}
864
865impl EquityOptionBase {
866 pub fn is_futures_option(&self) -> bool {
868 self.futures_settlement.is_some()
869 }
870 pub fn currency_code(&self) -> &str {
876 self.currency.as_deref().unwrap_or(crate::core::market::DEFAULT_CURRENCY)
877 }
878}
879
880impl EquityOption {
884 pub fn time_to_maturity(&self) -> f64 {
885 (self.base.maturity_date - self.market.valuation_date).num_days() as f64 / 365.0
886 }
887 pub fn maturity_discount_factor(&self) -> f64 {
889 self.market.discount_curve.df(self.time_to_maturity())
890 }
891 pub fn risk_free_rate(&self) -> f64 {
895 self.market
896 .discount_curve
897 .zero_rate_with(self.time_to_maturity(), Compounding::Continuous)
898 }
899 pub fn carry_yield(&self) -> f64 {
902 self.market.dividend_yield + self.market.borrow_cost
903 }
904 pub fn pv_cash_dividends(&self) -> f64 {
917 let carry = self.carry_yield();
918 self.market
919 .cash_dividends
920 .iter()
921 .filter(|(date, _)| {
922 *date > self.market.valuation_date && *date <= self.base.maturity_date
923 })
924 .map(|(date, amount)| {
925 let t = (*date - self.market.valuation_date).num_days() as f64 / 365.0;
926 amount * self.market.discount_curve.df(t) * (carry * t).exp()
929 })
930 .sum()
931 }
932 pub fn effective_spot(&self) -> f64 {
936 let s = self.market.spot.value() - self.pv_cash_dividends();
937 assert!(s > 0.0, "cash dividends exceed the spot price");
938 s
939 }
940 pub fn forward_price(&self) -> f64 {
943 let t = self.time_to_maturity();
944 self.effective_spot() * ((self.risk_free_rate() - self.carry_yield()) * t).exp()
945 }
946 pub fn volatility(&self) -> f64 {
949 self.market
950 .vol_surface
951 .vol(self.base.strike_price, self.forward_price(), self.time_to_maturity())
952 }
953 pub fn d1(&self) -> f64 {
954 let volatility = self.volatility();
956 let d1_numerator = (self.effective_spot() / self.base.strike_price).ln()
957 + (self.risk_free_rate() - self.carry_yield() + 0.5 * volatility.powi(2))
958 * self.time_to_maturity();
959 let d1_denominator = volatility * (self.time_to_maturity().sqrt());
960 d1_numerator / d1_denominator
961 }
962 pub fn d2(&self) -> f64 {
963 self.d1() - self.volatility() * self.time_to_maturity().sqrt()
964 }
965}
966impl EquityOption {
967 pub fn get_premium_at_risk(&self) -> f64 {
968 let value = self.npv();
969 let pay_off =
970 self.payoff.payoff_amount(self.market.spot.value(), self.base.strike_price);
971 if pay_off > 0.0 {
972 return value - pay_off;
973 } else {
974 return value;
975 }
976 }
977
978 pub fn try_imp_vol(&self, option_price: f64) -> Result<f64, RustyQLibError> {
981 blackscholes::implied_vol_from_price(
982 self.effective_spot(),
983 self.base.strike_price,
984 self.risk_free_rate(),
985 self.carry_yield(),
986 self.time_to_maturity(),
987 option_price,
988 *self.payoff.put_or_call(),
989 )
990 }
991 pub fn imp_vol(&mut self,option_price:f64) -> f64 {
995 let vol = self.try_imp_vol(option_price).expect("implied vol solve failed");
996 self.set_flat_vol(vol.max(1e-8));
997 vol
998 }
999 pub fn get_imp_vol(&mut self) -> f64 {
1000 let target = self.base.current_price.mid();
1001 self.imp_vol(target)
1002 }
1003 fn set_flat_vol(&mut self, vol: f64) {
1004 self.market.vol_surface = Arc::new(
1005 VolSurface::flat(
1006 vol,
1007 self.market.vol_surface.reference_date(),
1008 self.market.vol_surface.day_count(),
1009 )
1010 .expect("vol must be positive"),
1011 );
1012 }
1013}
1014
1015
1016impl EquityOption {
1017 pub(crate) fn check_engine_support(&self) -> Result<(), RustyQLibError> {
1020 let unsupported = |msg: &str| Err(RustyQLibError::UnsupportedEngine(msg.to_string()));
1021 let bermudan = matches!(self.payoff.exercise_style(), ContractStyle::Bermudan(_));
1022 let american =
1024 matches!(self.payoff.exercise_style(), ContractStyle::American) || bermudan;
1025 if self.base.is_futures_option() {
1026 if !matches!(self.engine, PricingEngine::BlackScholes) {
1027 return unsupported(
1028 "Options on futures (Black-76) price on the Analytical engine only",
1029 );
1030 }
1031 if american {
1032 return unsupported("Black-76 supports European exercise only");
1033 }
1034 }
1035 if self.payoff.is_path_dependent() {
1036 if american {
1037 return unsupported(
1038 "early-exercise (American/Bermudan) path-dependent options are not supported yet",
1039 );
1040 }
1041 if matches!(self.engine, PricingEngine::Binomial(_)) {
1042 return unsupported(
1043 "Path-dependent payoffs are not supported on the Binomial engine",
1044 );
1045 }
1046 if matches!(self.engine, PricingEngine::FiniteDifference(_))
1047 && !matches!(self.payoff.payoff_kind(), PayoffType::Barrier)
1048 {
1049 return unsupported(
1050 "Of the path-dependent payoffs only barriers price on the FD \
1051 engine; use MonteCarlo",
1052 );
1053 }
1054 if matches!(self.engine, PricingEngine::BlackScholes)
1055 && matches!(
1056 self.payoff.payoff_kind(),
1057 PayoffType::Autocallable | PayoffType::Accumulator
1058 )
1059 {
1060 return unsupported(
1061 "Autocallables and accumulators price on the MonteCarlo engine only",
1062 );
1063 }
1064 }
1065 let heston = self.model.is_heston();
1066 if heston && matches!(self.engine, PricingEngine::Binomial(_)) {
1067 return unsupported(
1068 "The Heston model is supported on the Analytical, MonteCarlo and \
1069 FiniteDifference (2-D ADI) engines, not Binomial",
1070 );
1071 }
1072 if heston
1073 && matches!(self.engine, PricingEngine::FiniteDifference(_))
1074 && !matches!(self.payoff.payoff_kind(), PayoffType::Vanilla | PayoffType::Binary)
1075 {
1076 return unsupported(
1077 "The Heston ADI engine prices vanilla and binary payoffs; \
1078 use MonteCarlo for path-dependent payoffs",
1079 );
1080 }
1081 match self.engine {
1082 PricingEngine::BlackScholes if american => unsupported(
1083 "Analytical engine cannot price early exercise; \
1084 use Binomial, FiniteDifference or MonteCarlo",
1085 ),
1086 PricingEngine::BaroneAdesiWhaley | PricingEngine::BjerksundStensland => {
1087 let name = match self.engine {
1088 PricingEngine::BaroneAdesiWhaley => "Barone-Adesi-Whaley",
1089 _ => "Bjerksund-Stensland",
1090 };
1091 if !matches!(self.payoff.payoff_kind(), PayoffType::Vanilla) {
1092 return Err(RustyQLibError::UnsupportedEngine(format!(
1093 "{name} approximates vanilla options only"
1094 )));
1095 }
1096 if heston {
1097 return Err(RustyQLibError::UnsupportedEngine(format!(
1098 "{name} assumes constant-vol Black-Scholes dynamics, not Heston"
1099 )));
1100 }
1101 if bermudan {
1102 return Err(RustyQLibError::UnsupportedEngine(format!(
1103 "{name} approximates American exercise only; Bermudan prices on \
1104 Binomial, FiniteDifference or MonteCarlo"
1105 )));
1106 }
1107 Ok(())
1108 }
1109 _ => Ok(()),
1110 }
1111 }
1112}
1113
1114impl Instrument for EquityOption {
1115 fn try_npv(&self) -> Result<f64, RustyQLibError> {
1116 self.check_engine_support()?;
1117 let heston = self.model.is_heston();
1118 Ok(match self.engine {
1119 PricingEngine::BlackScholes if heston => heston::analytic_npv(&self),
1120 PricingEngine::BlackScholes => BlackScholesPricer::new().npv(&self),
1121 PricingEngine::MonteCarlo(_) => montecarlo::npv(&self),
1122 PricingEngine::Binomial(_) => binomial::npv(&self),
1123 PricingEngine::FiniteDifference(_) => finite_difference::npv(&self),
1124 PricingEngine::BaroneAdesiWhaley => baw::npv(&self),
1125 PricingEngine::BjerksundStensland => bjerksund_stensland::npv(&self),
1126 })
1127 }
1128
1129 fn price(&self) -> Result<PricingResult, RustyQLibError> {
1134 self.check_engine_support()?;
1135 Ok(crate::equity::greeks::pricing_result(self))
1136 }
1137}
1138
1139impl EquityOption {
1148 pub(crate) fn analytic_heston(&self) -> bool {
1149 matches!(self.engine, PricingEngine::BlackScholes | PricingEngine::Binomial(_))
1150 && self.model.is_heston()
1151 }
1152 pub fn delta(&self) -> f64 {
1153 greeks::delta(self)
1154 }
1155 pub fn gamma(&self) -> f64 {
1156 greeks::gamma(self)
1157 }
1158 pub fn vega(&self) -> f64 {
1159 greeks::vega(self)
1160 }
1161 pub fn theta(&self) -> f64 {
1162 greeks::theta(self)
1163 }
1164 pub fn rho(&self) -> f64 {
1165 greeks::rho(self)
1166 }
1167 pub fn vanna(&self) -> f64 {
1169 greeks::vanna(self)
1170 }
1171 pub fn charm(&self) -> f64 {
1173 greeks::charm(self)
1174 }
1175 pub fn gamma_p(&self) -> f64 {
1177 greeks::gamma_p(self)
1178 }
1179 pub fn zomma(&self) -> f64 {
1181 greeks::zomma(self)
1182 }
1183 pub fn volga(&self) -> f64 {
1185 greeks::volga(self)
1186 }
1187 pub fn price_with(&self, d_spot: f64, d_vol: f64, d_rate: f64, d_time: f64) -> f64 {
1195 if self.base.is_futures_option() {
1196 let f = self.market.spot.value();
1197 let k = self.base.strike_price;
1198 let t = self.time_to_maturity();
1199 let sigma = self.market.vol_surface.vol(k, f, t);
1200 return crate::equity::black76::price(
1201 f + d_spot,
1202 k,
1203 self.risk_free_rate() + d_rate,
1204 sigma + d_vol,
1205 (t - d_time).max(1e-6),
1206 *self.payoff.put_or_call(),
1207 self.base.futures_settlement.expect("futures option must carry a settlement"),
1208 );
1209 }
1210 match self.engine {
1211 PricingEngine::MonteCarlo(_) => montecarlo::npv_with(&self, d_spot, d_vol, d_rate, d_time),
1212 PricingEngine::FiniteDifference(_) => {
1213 finite_difference::npv_with(&self, d_spot, d_vol, d_rate, d_time)
1214 }
1215 PricingEngine::BaroneAdesiWhaley => baw::price_with(&self, d_spot, d_vol, d_rate, d_time),
1216 PricingEngine::BjerksundStensland => {
1217 bjerksund_stensland::price_with(&self, d_spot, d_vol, d_rate, d_time)
1218 }
1219 _ if self.analytic_heston() => {
1222 heston::price_with(&self, d_spot, d_vol, d_rate, -d_time)
1223 }
1224 PricingEngine::Binomial(_) => binomial::npv_with(&self, d_spot, d_vol, d_rate, d_time),
1225 _ => BlackScholesPricer::price_with(&self, d_spot, d_vol, d_rate, -d_time),
1226 }
1227 }
1228}
1229