regit-svi 2.0.0

Arbitrage-free SVI volatility surfaces in pure Rust. Raw, Jump-Wings and SSVI parametrisations, calibration, and static-arbitrage checks. Zero dependencies.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
// Copyright 2026 Regit.io — Nicolas Koenig
// SPDX-License-Identifier: Apache-2.0

//! Calibration diagnostics and optimizer termination evidence.

use crate::no_arb::evidence::ArbitrageAssessment;

/// Raw SVI domain margins recomputed from the returned slice.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RawParameterMargins {
    slope: f64,
    correlation: f64,
    sigma: f64,
    minimum_variance: f64,
    arbitrage: f64,
}

impl RawParameterMargins {
    pub(crate) const fn new(
        slope: f64,
        correlation: f64,
        sigma: f64,
        minimum_variance: f64,
        arbitrage: f64,
    ) -> Self {
        Self {
            slope,
            correlation,
            sigma,
            minimum_variance,
            arbitrage,
        }
    }

    /// Distance from the `b >= 0` boundary.
    #[must_use]
    pub const fn slope(self) -> f64 {
        self.slope
    }
    /// Distance from the open `|rho| < 1` boundary.
    #[must_use]
    pub const fn correlation(self) -> f64 {
        self.correlation
    }
    /// Distance from the `sigma > 0` boundary.
    #[must_use]
    pub const fn sigma(self) -> f64 {
        self.sigma
    }
    /// Minimum total variance.
    #[must_use]
    pub const fn minimum_variance(self) -> f64 {
        self.minimum_variance
    }
    /// Signed margin from the final arbitrage assessment.
    #[must_use]
    pub const fn arbitrage(self) -> f64 {
        self.arbitrage
    }
}

/// SSVI parameter and feasibility margins recomputed from the returned model.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SurfaceParameterMargins {
    correlation: f64,
    phi_scale: f64,
    phi_shape: f64,
    butterfly: f64,
    calendar: f64,
}

impl SurfaceParameterMargins {
    pub(crate) const fn new(
        correlation: f64,
        phi_scale: f64,
        phi_shape: f64,
        butterfly: f64,
        calendar: f64,
    ) -> Self {
        Self {
            correlation,
            phi_scale,
            phi_shape,
            butterfly,
            calendar,
        }
    }
    /// Distance from the open `|rho| < 1` boundary.
    #[must_use]
    pub const fn correlation(self) -> f64 {
        self.correlation
    }
    /// Distance to the hard calibration-family scale boundary.
    #[must_use]
    pub const fn phi_scale(self) -> f64 {
        self.phi_scale
    }
    /// Distance to the hard calibration-family shape boundary; infinity for
    /// the one-parameter Heston family.
    #[must_use]
    pub const fn phi_shape(self) -> f64 {
        self.phi_shape
    }
    /// Signed global butterfly-envelope margin.
    #[must_use]
    pub const fn butterfly(self) -> f64 {
        self.butterfly
    }
    /// Signed calendar-condition margin.
    #[must_use]
    pub const fn calendar(self) -> f64 {
        self.calendar
    }
}

/// Parameter-transform and repair policy used to produce a returned model.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParameterizationEvidence {
    transform: &'static str,
    repair: &'static str,
}

impl ParameterizationEvidence {
    pub(crate) const fn new(transform: &'static str, repair: &'static str) -> Self {
        Self { transform, repair }
    }
    /// Hard transform used to map optimizer coordinates into the model domain.
    #[must_use]
    pub const fn transform(self) -> &'static str {
        self.transform
    }
    /// Projection, repair, or invalid-candidate policy.
    #[must_use]
    pub const fn repair(self) -> &'static str {
        self.repair
    }
}

/// Why a calibration search stopped.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TerminationReason {
    /// Objective and search geometry met their tolerances.
    ObjectiveConverged,
    /// The weighted gradient norm met its tolerance.
    GradientConverged,
    /// An accepted parameter step met its tolerance.
    StepConverged,
    /// No cost-reducing step was found; this is not convergence.
    Stagnated,
    /// The local normal equations remained singular.
    SingularModel,
    /// A required objective or residual evaluation was non-finite.
    NonFiniteEvaluation,
    /// The configured iteration budget was exhausted.
    MaximumIterations,
}

impl From<crate::numerics::OptimizerTermination> for TerminationReason {
    fn from(reason: crate::numerics::OptimizerTermination) -> Self {
        use crate::numerics::OptimizerTermination;
        match reason {
            OptimizerTermination::ObjectiveConverged => Self::ObjectiveConverged,
            OptimizerTermination::GradientConverged => Self::GradientConverged,
            OptimizerTermination::StepConverged => Self::StepConverged,
            OptimizerTermination::Stagnated => Self::Stagnated,
            OptimizerTermination::SingularModel => Self::SingularModel,
            OptimizerTermination::NonFiniteEvaluation => Self::NonFiniteEvaluation,
            OptimizerTermination::IterationLimit => Self::MaximumIterations,
        }
    }
}

/// Residual diagnostics recomputed from the final returned model.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ResidualDiagnostics {
    objective: f64,
    rmse: f64,
    max_absolute: f64,
    positive_weight_quotes: usize,
    distinct_strikes: usize,
    omitted_quotes: usize,
    total_weight: f64,
}

impl ResidualDiagnostics {
    pub(crate) const fn new(
        objective: f64,
        rmse: f64,
        max_absolute: f64,
        positive_weight_quotes: usize,
        distinct_strikes: usize,
        omitted_quotes: usize,
        total_weight: f64,
    ) -> Self {
        Self {
            objective,
            rmse,
            max_absolute,
            positive_weight_quotes,
            distinct_strikes,
            omitted_quotes,
            total_weight,
        }
    }

    /// Final weighted sum of squared residuals.
    #[must_use]
    pub const fn objective(self) -> f64 {
        self.objective
    }
    /// Final weighted root-mean-square residual.
    #[must_use]
    pub const fn rmse(self) -> f64 {
        self.rmse
    }
    /// Largest absolute residual among positive-weight observations.
    #[must_use]
    pub const fn max_absolute(self) -> f64 {
        self.max_absolute
    }
    /// Number of positive-weight observations used.
    #[must_use]
    pub const fn positive_weight_quotes(self) -> usize {
        self.positive_weight_quotes
    }
    /// Number of sufficiently distinct strikes used.
    #[must_use]
    pub const fn distinct_strikes(self) -> usize {
        self.distinct_strikes
    }
    /// Number of zero-weight observations omitted from the objective.
    #[must_use]
    pub const fn omitted_quotes(self) -> usize {
        self.omitted_quotes
    }
    /// Sum of positive fitting weights.
    #[must_use]
    pub const fn total_weight(self) -> f64 {
        self.total_weight
    }
}

/// Complete termination and postcondition evidence for a Raw slice fit.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CalibrationReport {
    algorithm: &'static str,
    starts: usize,
    selected_start: Option<usize>,
    tolerance: f64,
    condition_estimate: Option<f64>,
    parameterization: ParameterizationEvidence,
    margins: RawParameterMargins,
    iterations: usize,
    evaluations: usize,
    termination: TerminationReason,
    residuals: ResidualDiagnostics,
    arbitrage: ArbitrageAssessment,
}

impl CalibrationReport {
    #[allow(clippy::too_many_arguments, clippy::large_types_passed_by_value)]
    pub(crate) const fn new(
        iterations: usize,
        evaluations: usize,
        termination: TerminationReason,
        residuals: ResidualDiagnostics,
        arbitrage: ArbitrageAssessment,
        algorithm: &'static str,
        starts: usize,
        selected_start: Option<usize>,
        tolerance: f64,
        condition_estimate: Option<f64>,
        parameterization: ParameterizationEvidence,
        margins: RawParameterMargins,
    ) -> Self {
        Self {
            algorithm,
            starts,
            selected_start,
            tolerance,
            condition_estimate,
            parameterization,
            margins,
            iterations,
            evaluations,
            termination,
            residuals,
            arbitrage,
        }
    }

    /// Algorithm or composed pipeline stage that produced the model.
    #[must_use]
    pub const fn algorithm(self) -> &'static str {
        self.algorithm
    }
    /// Number of deterministic starts attempted.
    #[must_use]
    pub const fn starts(self) -> usize {
        self.starts
    }
    /// Zero-based deterministic start that produced the returned model.
    #[must_use]
    pub const fn selected_start(self) -> Option<usize> {
        self.selected_start
    }
    /// Configured numerical termination tolerance.
    #[must_use]
    pub const fn tolerance(self) -> f64 {
        self.tolerance
    }
    /// Estimated condition number of the selected normalized design, when available.
    #[must_use]
    pub const fn condition_estimate(self) -> Option<f64> {
        self.condition_estimate
    }
    /// Parameter transforms and repair policy.
    #[must_use]
    pub const fn parameterization(self) -> ParameterizationEvidence {
        self.parameterization
    }
    /// Recomputed Raw-domain and feasibility margins.
    #[must_use]
    pub const fn margins(self) -> RawParameterMargins {
        self.margins
    }

    /// Total optimizer iterations across all starts/stages.
    #[must_use]
    pub const fn iterations(self) -> usize {
        self.iterations
    }
    /// Total objective or residual evaluations.
    #[must_use]
    pub const fn evaluations(self) -> usize {
        self.evaluations
    }
    /// Optimizer termination reason.
    #[must_use]
    pub const fn termination(self) -> TerminationReason {
        self.termination
    }
    /// Residuals recomputed from the final model.
    #[must_use]
    pub const fn residuals(self) -> ResidualDiagnostics {
        self.residuals
    }
    /// Evidence-bearing Raw SVI arbitrage assessment.
    #[must_use]
    pub const fn arbitrage_assessment(self) -> ArbitrageAssessment {
        self.arbitrage
    }
}

/// Complete termination and feasibility evidence for an SSVI surface fit.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SurfaceCalibrationReport {
    algorithm: &'static str,
    starts: usize,
    selected_start: Option<usize>,
    tolerance: f64,
    parameterization: ParameterizationEvidence,
    margins: SurfaceParameterMargins,
    iterations: usize,
    evaluations: usize,
    termination: TerminationReason,
    residuals: ResidualDiagnostics,
    butterfly: ArbitrageAssessment,
    calendar: ArbitrageAssessment,
}

impl SurfaceCalibrationReport {
    #[allow(clippy::too_many_arguments, clippy::large_types_passed_by_value)]
    pub(crate) const fn new(
        iterations: usize,
        evaluations: usize,
        termination: TerminationReason,
        residuals: ResidualDiagnostics,
        butterfly: ArbitrageAssessment,
        calendar: ArbitrageAssessment,
        algorithm: &'static str,
        starts: usize,
        selected_start: Option<usize>,
        tolerance: f64,
        parameterization: ParameterizationEvidence,
        margins: SurfaceParameterMargins,
    ) -> Self {
        Self {
            algorithm,
            starts,
            selected_start,
            tolerance,
            parameterization,
            margins,
            iterations,
            evaluations,
            termination,
            residuals,
            butterfly,
            calendar,
        }
    }

    /// Algorithm used for the joint surface fit.
    #[must_use]
    pub const fn algorithm(self) -> &'static str {
        self.algorithm
    }
    /// Number of deterministic starts attempted.
    #[must_use]
    pub const fn starts(self) -> usize {
        self.starts
    }
    /// Zero-based deterministic start that produced the returned model.
    #[must_use]
    pub const fn selected_start(self) -> Option<usize> {
        self.selected_start
    }
    /// Configured numerical termination tolerance.
    #[must_use]
    pub const fn tolerance(self) -> f64 {
        self.tolerance
    }
    /// Parameter transforms and repair policy.
    #[must_use]
    pub const fn parameterization(self) -> ParameterizationEvidence {
        self.parameterization
    }
    /// Recomputed SSVI-domain and feasibility margins.
    #[must_use]
    pub const fn margins(self) -> SurfaceParameterMargins {
        self.margins
    }

    /// Total optimizer iterations across all starts.
    #[must_use]
    pub const fn iterations(self) -> usize {
        self.iterations
    }
    /// Total objective evaluations across all starts.
    #[must_use]
    pub const fn evaluations(self) -> usize {
        self.evaluations
    }
    /// Optimizer termination reason.
    #[must_use]
    pub const fn termination(self) -> TerminationReason {
        self.termination
    }
    /// Residuals recomputed from the final surface.
    #[must_use]
    pub const fn residuals(self) -> ResidualDiagnostics {
        self.residuals
    }
    /// Whole-family analytic sufficient butterfly assessment.
    #[must_use]
    pub const fn butterfly_assessment(self) -> ArbitrageAssessment {
        self.butterfly
    }
    /// Analytic calendar assessment on the validated term structure.
    #[must_use]
    pub const fn calendar_assessment(self) -> ArbitrageAssessment {
        self.calendar
    }
}