Skip to main content

black_76/
inputs.rs

1//! Ergonomic, typo-resistant input structs.
2//!
3//! The free functions in [`pricing`](crate::pricing), [`greeks`](crate::greeks),
4//! and [`iv_solver`](crate::iv_solver) take several positional `f64` arguments,
5//! which is easy to mis-order (`f`/`k`, `t`/`r`). [`BlackInputs`] and
6//! [`IvQuery`] name every field, so call sites are self-documenting and an
7//! argument swap becomes a field name rather than a silent wrong answer. The
8//! free functions remain available and are not deprecated; this is a thin,
9//! zero-cost convenience layer over them.
10
11use crate::config::SolverConfig;
12use crate::greeks::compute_greeks;
13use crate::iv_solver::solve_iv;
14use crate::pricing::{call_price, d1_d2, intrinsic_value, price, put_price, vega};
15use crate::types::{InstrumentGreeks, SolverResult};
16
17/// Named inputs for Black-76 pricing and Greeks: forward `f`, strike `k`,
18/// time-to-expiry `t` (in years), volatility `sigma`, and rate `r`.
19///
20/// A typo-resistant alternative to the positional free functions: every field
21/// is named, so `f`/`k` and `t`/`r` cannot be silently swapped.
22///
23/// ```
24/// use black_76::{BlackInputs, call_price};
25/// let inputs = BlackInputs::new(100.0, 100.0, 1.0, 0.20, 0.0);
26/// let c = inputs.call_price();
27/// assert!((c - call_price(100.0, 100.0, 1.0, 0.20, 0.0)).abs() < 1e-12);
28/// let g = inputs.greeks(true);
29/// assert!(g.delta > 0.0 && g.gamma > 0.0);
30/// ```
31///
32/// New fields may be added in future minor versions; construct via
33/// [`new`](Self::new) (the type is `#[non_exhaustive]`, so struct-literal
34/// construction is unavailable to downstream crates). The public fields
35/// remain readable and serde-serializable.
36#[derive(Debug, Clone, Copy, PartialEq)]
37#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
38#[non_exhaustive]
39pub struct BlackInputs {
40    /// Forward / futures price.
41    pub f: f64,
42    /// Strike.
43    pub k: f64,
44    /// Time to expiry, in years.
45    pub t: f64,
46    /// Volatility (annualized).
47    pub sigma: f64,
48    /// Risk-free rate (annualized, continuously compounded).
49    pub r: f64,
50}
51
52impl BlackInputs {
53    /// Construct inputs from forward, strike, time (years), volatility, rate.
54    #[must_use]
55    pub const fn new(f: f64, k: f64, t: f64, sigma: f64, r: f64) -> Self {
56        Self { f, k, t, sigma, r }
57    }
58
59    /// `(d1, d2)` for these inputs (independent of `r`).
60    #[must_use]
61    pub fn d1_d2(&self) -> (f64, f64) {
62        d1_d2(self.f, self.k, self.t, self.sigma)
63    }
64
65    /// Black-76 call price.
66    #[must_use]
67    pub fn call_price(&self) -> f64 {
68        call_price(self.f, self.k, self.t, self.sigma, self.r)
69    }
70
71    /// Black-76 put price.
72    #[must_use]
73    pub fn put_price(&self) -> f64 {
74        put_price(self.f, self.k, self.t, self.sigma, self.r)
75    }
76
77    /// Call or put price, selected by `is_call`.
78    #[must_use]
79    pub fn price(&self, is_call: bool) -> f64 {
80        price(self.f, self.k, self.t, self.sigma, self.r, is_call)
81    }
82
83    /// Vega (price change per 1.0 absolute change in volatility).
84    #[must_use]
85    pub fn vega(&self) -> f64 {
86        vega(self.f, self.k, self.t, self.sigma, self.r)
87    }
88
89    /// First-order Greeks (delta, gamma, vega-per-1%, theta-per-day, rho-per-1%).
90    #[must_use]
91    pub fn greeks(&self, is_call: bool) -> InstrumentGreeks {
92        compute_greeks(self.f, self.k, self.t, self.sigma, self.r, is_call)
93    }
94
95    /// Intrinsic value: `max(F - K, 0)` (call) or `max(K - F, 0)` (put).
96    #[must_use]
97    pub fn intrinsic_value(&self, is_call: bool) -> f64 {
98        intrinsic_value(self.f, self.k, is_call)
99    }
100}
101
102/// A market quote to invert for implied volatility: the observed
103/// `market_price` plus the option's `f`, `k`, `t`, `r`, and `is_call`.
104///
105/// ```
106/// use black_76::{IvQuery, SolverConfig, call_price};
107/// let market = call_price(100.0, 100.0, 1.0, 0.20, 0.0);
108/// let result = IvQuery::new(market, 100.0, 100.0, 1.0, 0.0, true)
109///     .solve(&SolverConfig::default());
110/// assert!(result.converged);
111/// assert!((result.iv - 0.20).abs() < 1e-6);
112/// ```
113///
114/// New fields may be added in future minor versions.
115#[derive(Debug, Clone, Copy, PartialEq)]
116#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
117#[non_exhaustive]
118pub struct IvQuery {
119    /// Observed option market price.
120    pub market_price: f64,
121    /// Forward / futures price.
122    pub f: f64,
123    /// Strike.
124    pub k: f64,
125    /// Time to expiry, in years.
126    pub t: f64,
127    /// Risk-free rate (annualized, continuously compounded).
128    pub r: f64,
129    /// `true` for a call quote, `false` for a put.
130    pub is_call: bool,
131}
132
133impl IvQuery {
134    /// Construct a quote from market price, forward, strike, time, rate, flag.
135    #[must_use]
136    pub const fn new(market_price: f64, f: f64, k: f64, t: f64, r: f64, is_call: bool) -> Self {
137        Self {
138            market_price,
139            f,
140            k,
141            t,
142            r,
143            is_call,
144        }
145    }
146
147    /// Solve for implied volatility. Like [`solve_iv`], this returns a
148    /// [`SolverResult`]: **check `converged` before consuming `iv`** (it is
149    /// `NaN` on every non-converged path).
150    #[must_use]
151    pub fn solve(&self, config: &SolverConfig) -> SolverResult {
152        solve_iv(
153            self.market_price,
154            self.f,
155            self.k,
156            self.t,
157            self.r,
158            self.is_call,
159            config,
160        )
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn black_inputs_match_free_functions() {
170        let (f, k, t, s, r) = (100.0, 105.0, 0.5, 0.25, 0.03);
171        let bi = BlackInputs::new(f, k, t, s, r);
172        assert_eq!(bi.call_price(), call_price(f, k, t, s, r));
173        assert_eq!(bi.put_price(), put_price(f, k, t, s, r));
174        assert_eq!(bi.price(true), price(f, k, t, s, r, true));
175        assert_eq!(bi.price(false), price(f, k, t, s, r, false));
176        assert_eq!(bi.vega(), vega(f, k, t, s, r));
177        assert_eq!(bi.d1_d2(), d1_d2(f, k, t, s));
178        assert_eq!(bi.intrinsic_value(true), intrinsic_value(f, k, true));
179
180        let g = bi.greeks(true);
181        let g2 = compute_greeks(f, k, t, s, r, true);
182        assert_eq!(g.delta, g2.delta);
183        assert_eq!(g.gamma, g2.gamma);
184        assert_eq!(g.vega, g2.vega);
185        assert_eq!(g.theta, g2.theta);
186        assert_eq!(g.rho, g2.rho);
187    }
188
189    #[test]
190    fn iv_query_round_trip() {
191        let cfg = SolverConfig::default();
192        let market = call_price(100.0, 100.0, 1.0, 0.20, 0.0);
193        let result = IvQuery::new(market, 100.0, 100.0, 1.0, 0.0, true).solve(&cfg);
194        assert!(result.converged);
195        assert!((result.iv - 0.20).abs() < 1e-6);
196    }
197
198    #[test]
199    fn const_construction() {
200        // Both constructors are usable in a `const` context (this compiles).
201        const BI: BlackInputs = BlackInputs::new(100.0, 100.0, 1.0, 0.20, 0.0);
202        const Q: IvQuery = IvQuery::new(7.96, 100.0, 100.0, 1.0, 0.0, true);
203        // Exercise the values at runtime (avoids asserting on constants).
204        let (bi, q) = (BI, Q);
205        assert_eq!(bi.k, 100.0);
206        assert!(q.is_call);
207    }
208}