Skip to main content

black_76/
types.rs

1//! Core types: `OptionType`, `SolverMethod`, `SolverStatus`, `SolverResult`,
2//! `InstrumentGreeks`.
3//!
4//! All numerical math uses `f64`. The crate exposes no `Decimal`-typed APIs.
5
6/// Option type: call or put.
7///
8/// A typed convenience/serde label for call-vs-put. The numeric API
9/// ([`price`](crate::price), [`compute_greeks`](crate::compute_greeks),
10/// [`solve_iv`](crate::solve_iv), …) selects via an `is_call: bool` argument;
11/// bridge to it with [`OptionType::is_call`]. `OptionType`-taking convenience
12/// overloads can be added in a future minor version without a breaking change.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15pub enum OptionType {
16    /// European call option (right to buy at strike).
17    Call,
18    /// European put option (right to sell at strike).
19    Put,
20}
21
22impl OptionType {
23    /// Returns `true` if this is a call.
24    #[inline]
25    #[must_use]
26    pub const fn is_call(self) -> bool {
27        matches!(self, OptionType::Call)
28    }
29}
30
31impl std::fmt::Display for OptionType {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        match self {
34            OptionType::Call => write!(f, "Call"),
35            OptionType::Put => write!(f, "Put"),
36        }
37    }
38}
39
40// ---------------------------------------------------------------------------
41// IV Solver types
42// ---------------------------------------------------------------------------
43
44/// Method used by the IV solver.
45///
46/// New variants may be added in future minor versions; match exhaustively
47/// at your own peril (the enum is `#[non_exhaustive]`).
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
50#[non_exhaustive]
51pub enum SolverMethod {
52    /// Newton-Raphson iteration.
53    NewtonRaphson,
54    /// Brent's method (bracketed root-finding).
55    Brent,
56}
57
58/// Why the IV solver returned: the precise outcome behind a (non-)convergence
59/// (see [`SolverResult`]).
60///
61/// New variants may be added in future minor versions (the enum is
62/// `#[non_exhaustive]`); always include a wildcard arm when matching.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
64#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
65#[non_exhaustive]
66pub enum SolverStatus {
67    /// A volatility was found within tolerance. `iv` is finite and usable.
68    Converged,
69    /// Time to expiry was below the near-expiry cutoff; the option should be
70    /// priced intrinsically and no implied volatility is solved. `iv` is
71    /// [`f64::NAN`].
72    NearExpiryIntrinsic,
73    /// The market price was zero or negative; no implied volatility exists.
74    /// `iv` is [`f64::NAN`].
75    NonPositivePrice,
76    /// The market price was below the discounted intrinsic value (the
77    /// Black-76 no-arbitrage lower bound). `iv` is [`f64::NAN`].
78    BelowIntrinsic,
79    /// No root exists in `[iv_min, iv_max]`; the price is unattainable for
80    /// any volatility in that range. `iv` is [`f64::NAN`].
81    NoBracketInRange,
82    /// Vega is below `vega_floor`, so the implied volatility is not
83    /// numerically identifiable from the price. `iv` is [`f64::NAN`].
84    NotIdentifiable,
85    /// The solver exhausted its iteration budget without meeting the
86    /// volatility-space tolerance. `iv` is [`f64::NAN`].
87    MaxIterations,
88    /// An input (market price, `F`, `K`, `T`, or `r`) was non-finite, so no
89    /// solve was attempted. `iv` is [`f64::NAN`]. Validate untrusted or
90    /// exchange-sourced inputs before calling.
91    InvalidInput,
92}
93
94/// Result of an [`iv_solver::solve_iv`](crate::iv_solver::solve_iv) attempt.
95///
96/// **Check `converged` (or `status`) before consuming `iv`.** Whenever the
97/// solver does not converge, `iv` is [`f64::NAN`] and `status` explains why.
98/// `converged` is exactly `status == SolverStatus::Converged`.
99///
100/// New fields may be added in future minor versions; construct via the
101/// solver, not via field initializers.
102#[derive(Debug, Clone)]
103#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
104#[non_exhaustive]
105pub struct SolverResult {
106    /// Solved implied volatility (annualized). [`f64::NAN`] unless `status` is
107    /// [`SolverStatus::Converged`].
108    pub iv: f64,
109    /// Solver method used to produce this result.
110    pub method: SolverMethod,
111    /// Number of iterations taken.
112    pub iterations: u32,
113    /// Whether the solver converged. Equivalent to
114    /// `status == SolverStatus::Converged`.
115    pub converged: bool,
116    /// The precise outcome of the solve (the reason behind `converged`).
117    pub status: SolverStatus,
118    /// Residual `|model_price - market_price|` at the solution (or at the
119    /// best endpoint examined when no root was found).
120    pub residual: f64,
121}
122
123// ---------------------------------------------------------------------------
124// Greeks
125// ---------------------------------------------------------------------------
126
127/// First-order Greeks (plus gamma) for a single option.
128///
129/// Sign and unit conventions:
130/// - **delta**: dimensionless; `df * N(d1)` (call) or `df * (N(d1) - 1)` (put).
131/// - **gamma**: `d^2 price / dF^2`, per unit forward (raw, not scaled);
132///   identical for calls and puts.
133/// - **vega**: price change per **1%** absolute change in volatility
134///   (`raw_dv_dsigma / 100`); identical for calls and puts.
135/// - **theta**: per-calendar-day time decay (year = 365.25 days); negative
136///   for long positions.
137/// - **rho**: price change per **1%** absolute change in the rate
138///   (`dprice/dr / 100`); negative under Black-76 (`dC/dr = -T*C`).
139///
140/// New fields may be added in future minor versions.
141#[derive(Debug, Clone, Copy)]
142#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
143#[non_exhaustive]
144pub struct InstrumentGreeks {
145    /// Delta: sensitivity to the forward, `df * N(d1)` (call) or `df * (N(d1) - 1)` (put).
146    pub delta: f64,
147    /// Gamma: `d^2 price / dF^2 = df * n(d1) / (F * sigma * sqrt(T))` (per unit forward, raw).
148    pub gamma: f64,
149    /// Vega: price change for a 1% absolute change in IV (per-1%, not per-1).
150    pub vega: f64,
151    /// Theta: per-day price decay.
152    pub theta: f64,
153    /// Rho: price change for a 1% absolute change in the rate (per-1%).
154    pub rho: f64,
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn option_type_display() {
163        assert_eq!(format!("{}", OptionType::Call), "Call");
164        assert_eq!(format!("{}", OptionType::Put), "Put");
165    }
166
167    #[test]
168    fn option_type_is_call() {
169        assert!(OptionType::Call.is_call());
170        assert!(!OptionType::Put.is_call());
171    }
172}