black_76/config.rs
1//! Solver configuration.
2//!
3//! The default values (50 NR iterations, 1e-8 price tolerance, IV in [0.01, 5.0])
4//! are tuned for crypto options pricing but work generally. Use
5//! [`SolverConfig::builder`] for fluent construction.
6
7/// Configuration for the implied-volatility solver.
8///
9/// Construct via [`SolverConfig::default`] for sensible defaults, or via
10/// [`SolverConfig::builder`] for selective overrides:
11///
12/// ```
13/// use black_76::SolverConfig;
14///
15/// let cfg = SolverConfig::builder()
16/// .iv_min(0.001)
17/// .iv_max(10.0)
18/// .price_tolerance(1e-10)
19/// .build();
20/// assert_eq!(cfg.iv_max, 10.0);
21/// ```
22///
23/// New fields may be added in future minor versions.
24#[derive(Debug, Clone, PartialEq)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26#[cfg_attr(feature = "serde", serde(default))]
27#[non_exhaustive]
28pub struct SolverConfig {
29 /// Maximum Newton-Raphson iterations before switching to Brent.
30 pub nr_max_iterations: u32,
31 /// Price residual reported by the solver, and the secondary price sanity
32 /// threshold for the bracket-collapse path. Convergence itself is decided
33 /// in volatility space (see [`iv_tolerance`](Self::iv_tolerance)).
34 pub price_tolerance: f64,
35 /// Volatility-space convergence tolerance. The solver converges when the
36 /// Newton step (or the Brent bracket width) in `sigma` falls below this
37 /// value. Deciding convergence in `sigma`-space behaves the same whether the
38 /// forward is `1.0` or `100_000.0`, unlike an absolute price tolerance.
39 pub iv_tolerance: f64,
40 /// Minimum vega to continue Newton-Raphson iterations. Below this, the
41 /// solver falls back to Brent's method to avoid the explosive Newton step
42 /// `(price - target) / vega` as vega -> 0 (deep OTM, near expiry).
43 pub vega_floor: f64,
44 /// Minimum allowed IV (annualized). The solver clamps every NR step to
45 /// `[iv_min, iv_max]`. Default 0.01 (1%).
46 pub iv_min: f64,
47 /// Maximum allowed IV (annualized). Default 5.0 (500%).
48 pub iv_max: f64,
49 /// Maximum Brent's-method iterations.
50 pub brent_max_iterations: u32,
51 /// Near-expiry cutoff in hours. Below this, the solver bypasses NR/Brent
52 /// and returns `iv = NaN`, `converged = false`,
53 /// `status = SolverStatus::NearExpiryIntrinsic`; price the option
54 /// intrinsically yourself rather than implying a volatility.
55 pub near_expiry_cutoff_hours: f64,
56}
57
58impl Default for SolverConfig {
59 fn default() -> Self {
60 Self {
61 nr_max_iterations: 50,
62 price_tolerance: 1e-8,
63 iv_tolerance: 1e-7,
64 vega_floor: 1e-10,
65 iv_min: 0.01,
66 iv_max: 5.0,
67 brent_max_iterations: 100,
68 near_expiry_cutoff_hours: 2.0,
69 }
70 }
71}
72
73impl SolverConfig {
74 /// Returns a fresh builder seeded with the default config.
75 pub fn builder() -> SolverConfigBuilder {
76 SolverConfigBuilder {
77 inner: SolverConfig::default(),
78 }
79 }
80}
81
82/// Builder for [`SolverConfig`].
83///
84/// Each setter consumes `self` and returns a new builder; call [`build`](Self::build)
85/// to produce the final config. All setters are `const fn`, so a config can be
86/// assembled in a `const` context.
87#[derive(Debug, Clone)]
88#[must_use = "a builder does nothing unless you call `.build()`"]
89pub struct SolverConfigBuilder {
90 inner: SolverConfig,
91}
92
93impl SolverConfigBuilder {
94 /// Sets `nr_max_iterations`.
95 pub const fn nr_max_iterations(mut self, value: u32) -> Self {
96 self.inner.nr_max_iterations = value;
97 self
98 }
99
100 /// Sets `price_tolerance`.
101 pub const fn price_tolerance(mut self, value: f64) -> Self {
102 self.inner.price_tolerance = value;
103 self
104 }
105
106 /// Sets `iv_tolerance`.
107 pub const fn iv_tolerance(mut self, value: f64) -> Self {
108 self.inner.iv_tolerance = value;
109 self
110 }
111
112 /// Sets `vega_floor`.
113 pub const fn vega_floor(mut self, value: f64) -> Self {
114 self.inner.vega_floor = value;
115 self
116 }
117
118 /// Sets `iv_min`.
119 pub const fn iv_min(mut self, value: f64) -> Self {
120 self.inner.iv_min = value;
121 self
122 }
123
124 /// Sets `iv_max`.
125 pub const fn iv_max(mut self, value: f64) -> Self {
126 self.inner.iv_max = value;
127 self
128 }
129
130 /// Sets `brent_max_iterations`.
131 pub const fn brent_max_iterations(mut self, value: u32) -> Self {
132 self.inner.brent_max_iterations = value;
133 self
134 }
135
136 /// Sets `near_expiry_cutoff_hours`.
137 pub const fn near_expiry_cutoff_hours(mut self, value: f64) -> Self {
138 self.inner.near_expiry_cutoff_hours = value;
139 self
140 }
141
142 /// Consumes the builder and returns the configured [`SolverConfig`].
143 #[must_use]
144 pub const fn build(self) -> SolverConfig {
145 self.inner
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn default_values() {
155 let cfg = SolverConfig::default();
156 assert_eq!(cfg.nr_max_iterations, 50);
157 assert_eq!(cfg.iv_min, 0.01);
158 assert_eq!(cfg.iv_max, 5.0);
159 }
160
161 #[test]
162 fn builder_overrides() {
163 let cfg = SolverConfig::builder()
164 .iv_min(0.005)
165 .iv_max(8.0)
166 .price_tolerance(1e-10)
167 .build();
168 assert_eq!(cfg.iv_min, 0.005);
169 assert_eq!(cfg.iv_max, 8.0);
170 assert_eq!(cfg.price_tolerance, 1e-10);
171 // Untouched defaults preserved
172 assert_eq!(cfg.nr_max_iterations, 50);
173 }
174}