Skip to main content

rustyqlib/equity/
vanila_option.rs

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/// Binary (digital) settlement style.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum BinaryType {
30    /// Pays a fixed cash amount when in the money.
31    CashOrNothing,
32    /// Delivers the underlying (pays its level) when in the money.
33    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    /// Amount paid by a cash-or-nothing binary (ignored for asset-or-nothing).
42    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
53/// Barrier payoff: `payoff` is the underlying vanilla leg (used by the
54/// analytic building blocks and as the terminal leg of path pricing);
55/// `path_payoff` applies discretely monitored knock logic to a full path.
56/// The Monte Carlo engine additionally applies a Brownian-bridge crossing
57/// correction, so its effective monitoring is continuous.
58impl 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
104/// Asian payoff: the average is taken over the monitored path points
105/// (equally spaced, spot excluded). Fixed strike pays on the average
106/// against the strike; floating strike pays on the terminal spot against
107/// the average.
108impl Payoff for AsianPayoff {
109    /// Degenerate single-point average (used for intrinsic display only;
110    /// engines route Asians through `path_payoff`).
111    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
171/// Binary (digital) payoff, strictly in the money beyond the strike:
172/// cash-or-nothing pays `cash`, asset-or-nothing pays the underlying level.
173impl 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 payoff_type: String, // Vanilla/Barrier/Binary
212    pub underlying_price: Quote,
213    pub current_price: Quote,
214    pub strike_price: f64,
215    pub dividend_yield: f64,
216    /// Continuous stock borrow (repo) cost; part of the carry alongside
217    /// the dividend yield.
218    pub borrow_cost: f64,
219    /// When set, the underlying is a future priced with Black-76
220    /// (`underlying_price` is the futures price `F`), settled either with an
221    /// up-front discounted premium or futures-style margined. European
222    /// vanilla only, on the Analytical engine.
223    pub futures_settlement: Option<crate::equity::black76::FuturesSettlement>,
224    /// Discrete cash dividends (ex-date, amount per share). Analytic,
225    /// tree and terminal Monte Carlo engines use the escrowed model
226    /// (spot minus PV of dividends); path-wise Monte Carlo and finite
227    /// difference apply the jumps at the ex-dates.
228    pub cash_dividends: Vec<(NaiveDate, f64)>,
229    /// Volatility surface; a flat surface represents a single constant vol.
230    pub vol_surface: VolSurface,
231    pub maturity_date: NaiveDate,
232    pub valuation_date: NaiveDate,
233    /// Discounting curve anchored at `valuation_date`; discount factors are
234    /// the source of truth, rates are derived views.
235    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    /// Monte Carlo settings (paths, time steps, scheme, sampler, seed).
247    /// `mc.model` (GBM vs local vol) also applies to the FD engine.
248    pub mc: montecarlo::MonteCarloConfig,
249    /// Finite difference grid settings; only consulted when `engine` is
250    /// [`Engine::FiniteDifference`].
251    pub fd: finite_difference::FdConfig,
252    /// Heston parameters; required when the model is Heston.
253    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            // strike is set by the contract mechanics for these payoffs
292            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    /// Discount factor from the valuation date to maturity, off the curve.
519    pub fn maturity_discount_factor(&self) -> f64 {
520        self.discount_curve.df(self.time_to_maturity())
521    }
522    /// Continuously compounded zero rate to maturity implied by the curve.
523    /// This is the `r` that enters d1/d2; it is consistent with
524    /// [`maturity_discount_factor`](Self::maturity_discount_factor) by construction.
525    pub fn risk_free_rate(&self) -> f64 {
526        self.discount_curve
527            .zero_rate_with(self.time_to_maturity(), Compounding::Continuous)
528    }
529    /// Total continuous carry on the underlying: dividend yield plus
530    /// borrow cost. This is the "q" every pricing formula uses.
531    pub fn carry_yield(&self) -> f64 {
532        self.dividend_yield + self.borrow_cost
533    }
534    /// True when the underlying is a future priced with Black-76.
535    pub fn is_futures_option(&self) -> bool {
536        self.futures_settlement.is_some()
537    }
538    /// Escrow value of the cash dividends with ex-dates inside the option's
539    /// life: the amount to carve out of spot so the risky stub reproduces
540    /// the jump-model forward.
541    ///
542    /// Each dividend is discounted at the **net carry rate** `r - carry`,
543    /// not the risk-free rate, so that the escrow accretes at the same rate
544    /// the risky stub grows (`effective_spot` is grown at `r - carry` in
545    /// [`forward_price`]). This makes the analytic forward match the
546    /// well-defined jump model `F = (S - D e^{-(r-carry)t}) e^{(r-carry)T}`
547    /// used by the FD and path-wise Monte Carlo engines. With no continuous
548    /// carry this reduces to plain risk-free discounting.
549    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                // df(t) = e^{-r t}; multiplying by e^{carry t} discounts at
557                // the net carry (r - carry), generalizing to any curve shape.
558                amount * self.discount_curve.df(t) * (carry * t).exp()
559            })
560            .sum()
561    }
562    /// Escrowed-model spot: the quoted spot minus the PV of cash dividends
563    /// paid over the option's life. This is the lognormal driver for the
564    /// analytic and terminal-simulation engines.
565    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    /// Forward price of the underlying at maturity: escrowed spot grown at
571    /// the carry-adjusted rate, `(S - PV(divs)) * exp((r - q - b) * T)`.
572    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    /// Black volatility for this option's strike and expiry, read off the
577    /// surface (a flat surface returns its single vol).
578    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        // Black-Scholes-Merton d1 on the escrowed spot and total carry
584        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    /// Implied Black-Scholes volatility for `option_price` (safeguarded
609    /// Newton with arbitrage-bound checks); does not modify the option.
610    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    /// Implied vol for `option_price`; leaves the option holding a flat
622    /// surface at the solved vol. Panics on arbitrage-violating prices —
623    /// use [`try_imp_vol`](Self::try_imp_vol) to handle those gracefully.
624    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
706/// Greeks per engine: Monte Carlo uses bump-and-reprice with common random
707/// numbers (supporting American via Longstaff-Schwartz repricing); the FD
708/// engine reads delta/gamma/theta off its own grid (so American and barrier
709/// sensitivities are engine-consistent) with vega/rho by re-solving; the
710/// remaining engines use the analytic Black-Scholes closed forms.
711impl 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// #[cfg(test)]
758// mod tests {
759//     //write a unit test for from_json
760//     use super::*;
761//     use crate::core::utils::{Contract,MarketData};
762//     use crate::core::trade::OptionType;
763//     use crate::core::trade::Transection;
764//     use crate::core::utils::ContractStyle;
765//     use crate::core::termstructure::YieldTermStructure;
766//     use crate::core::quotes::Quote;
767//     use chrono::{Datelike, Local, NaiveDate};
768//     #[test]
769//     fn test_from_json() {
770//         let data = Contract {
771//             action: "PV".to_string(),
772//             market_data: Some(MarketData {
773//                 underlying_price: 100.0,
774//                 strike_price: 100.0,
775//                 volatility: None,
776//                 option_price: Some(10.0),
777//                 risk_free_rate: Some(0.05),
778//                 dividend: Some(0.0),
779//                 maturity: "2024-01-01".to_string(),
780//                 option_type: "C".to_string(),
781//                 simulation: None
782//             }),
783//             pricer: "Analytical".to_string(),
784//             asset: "".to_string(),
785//             style: Some("European".to_string()),
786//             rate_data: None
787//         };
788//         let option = EquityOption::from_json(&data);
789//         assert_eq!(option.option_type, OptionType::Call);
790//         assert_eq!(option.transection, Transection::Buy);
791//         assert_eq!(option.underlying_price.value, 100.0);
792//         assert_eq!(option.strike_price, 100.0);
793//         assert_eq!(option.current_price.value, 10.0);
794//         assert_eq!(option.dividend_yield, 0.0);
795//         assert_eq!(option.volatility, 0.2);
796//         assert_eq!(option.maturity_date, NaiveDate::from_ymd(2024, 1, 1));
797//         assert_eq!(option.valuation_date, Local::today().naive_utc());
798//         assert_eq!(option.engine, Engine::BlackScholes);
799//         assert_eq!(option.style, ContractStyle::European);
800//     }
801// }
802