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}