Skip to main content

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}