1use std::error::Error;
2use chrono::{Datelike, Local, NaiveDate};
3use crate::equity::{binomial,finite_difference,montecarlo};
4use crate::core::curves::{Compounding, YieldCurve};
5use crate::core::daycount::DayCountConvention;
6use crate::core::vols::VolSurface;
7use crate::equity::asian::{AsianStrikeType, AveragingType};
8use crate::equity::barrier::{BarrierDirection, KnockType};
9use crate::equity::heston;
10use super::super::core::quotes::Quote;
11use super::super::core::traits::{Instrument,Greeks};
12use super::blackscholes;
13use crate::equity::utils::{Engine, PayoffType, Payoff, LongShort};
14use crate::core::trade::{PutOrCall,Transection};
15use crate::core::utils::{Contract,ContractStyle};
16use crate::core::trade;
17use serde::Deserialize;
18use blackscholes::BlackScholesPricer;
19use crate::core::data_models::EquityOptionData;
20
21#[derive(Debug)]
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)]
37pub struct BinaryPayoff {
38 pub put_or_call: PutOrCall,
39 pub exercise_style: ContractStyle,
40 pub binary_type: BinaryType,
41 pub cash: f64,
43}
44#[derive(Debug)]
45pub struct BarrierPayoff {
46 pub put_or_call: PutOrCall,
47 pub exercise_style: ContractStyle,
48 pub direction: BarrierDirection,
49 pub knock: KnockType,
50 pub barrier: f64,
51}
52
53impl Payoff for BarrierPayoff {
59 fn payoff(&self, spot: f64, strike: f64) -> f64 {
60 match &self.put_or_call {
61 PutOrCall::Call => (spot - strike).max(0.0),
62 PutOrCall::Put => (strike - spot).max(0.0),
63 }
64 }
65 fn path_payoff(&self, path: &[f64], strike: f64) -> f64 {
66 let crossed = path.iter().any(|&s| match self.direction {
67 BarrierDirection::Up => s >= self.barrier,
68 BarrierDirection::Down => s <= self.barrier,
69 });
70 let alive = match self.knock {
71 KnockType::Out => !crossed,
72 KnockType::In => crossed,
73 };
74 if alive {
75 self.payoff(*path.last().expect("empty path"), strike)
76 } else {
77 0.0
78 }
79 }
80 fn is_path_dependent(&self) -> bool {
81 true
82 }
83 fn payoff_kind(&self) -> PayoffType {
84 PayoffType::Barrier
85 }
86 fn put_or_call(&self) -> &PutOrCall {
87 &self.put_or_call
88 }
89 fn exercise_style(&self) -> &ContractStyle {
90 &self.exercise_style
91 }
92 fn as_any(&self) -> &dyn std::any::Any {
93 self
94 }
95}
96#[derive(Debug)]
97pub struct AsianPayoff {
98 pub put_or_call: PutOrCall,
99 pub exercise_style: ContractStyle,
100 pub averaging: AveragingType,
101 pub strike_type: AsianStrikeType,
102}
103
104impl Payoff for AsianPayoff {
109 fn payoff(&self, spot: f64, strike: f64) -> f64 {
112 match &self.put_or_call {
113 PutOrCall::Call => (spot - strike).max(0.0),
114 PutOrCall::Put => (strike - spot).max(0.0),
115 }
116 }
117 fn path_payoff(&self, path: &[f64], strike: f64) -> f64 {
118 let n = path.len() as f64;
119 let average = match self.averaging {
120 AveragingType::Arithmetic => path.iter().sum::<f64>() / n,
121 AveragingType::Geometric => (path.iter().map(|s| s.ln()).sum::<f64>() / n).exp(),
122 };
123 let terminal = *path.last().expect("empty path");
124 let (long_leg, short_leg) = match self.strike_type {
125 AsianStrikeType::FixedStrike => (average, strike),
126 AsianStrikeType::FloatingStrike => (terminal, average),
127 };
128 match &self.put_or_call {
129 PutOrCall::Call => (long_leg - short_leg).max(0.0),
130 PutOrCall::Put => (short_leg - long_leg).max(0.0),
131 }
132 }
133 fn is_path_dependent(&self) -> bool {
134 true
135 }
136 fn payoff_kind(&self) -> PayoffType {
137 PayoffType::Asian
138 }
139 fn put_or_call(&self) -> &PutOrCall {
140 &self.put_or_call
141 }
142 fn exercise_style(&self) -> &ContractStyle {
143 &self.exercise_style
144 }
145 fn as_any(&self) -> &dyn std::any::Any {
146 self
147 }
148}
149impl Payoff for VanillaPayoff {
150 fn payoff(&self, spot: f64, strike: f64) -> f64 {
151 match &self.put_or_call {
152 PutOrCall::Call => (spot - strike).max(0.0),
153 PutOrCall::Put => (strike - spot).max(0.0),
154 }
155 }
156 fn payoff_kind(&self) -> PayoffType {
157 PayoffType::Vanilla
158 }
159 fn put_or_call(&self) -> &PutOrCall {
160 &self.put_or_call
161 }
162 fn exercise_style(&self) -> &ContractStyle {
163 &self.exercise_style
164 }
165 fn as_any(&self) -> &dyn std::any::Any {
166 self
167 }
168
169}
170
171impl Payoff for BinaryPayoff {
174 fn payoff(&self, spot: f64, strike: f64) -> f64 {
175 let in_the_money = match &self.put_or_call {
176 PutOrCall::Call => spot > strike,
177 PutOrCall::Put => spot < strike,
178 };
179 if !in_the_money {
180 return 0.0;
181 }
182 match self.binary_type {
183 BinaryType::CashOrNothing => self.cash,
184 BinaryType::AssetOrNothing => spot,
185 }
186 }
187 fn payoff_kind(&self) -> PayoffType {
188 PayoffType::Binary
189 }
190 fn put_or_call(&self) -> &PutOrCall {
191 &self.put_or_call
192 }
193 fn exercise_style(&self) -> &ContractStyle {
194 &self.exercise_style
195 }
196 fn as_any(&self) -> &dyn std::any::Any {
197 self
198 }
199}
200
201#[derive(Debug)]
202pub struct EquityOptionBase {
203 pub symbol: String,
204 pub currency: Option<String>,
205 pub exchange: Option<String>,
206 pub name: Option<String>,
207 pub cusip: Option<String>,
208 pub isin: Option<String>,
209 pub settlement_type: Option<String>,
210
211 pub underlying_price: Quote,
213 pub current_price: Quote,
214 pub strike_price: f64,
215 pub dividend_yield: f64,
216 pub borrow_cost: f64,
219 pub futures_settlement: Option<crate::equity::black76::FuturesSettlement>,
224 pub cash_dividends: Vec<(NaiveDate, f64)>,
229 pub vol_surface: VolSurface,
231 pub maturity_date: NaiveDate,
232 pub valuation_date: NaiveDate,
233 pub discount_curve: YieldCurve,
236 pub entry_price: f64,
237 pub long_short: LongShort,
238 pub multiplier: f64,
239
240}
241#[derive(Debug)]
242pub struct EquityOption {
243 pub base: EquityOptionBase,
244 pub payoff: Box<dyn Payoff>,
245 pub engine: Engine,
246 pub mc: montecarlo::MonteCarloConfig,
249 pub fd: finite_difference::FdConfig,
252 pub heston: Option<crate::equity::heston::HestonParams>,
254}
255impl EquityOption{
256 pub fn time_to_maturity(&self) -> f64{
257 let time_to_maturity = (self.base.maturity_date - self.base.valuation_date).num_days() as f64/365.0;
258 time_to_maturity
259 }
260
261}
262impl EquityOption {
263
264 pub fn from_json(data: &EquityOptionData) -> Box<EquityOption> {
265 let valuation_date = Local::now().date_naive();
266 let discount_curve = match &data.discount_curve {
267 Some(input) => YieldCurve::from_input(input, valuation_date)
268 .expect("Invalid discount curve"),
269 None => YieldCurve::flat(
270 data.base.risk_free_rate.unwrap_or(0.0),
271 valuation_date,
272 DayCountConvention::Act365,
273 Compounding::Continuous,
274 )
275 .expect("Invalid risk free rate"),
276 };
277 let vol_surface = match &data.vol_surface {
278 Some(input) => VolSurface::from_input(input, valuation_date)
279 .expect("Invalid vol surface"),
280 None => VolSurface::flat(
281 data.volatility
282 .expect("Either volatility or vol_surface must be provided"),
283 valuation_date,
284 DayCountConvention::Act365,
285 )
286 .expect("Invalid volatility"),
287 };
288 let maturity_date = NaiveDate::parse_from_str(&data.maturity, "%Y-%m-%d").expect("Invalid date format");
289 let payoff_type = data.payoff_type.parse::<PayoffType>().unwrap();
290 let strike_price = match payoff_type {
291 PayoffType::ForwardStart | PayoffType::Autocallable => {
293 data.strike_price.unwrap_or(0.0)
294 }
295 _ => data.strike_price.expect("strike_price is required for this payoff"),
296 };
297 let cash_dividends: Vec<(NaiveDate, f64)> = data
298 .cash_dividends
299 .as_deref()
300 .unwrap_or(&[])
301 .iter()
302 .map(|d| {
303 (
304 NaiveDate::parse_from_str(&d.date, "%Y-%m-%d")
305 .expect("Invalid cash dividend date"),
306 d.amount,
307 )
308 })
309 .collect();
310 let futures_settlement = data.futures_settlement.as_deref().map(|s| {
311 s.parse::<crate::equity::black76::FuturesSettlement>()
312 .expect("Invalid futures_settlement")
313 });
314 if futures_settlement.is_some() {
315 assert!(
316 matches!(payoff_type, PayoffType::Vanilla),
317 "options on futures (Black-76) support the vanilla payoff only"
318 );
319 }
320 let base_option = EquityOptionBase {
321 symbol:data.base.symbol.clone(),
322 currency: data.base.currency.clone(),
323 exchange:data.base.exchange.clone(),
324 name: data.base.name.clone(),
325 cusip: data.base.cusip.clone(),
326 isin: data.base.isin.clone(),
327 settlement_type: data.base.settlement_type.clone(),
328
329 underlying_price: Quote::new(data.base.underlying_price),
330 current_price: Quote::new(data.current_price.unwrap_or(0.0)),
331 strike_price,
332 vol_surface,
333 maturity_date,
334 discount_curve,
335 entry_price: data.entry_price.unwrap_or(0.0),
336 long_short: LongShort::LONG,
337 dividend_yield: data.dividend.unwrap_or(0.0),
338 borrow_cost: data.base.borrow_cost.unwrap_or(0.0),
339 cash_dividends,
340 futures_settlement,
341 valuation_date,
342 multiplier: data.multiplier.unwrap_or(1.0),
343 };
344 let payoff_type = &payoff_type;
345 let side: PutOrCall;
346 let put_or_call = data.put_or_call.clone();
347 match put_or_call.trim() {
348 "C" | "c" | "Call" | "call" => side = PutOrCall::Call,
349 "P" | "p" | "Put" | "put" => side = PutOrCall::Put,
350 _ => panic!("Invalid side argument! Side has to be either 'C' or 'P'."),
351 }
352
353 let style = match data.exercise_style.as_ref().unwrap_or(&"European".to_string()).trim() {
354 "European" | "european" => {
355 ContractStyle::European
356 }
357 "American" | "american" => {
358 ContractStyle::American
359 }
360 _ => {
361 ContractStyle::European
362 }
363 };
364
365 let payoff:Box<dyn Payoff> = match &payoff_type {
366 PayoffType::Vanilla => Box::new(VanillaPayoff{
367 put_or_call:side,
368 exercise_style:style}),
369 PayoffType::Binary => {
370 let binary_type = match data
371 .binary_type
372 .as_deref()
373 .unwrap_or("cash")
374 .trim()
375 .to_lowercase()
376 .as_str()
377 {
378 "cash" | "cash_or_nothing" | "cash-or-nothing" => BinaryType::CashOrNothing,
379 "asset" | "asset_or_nothing" | "asset-or-nothing" => BinaryType::AssetOrNothing,
380 other => panic!("Invalid binary_type '{other}' (use 'cash' or 'asset')"),
381 };
382 Box::new(BinaryPayoff {
383 put_or_call: side,
384 exercise_style: style,
385 binary_type,
386 cash: data.cash_amount.unwrap_or(1.0),
387 })
388 }
389 PayoffType::Barrier => {
390 let barrier = data
391 .barrier_level
392 .expect("barrier_level is required for barrier options");
393 let (direction, knock) = match data
394 .barrier_type
395 .as_deref()
396 .unwrap_or("")
397 .trim()
398 .to_lowercase()
399 .as_str()
400 {
401 "up_in" | "up-in" | "ui" => (BarrierDirection::Up, KnockType::In),
402 "up_out" | "up-out" | "uo" => (BarrierDirection::Up, KnockType::Out),
403 "down_in" | "down-in" | "di" => (BarrierDirection::Down, KnockType::In),
404 "down_out" | "down-out" | "do" => (BarrierDirection::Down, KnockType::Out),
405 other => panic!(
406 "barrier_type must be up_in/up_out/down_in/down_out, got '{other}'"
407 ),
408 };
409 Box::new(BarrierPayoff {
410 put_or_call: side,
411 exercise_style: style,
412 direction,
413 knock,
414 barrier,
415 })
416 }
417 PayoffType::Asian => {
418 let averaging = match data
419 .averaging_type
420 .as_deref()
421 .unwrap_or("arithmetic")
422 .trim()
423 .to_lowercase()
424 .as_str()
425 {
426 "arithmetic" | "arith" => AveragingType::Arithmetic,
427 "geometric" | "geo" => AveragingType::Geometric,
428 other => panic!("averaging_type must be arithmetic or geometric, got '{other}'"),
429 };
430 let strike_type = match data
431 .asian_strike_type
432 .as_deref()
433 .unwrap_or("fixed")
434 .trim()
435 .to_lowercase()
436 .as_str()
437 {
438 "fixed" | "average_price" => AsianStrikeType::FixedStrike,
439 "floating" | "average_strike" => AsianStrikeType::FloatingStrike,
440 other => panic!("asian_strike_type must be fixed or floating, got '{other}'"),
441 };
442 Box::new(AsianPayoff {
443 put_or_call: side,
444 exercise_style: style,
445 averaging,
446 strike_type,
447 })
448 }
449 PayoffType::ForwardStart => {
450 let start_date_str = data
451 .forward_start_date
452 .as_ref()
453 .expect("forward_start_date is required for forward-start options");
454 let start_date = NaiveDate::parse_from_str(start_date_str, "%Y-%m-%d")
455 .expect("Invalid forward_start_date");
456 assert!(
457 start_date > valuation_date && start_date < maturity_date,
458 "forward_start_date must lie between valuation and maturity"
459 );
460 let start_fraction = (start_date - valuation_date).num_days() as f64
461 / (maturity_date - valuation_date).num_days() as f64;
462 Box::new(crate::equity::forward_start_option::ForwardStartPayoff {
463 put_or_call: side,
464 exercise_style: style,
465 strike_fraction: data.strike_fraction.unwrap_or(1.0),
466 start_fraction,
467 })
468 }
469 PayoffType::Autocallable => {
470 Box::new(crate::equity::autocallable::AutocallablePayoff {
471 exercise_style: style,
472 autocall_barrier: data
473 .autocall_barrier
474 .expect("autocall_barrier is required for autocallables"),
475 protection_barrier: data
476 .protection_barrier
477 .expect("protection_barrier is required for autocallables"),
478 coupon: data.autocall_coupon.unwrap_or(0.0),
479 observations: data.autocall_observations.unwrap_or(4).max(1),
480 notional: data.notional.unwrap_or(100.0),
481 initial_fixing: data.base.underlying_price,
482 })
483 }
484 };
485
486 let equityoption = EquityOption {
487 base: base_option,
488 payoff,
489 engine: match data.pricer.as_ref().map_or("Analytical",|v| v).trim() {
490 "Analytical" | "analytical" | "bs" => Engine::BlackScholes,
491 "MonteCarlo" | "montecarlo" | "MC" | "mc" => Engine::MonteCarlo,
492 "Binomial" | "binomial" | "bino" => Engine::Binomial,
493 "FiniteDifference" | "finitdifference" | "FD" | "fd" => Engine::FiniteDifference,
494 _ => {
495 panic!("Invalid pricer");
496 }
497 },
498 mc: montecarlo::MonteCarloConfig::from_data(data),
499 fd: finite_difference::FdConfig::from_data(data),
500 heston: data.heston
501 };
502 if equityoption.mc.model == montecarlo::McModel::Heston {
503 equityoption
504 .heston
505 .expect("heston parameters are required when mc_model = heston")
506 .validate()
507 .expect("invalid heston parameters");
508 }
509 Box::new(equityoption)
510 }
511}
512
513impl EquityOptionBase {
514 pub fn time_to_maturity(&self) -> f64{
515 let time_to_maturity = (self.maturity_date - self.valuation_date).num_days() as f64/365.0;
516 time_to_maturity
517 }
518 pub fn maturity_discount_factor(&self) -> f64 {
520 self.discount_curve.df(self.time_to_maturity())
521 }
522 pub fn risk_free_rate(&self) -> f64 {
526 self.discount_curve
527 .zero_rate_with(self.time_to_maturity(), Compounding::Continuous)
528 }
529 pub fn carry_yield(&self) -> f64 {
532 self.dividend_yield + self.borrow_cost
533 }
534 pub fn is_futures_option(&self) -> bool {
536 self.futures_settlement.is_some()
537 }
538 pub fn pv_cash_dividends(&self) -> f64 {
550 let carry = self.carry_yield();
551 self.cash_dividends
552 .iter()
553 .filter(|(date, _)| *date > self.valuation_date && *date <= self.maturity_date)
554 .map(|(date, amount)| {
555 let t = (*date - self.valuation_date).num_days() as f64 / 365.0;
556 amount * self.discount_curve.df(t) * (carry * t).exp()
559 })
560 .sum()
561 }
562 pub fn effective_spot(&self) -> f64 {
566 let s = self.underlying_price.value() - self.pv_cash_dividends();
567 assert!(s > 0.0, "cash dividends exceed the spot price");
568 s
569 }
570 pub fn forward_price(&self) -> f64 {
573 let t = self.time_to_maturity();
574 self.effective_spot() * ((self.risk_free_rate() - self.carry_yield()) * t).exp()
575 }
576 pub fn volatility(&self) -> f64 {
579 self.vol_surface
580 .vol(self.strike_price, self.forward_price(), self.time_to_maturity())
581 }
582 pub fn d1(&self) -> f64 {
583 let volatility = self.volatility();
585 let d1_numerator = (self.effective_spot() / self.strike_price).ln()
586 + (self.risk_free_rate() - self.carry_yield() + 0.5 * volatility.powi(2))
587 * self.time_to_maturity();
588
589 let d1_denominator = volatility * (self.time_to_maturity().sqrt());
590 return d1_numerator / d1_denominator;
591 }
592 pub fn d2(&self) -> f64 {
593 let d2 = self.d1() - self.volatility() * self.time_to_maturity().powf(0.5);
594 return d2;
595 }
596}
597impl EquityOption {
598 pub fn get_premium_at_risk(&self) -> f64 {
599 let value = self.npv();
600 let mut pay_off = self.payoff.payoff_amount(&self.base);
601 if pay_off > 0.0 {
602 return value - pay_off;
603 } else {
604 return value;
605 }
606 }
607
608 pub fn try_imp_vol(&self, option_price: f64) -> Result<f64, String> {
611 blackscholes::implied_vol_from_price(
612 self.base.effective_spot(),
613 self.base.strike_price,
614 self.base.risk_free_rate(),
615 self.base.carry_yield(),
616 self.time_to_maturity(),
617 option_price,
618 *self.payoff.put_or_call(),
619 )
620 }
621 pub fn imp_vol(&mut self,option_price:f64) -> f64 {
625 let vol = self.try_imp_vol(option_price).expect("implied vol solve failed");
626 self.set_flat_vol(vol.max(1e-8));
627 vol
628 }
629 pub fn get_imp_vol(&mut self) -> f64 {
630 let target = self.base.current_price.value;
631 self.imp_vol(target)
632 }
633 fn set_flat_vol(&mut self, vol: f64) {
634 self.base.vol_surface = VolSurface::flat(
635 vol,
636 self.base.vol_surface.reference_date(),
637 self.base.vol_surface.day_count(),
638 )
639 .expect("vol must be positive");
640 }
641}
642
643
644impl Instrument for EquityOption {
645 fn npv(&self) -> f64 {
646 let american = matches!(self.payoff.exercise_style(), ContractStyle::American);
647 if self.base.is_futures_option() {
648 if !matches!(self.engine, Engine::BlackScholes) {
649 panic!(
650 "Options on futures (Black-76) price on the Analytical engine only"
651 );
652 }
653 if american {
654 panic!("Black-76 supports European exercise only");
655 }
656 }
657 if self.payoff.is_path_dependent() {
658 if american {
659 panic!("American path-dependent options are not supported yet");
660 }
661 if matches!(self.engine, Engine::Binomial) {
662 panic!("Path-dependent payoffs are not supported on the Binomial engine");
663 }
664 if matches!(self.engine, Engine::FiniteDifference)
665 && !matches!(self.payoff.payoff_kind(), PayoffType::Barrier)
666 {
667 panic!(
668 "Of the path-dependent payoffs only barriers price on the FD \
669 engine; use MonteCarlo"
670 );
671 }
672 if matches!(self.engine, Engine::BlackScholes)
673 && matches!(self.payoff.payoff_kind(), PayoffType::Autocallable)
674 {
675 panic!("Autocallables price on the MonteCarlo engine only");
676 }
677 }
678 let heston = self.mc.model == montecarlo::McModel::Heston;
679 if heston && matches!(self.engine, Engine::Binomial | Engine::FiniteDifference) {
680 panic!(
681 "The Heston model is supported on the Analytical and MonteCarlo \
682 engines only (a 2-D ADI FD solver is future work)"
683 );
684 }
685 match self.engine {
686 Engine::BlackScholes => {
687 if american {
688 panic!(
689 "Analytical engine cannot price American exercise; \
690 use Binomial, FiniteDifference or MonteCarlo"
691 );
692 }
693 if heston {
694 heston::analytic_npv(&self)
695 } else {
696 BlackScholesPricer::new().npv(&self)
697 }
698 }
699 Engine::MonteCarlo => montecarlo::npv(&self),
700 Engine::Binomial => binomial::npv(&self),
701 Engine::FiniteDifference => finite_difference::npv(&self),
702 }
703 }
704}
705
706impl EquityOption {
712 fn analytic_heston(&self) -> bool {
713 matches!(self.engine, Engine::BlackScholes | Engine::Binomial)
714 && self.mc.model == montecarlo::McModel::Heston
715 }
716 pub fn delta(&self) -> f64 {
717 match self.engine {
718 Engine::MonteCarlo => montecarlo::delta(&self),
719 Engine::FiniteDifference => finite_difference::delta(&self),
720 _ if self.analytic_heston() => heston::analytic_delta(&self),
721 _ => BlackScholesPricer::new().delta(&self),
722 }
723 }
724 pub fn gamma(&self) -> f64 {
725 match self.engine {
726 Engine::MonteCarlo => montecarlo::gamma(&self),
727 Engine::FiniteDifference => finite_difference::gamma(&self),
728 _ if self.analytic_heston() => heston::analytic_gamma(&self),
729 _ => BlackScholesPricer::new().gamma(&self),
730 }
731 }
732 pub fn vega(&self) -> f64 {
733 match self.engine {
734 Engine::MonteCarlo => montecarlo::vega(&self),
735 Engine::FiniteDifference => finite_difference::vega(&self),
736 _ if self.analytic_heston() => heston::analytic_vega(&self),
737 _ => BlackScholesPricer::new().vega(&self),
738 }
739 }
740 pub fn theta(&self) -> f64 {
741 match self.engine {
742 Engine::MonteCarlo => montecarlo::theta(&self),
743 Engine::FiniteDifference => finite_difference::theta(&self),
744 _ if self.analytic_heston() => heston::analytic_theta(&self),
745 _ => BlackScholesPricer::new().theta(&self),
746 }
747 }
748 pub fn rho(&self) -> f64 {
749 match self.engine {
750 Engine::MonteCarlo => montecarlo::rho(&self),
751 Engine::FiniteDifference => finite_difference::rho(&self),
752 _ if self.analytic_heston() => heston::analytic_rho(&self),
753 _ => BlackScholesPricer::new().rho(&self),
754 }
755 }
756}
757