Skip to main content

rustyqlib/core/
results.rs

1//! Structured pricing results: everything one pricing call produces.
2
3use serde::{Deserialize, Serialize};
4
5/// First- and second-order sensitivities of a priced instrument.
6///
7/// Instruments that do not report a sensitivity leave it at `0.0`
8/// (e.g. spot Greeks of return-based payoffs such as cliquets, which are
9/// spot-homogeneous by construction).
10#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
11pub struct Greeks {
12    /// Change in value per unit change in the underlying, `dV/dS`.
13    pub delta: f64,
14    /// Change in delta per unit change in the underlying, `d²V/dS²`.
15    pub gamma: f64,
16    /// Change in value per unit change in implied volatility, `dV/dσ`.
17    pub vega: f64,
18    /// Change in value per year of calendar time, `dV/dt`.
19    pub theta: f64,
20    /// Change in value per unit change in the risk-free rate, `dV/dr`.
21    pub rho: f64,
22    /// Change in delta per unit change in implied volatility, `d²V/(dS dσ)`.
23    pub vanna: f64,
24    /// Change in delta per year of calendar time, `d²V/(dS dt)`.
25    pub charm: f64,
26    /// Delta elasticity, `S * gamma / delta`.
27    pub gamma_p: f64,
28    /// Change in gamma per unit change in implied volatility, `d³V/(dS² dσ)`.
29    pub zomma: f64,
30}
31
32/// The result of a single [`price()`](crate::core::traits::Instrument::price)
33/// call: present value, sensitivities, and the Monte Carlo standard error
34/// when a simulation engine produced the value.
35#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
36pub struct PricingResult {
37    /// Present value.
38    pub pv: f64,
39    /// Sensitivities of `pv`.
40    pub greeks: Greeks,
41    /// Monte Carlo standard error of `pv`; `None` for deterministic engines.
42    pub std_err: Option<f64>,
43}
44
45impl PricingResult {
46    /// A result with the given present value, zero Greeks and no standard
47    /// error — the shape produced by deterministic pricers of instruments
48    /// that do not report sensitivities.
49    pub fn from_pv(pv: f64) -> Self {
50        PricingResult { pv, greeks: Greeks::default(), std_err: None }
51    }
52}
53
54#[cfg(test)]
55mod tests {
56    use crate::core::trade::PutOrCall;
57    use crate::core::traits::Instrument;
58    use crate::equity::builder::EquityOptionBuilder;
59    use crate::equity::utils::Engine;
60
61    fn vanilla(engine: Engine) -> crate::equity::vanilla_option::EquityOption {
62        EquityOptionBuilder::new()
63            .spot(100.0)
64            .strike(100.0)
65            .flat_vol(0.30)
66            .flat_rate(0.05)
67            .years_to_maturity(1.0)
68            .vanilla(PutOrCall::Call)
69            .engine(engine)
70            .build().expect("option must build")
71    }
72
73    #[test]
74    fn price_matches_individual_accessors() {
75        let option = vanilla(Engine::BlackScholes);
76        let result = option.price().unwrap();
77        assert_eq!(result.pv, option.npv());
78        assert_eq!(result.greeks.delta, option.delta());
79        assert_eq!(result.greeks.gamma, option.gamma());
80        assert_eq!(result.greeks.vega, option.vega());
81        assert_eq!(result.greeks.theta, option.theta());
82        assert_eq!(result.greeks.rho, option.rho());
83        assert_eq!(result.greeks.vanna, option.vanna());
84        assert_eq!(result.greeks.charm, option.charm());
85        assert_eq!(result.greeks.zomma, option.zomma());
86        assert_eq!(result.std_err, None, "deterministic engine has no std_err");
87    }
88
89    #[test]
90    fn monte_carlo_price_reports_std_err_and_reproducible_pv() {
91        let option = vanilla(Engine::MonteCarlo);
92        let result = option.price().unwrap();
93        let se = result.std_err.expect("MC engine must report a standard error");
94        assert!(se > 0.0 && se.is_finite());
95        // bit-reproducible MC: price() sees the same paths as npv()
96        assert_eq!(result.pv, option.npv());
97    }
98
99    #[test]
100    fn unsupported_combination_errors_through_price() {
101        use crate::core::errors::RustyQLibError;
102        // build() enforces engine support, so force the bad combination
103        // onto an already-built option to exercise the price()-time check
104        let mut option = vanilla(Engine::MonteCarlo);
105        option.payoff = Box::new(crate::equity::vanilla_option::LookbackPayoff {
106            put_or_call: PutOrCall::Call,
107            exercise_style: crate::core::utils::ContractStyle::European,
108            lookback_type: crate::equity::vanilla_option::LookbackType::FloatingStrike,
109        });
110        option.engine = crate::equity::utils::PricingEngine::Binomial(Default::default());
111        match option.price() {
112            Err(RustyQLibError::UnsupportedEngine(msg)) => {
113                assert!(msg.contains("Binomial"), "should explain the refusal: {msg}")
114            }
115            other => panic!("expected UnsupportedEngine, got {other:?}"),
116        }
117    }
118}