Skip to main content

rustyqlib/equity/
vanilla_option.rs

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/// 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/// Lookback flavor: floating strike pays against the path extremum,
37/// fixed strike pays the extremum against a fixed strike.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum LookbackType {
40    FloatingStrike,
41    FixedStrike,
42}
43
44/// Lookback payoff: the call watches the minimum (floating) or maximum
45/// (fixed), the put the mirror image. Discretely monitored on the path
46/// grid under Monte Carlo; the analytic engine prices the continuous-
47/// monitoring closed forms.
48#[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    /// Degenerate single-point value (fresh option): zero.
57    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    /// Amount paid by a cash-or-nothing binary (ignored for asset-or-nothing).
116    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    /// Second barrier level: `Some` makes this a **double** barrier (the
126    /// corridor `[min, max]` of the two levels; `direction` is ignored).
127    pub barrier2: Option<f64>,
128    /// Rebate paid on the knock event (knock-out) or at expiry when the
129    /// option never knocks in (knock-in). Zero = no rebate.
130    pub rebate: f64,
131    /// Knock-out rebate timing: at the touch (`true`, analytic engine
132    /// only) or at expiry (`false`; also the Monte Carlo convention).
133    pub rebate_at_hit: bool,
134}
135
136/// Barrier payoff: `payoff` is the underlying vanilla leg (used by the
137/// analytic building blocks and as the terminal leg of path pricing);
138/// `path_payoff` applies discretely monitored knock logic to a full path.
139/// The Monte Carlo engine additionally applies a Brownian-bridge crossing
140/// correction, so its effective monitoring is continuous.
141impl 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        // rebate legs pay at expiry under path pricing: a knocked-out
164        // path collects the rebate, a never-in path of a knock-in does
165        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
204/// Asian payoff: the average is taken over the monitored path points
205/// (equally spaced, spot excluded). Fixed strike pays on the average
206/// against the strike; floating strike pays on the terminal spot against
207/// the average.
208impl Payoff for AsianPayoff {
209    /// Degenerate single-point average (used for intrinsic display only;
210    /// engines route Asians through `path_payoff`).
211    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
317/// Binary (digital) payoff, strictly in the money beyond the strike:
318/// cash-or-nothing pays `cash`, asset-or-nothing pays the underlying level.
319impl 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/// Contract terms and trade identity — **no market state**. Immutable
351/// for the life of the trade; everything that moves with the market
352/// lives in [`EquityMarketData`].
353#[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    /// When set, the underlying is a future priced with Black-76
366    /// (the market spot is the futures price `F`), settled either with an
367    /// up-front discounted premium or futures-style margined. European
368    /// vanilla only, on the Analytical engine.
369    pub futures_settlement: Option<crate::equity::black76::FuturesSettlement>,
370    pub multiplier: f64,
371
372    // trade info (candidate for a future Trade struct)
373    pub current_price: Quote,
374    pub entry_price: f64,
375    pub long_short: LongShort,
376}
377
378/// The market state one equity instrument is currently **bound to** — the
379/// pricing-view companion of the contract (QuantLib's process, Strata's
380/// provider). Resolved from / snapshotted to the typed
381/// [`Market`](crate::core::market::Market) store; swapped wholesale by
382/// [`EquityOption::with_market`].
383#[derive(Debug, Clone)]
384pub struct EquityMarketData {
385    /// The as-of date of this market snapshot; anchors every year fraction.
386    pub valuation_date: NaiveDate,
387    pub spot: Quote,
388    pub dividend_yield: f64,
389    /// Continuous stock borrow (repo) cost; part of the carry alongside
390    /// the dividend yield.
391    pub borrow_cost: f64,
392    /// Discrete cash dividend forecasts (ex-date, amount per share).
393    /// Analytic, tree and terminal Monte Carlo engines use the escrowed
394    /// model (spot minus PV of dividends); path-wise Monte Carlo and
395    /// finite difference apply the jumps at the ex-dates.
396    pub cash_dividends: Vec<(NaiveDate, f64)>,
397    /// Volatility surface; a flat surface represents a single constant vol.
398    /// `Arc`-shared with the [`Market`](crate::core::market::Market) store:
399    /// rebinding an instrument is a refcount bump, and replacing the
400    /// surface means installing a **new** `Arc` (copy-on-write), never
401    /// mutating through it.
402    pub vol_surface: Arc<VolSurface>,
403    /// Discounting curve anchored at `valuation_date`; discount factors are
404    /// the source of truth, rates are derived views. `Arc`-shared and
405    /// copy-on-write, like `vol_surface`.
406    pub discount_curve: Arc<YieldCurve>,
407}
408
409#[derive(Debug)]
410pub struct EquityOption {
411    /// The contract (and trade identity): pure data, never market state.
412    pub base: EquityOptionBase,
413    /// The market this instrument is currently bound to.
414    pub market: EquityMarketData,
415    pub payoff: Box<dyn Payoff>,
416    /// The numerical method, carrying its own settings.
417    pub engine: PricingEngine,
418    /// The dynamics of the underlying (GBM, local vol, or Heston with
419    /// its parameters); consulted by the MC, FD and analytic engines.
420    pub model: Model,
421}
422
423// manual impl: `payoff` clones through the trait object, engine and model
424// are Copy
425impl 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    /// The Monte Carlo settings. Invariant: only called on the Monte
439    /// Carlo engine's code paths (the dispatch guarantees it).
440    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    /// Heston parameters. Invariant: only called on Heston-model code
455    /// paths (the model dispatch guarantees it).
456    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    /// Test-only shortcuts for tweaking engine configuration in place;
471    /// production code configures engines through the builder.
472    #[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    /// Build an option from contract data, panicking on any invalid field.
491    /// Fallible callers (batch pricing, services) should use
492    /// [`EquityOption::try_from_json`].
493    pub fn from_json(data: &EquityOptionData) -> Box<EquityOption> {
494        Self::try_from_json(data).unwrap_or_else(|e| panic!("{e}"))
495    }
496
497    /// Build an option from contract data, reporting the offending field in
498    /// the error instead of panicking.
499    ///
500    /// This is a thin translation layer: it parses JSON-level fields
501    /// (dates, enum strings) into typed values and feeds them through
502    /// [`EquityOptionBuilder`], which owns all domain validation and
503    /// assembly — both construction paths share one set of checks.
504    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            // strike is set by the contract mechanics for these payoffs
519            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        // ── market objects ──────────────────────────────────────────────
538        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        // ── exercise style ──────────────────────────────────────────────
570        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            // unknown styles fall back to European, as before
582            _ => 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        // ── payoff ──────────────────────────────────────────────────────
595        builder = match payoff_type {
596            // not reachable from JSON yet: PayoffType::from_str does not
597            // produce Accumulator; build through EquityOptionBuilder
598            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                // a second level makes it a double barrier: the corridor
667                // between the two levels (direction is then ignored)
668                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                // a coupon barrier makes it a phoenix; memory is inert
759                // without one
760                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        // ── engine (it carries only its own configuration) ──────────────
789        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        // trade and reporting metadata the builder does not model
835        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
848/// Parse a JSON date-string list into `NaiveDate`s. Ordering and range
849/// validation happens in [`EquityOptionBuilder::build`]; this only
850/// handles the string format, naming `field` in errors.
851fn 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    /// True when the underlying is a future priced with Black-76.
867    pub fn is_futures_option(&self) -> bool {
868        self.futures_settlement.is_some()
869    }
870    /// The contract's currency code, falling back to
871    /// [`DEFAULT_CURRENCY`](crate::core::market::DEFAULT_CURRENCY) when the
872    /// contract does not state one — used as the [`Discount`]
873    /// (crate::core::market::Discount) key into a [`Market`]
874    /// (crate::core::market::Market).
875    pub fn currency_code(&self) -> &str {
876        self.currency.as_deref().unwrap_or(crate::core::market::DEFAULT_CURRENCY)
877    }
878}
879
880// Contract-and-market quantities: these straddle the boundary (a year
881// fraction needs the contract's maturity AND the market's valuation
882// date), so they live on the pairing — the option — not on either half.
883impl 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    /// Discount factor from the valuation date to maturity, off the curve.
888    pub fn maturity_discount_factor(&self) -> f64 {
889        self.market.discount_curve.df(self.time_to_maturity())
890    }
891    /// Continuously compounded zero rate to maturity implied by the curve.
892    /// This is the `r` that enters d1/d2; it is consistent with
893    /// [`maturity_discount_factor`](Self::maturity_discount_factor) by construction.
894    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    /// Total continuous carry on the underlying: dividend yield plus
900    /// borrow cost. This is the "q" every pricing formula uses.
901    pub fn carry_yield(&self) -> f64 {
902        self.market.dividend_yield + self.market.borrow_cost
903    }
904    /// Escrow value of the cash dividends with ex-dates inside the option's
905    /// life: the amount to carve out of spot so the risky stub reproduces
906    /// the jump-model forward.
907    ///
908    /// Each dividend is discounted at the **net carry rate** `r - carry`,
909    /// not the risk-free rate, so that the escrow accretes at the same rate
910    /// the risky stub grows (`effective_spot` is grown at `r - carry` in
911    /// [`forward_price`](Self::forward_price)). This makes the analytic
912    /// forward match the well-defined jump model
913    /// `F = (S - D e^{-(r-carry)t}) e^{(r-carry)T}` used by the FD and
914    /// path-wise Monte Carlo engines. With no continuous carry this
915    /// reduces to plain risk-free discounting.
916    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                // df(t) = e^{-r t}; multiplying by e^{carry t} discounts at
927                // the net carry (r - carry), generalizing to any curve shape.
928                amount * self.market.discount_curve.df(t) * (carry * t).exp()
929            })
930            .sum()
931    }
932    /// Escrowed-model spot: the quoted spot minus the PV of cash dividends
933    /// paid over the option's life. This is the lognormal driver for the
934    /// analytic and terminal-simulation engines.
935    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    /// Forward price of the underlying at maturity: escrowed spot grown at
941    /// the carry-adjusted rate, `(S - PV(divs)) * exp((r - q - b) * T)`.
942    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    /// Black volatility for this option's strike and expiry, read off the
947    /// surface (a flat surface returns its single vol).
948    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        // Black-Scholes-Merton d1 on the escrowed spot and total carry
955        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    /// Implied Black-Scholes volatility for `option_price` (safeguarded
979    /// Newton with arbitrage-bound checks); does not modify the option.
980    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    /// Implied vol for `option_price`; leaves the option holding a flat
992    /// surface at the solved vol. Panics on arbitrage-violating prices —
993    /// use [`try_imp_vol`](Self::try_imp_vol) to handle those gracefully.
994    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    /// Reject engine/model/payoff combinations the library cannot price,
1018    /// with an error naming the engine that can.
1019    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        // American and Bermudan share the early-exercise engine rules
1023        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    /// Value, all nine Greeks, and (on the Monte Carlo engine) the
1130    /// standard error, from one call — batched through the central
1131    /// sensitivity engine ([`crate::equity::greeks`]), which shares
1132    /// solves and reprices across the Greeks.
1133    fn price(&self) -> Result<PricingResult, RustyQLibError> {
1134        self.check_engine_support()?;
1135        Ok(crate::equity::greeks::pricing_result(self))
1136    }
1137}
1138
1139/// Greeks route through the central sensitivity engine
1140/// ([`crate::equity::greeks`]): the FD and Binomial engines read
1141/// delta/gamma/theta off their own grid/tree with higher orders from
1142/// bumped solutions; the analytic engine uses the payoff-aware
1143/// Black-Scholes closed forms (including Black-76 futures); the bump
1144/// engines (Monte Carlo with common random numbers, BAW,
1145/// Bjerksund-Stensland, analytic Heston) share one set of
1146/// central-difference stencils with per-engine bump sizes.
1147impl 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    /// Vanna: change in delta per unit change in implied volatility.
1168    pub fn vanna(&self) -> f64 {
1169        greeks::vanna(self)
1170    }
1171    /// Charm: change in delta per year of calendar time.
1172    pub fn charm(&self) -> f64 {
1173        greeks::charm(self)
1174    }
1175    /// Delta elasticity (`S * gamma / delta`), also called percentage gamma.
1176    pub fn gamma_p(&self) -> f64 {
1177        greeks::gamma_p(self)
1178    }
1179    /// Zomma: change in gamma per unit change in implied volatility.
1180    pub fn zomma(&self) -> f64 {
1181        greeks::zomma(self)
1182    }
1183    /// Volga (vomma): change in vega per unit change in implied volatility.
1184    pub fn volga(&self) -> f64 {
1185        greeks::volga(self)
1186    }
1187    /// Reprice under a shifted market: spot `+ d_spot`, a parallel implied
1188    /// vol shift `+ d_vol`, rate `+ d_rate`, and `d_time` years of elapsed
1189    /// calendar time. `price_with(0, 0, 0, 0)` is the base price; the
1190    /// portfolio PnL attribution uses the difference of the two.
1191    ///
1192    /// Monte Carlo repricing uses common random numbers, so the difference is
1193    /// free of sampling noise.
1194    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            // price_with shifts the maturity, so elapsed calendar time enters
1220            // with the opposite sign
1221            _ 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// #[cfg(test)]
1230// mod tests {
1231//     //write a unit test for from_json
1232//     use super::*;
1233//     use crate::core::utils::{Contract,MarketData};
1234//     use crate::core::trade::OptionType;
1235//     use crate::core::trade::Transection;
1236//     use crate::core::utils::ContractStyle;
1237//     use crate::core::termstructure::YieldTermStructure;
1238//     use crate::core::quotes::Quote;
1239//     use chrono::{Datelike, Local, NaiveDate};
1240//     #[test]
1241//     fn test_from_json() {
1242//         let data = Contract {
1243//             action: "PV".to_string(),
1244//             market_data: Some(MarketData {
1245//                 underlying_price: 100.0,
1246//                 strike_price: 100.0,
1247//                 volatility: None,
1248//                 option_price: Some(10.0),
1249//                 risk_free_rate: Some(0.05),
1250//                 dividend: Some(0.0),
1251//                 maturity: "2024-01-01".to_string(),
1252//                 option_type: "C".to_string(),
1253//                 simulation: None
1254//             }),
1255//             pricer: "Analytical".to_string(),
1256//             asset: "".to_string(),
1257//             style: Some("European".to_string()),
1258//             rate_data: None
1259//         };
1260//         let option = EquityOption::from_json(&data);
1261//         assert_eq!(option.option_type, OptionType::Call);
1262//         assert_eq!(option.transection, Transection::Buy);
1263//         assert_eq!(option.underlying_price.value, 100.0);
1264//         assert_eq!(option.strike_price, 100.0);
1265//         assert_eq!(option.current_price.value, 10.0);
1266//         assert_eq!(option.dividend_yield, 0.0);
1267//         assert_eq!(option.volatility, 0.2);
1268//         assert_eq!(option.maturity_date, NaiveDate::from_ymd(2024, 1, 1));
1269//         assert_eq!(option.valuation_date, Local::today().naive_utc());
1270//         assert_eq!(option.engine, Engine::BlackScholes);
1271//         assert_eq!(option.style, ContractStyle::European);
1272//     }
1273// }