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}