use crate::error::{CalibrationError, ParamError};
use crate::market::quote::Quote;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConstraintMode {
Constrained,
BestEffort,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InitializationPolicy {
DeterministicMultiStart,
DeterministicSingleStart,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SliceCalibrationConfig {
outer_iterations: usize,
polish_iterations: usize,
tolerance: f64,
arbitrage_tolerance: f64,
distinct_tolerance: f64,
constraint_mode: ConstraintMode,
initialization: InitializationPolicy,
}
impl SliceCalibrationConfig {
#[allow(clippy::too_many_arguments)]
pub fn new(
outer_iterations: usize,
polish_iterations: usize,
tolerance: f64,
arbitrage_tolerance: f64,
distinct_tolerance: f64,
constraint_mode: ConstraintMode,
initialization: InitializationPolicy,
) -> Result<Self, CalibrationError> {
if outer_iterations == 0 {
return Err(CalibrationError::InvalidConfig {
field: "outer_iterations",
value: 0.0,
});
}
if polish_iterations == 0 {
return Err(CalibrationError::InvalidConfig {
field: "polish_iterations",
value: 0.0,
});
}
for (field, value) in [
("tolerance", tolerance),
("arbitrage_tolerance", arbitrage_tolerance),
("distinct_tolerance", distinct_tolerance),
] {
if !value.is_finite() || value <= 0.0 {
return Err(CalibrationError::InvalidConfig { field, value });
}
}
Ok(Self {
outer_iterations,
polish_iterations,
tolerance,
arbitrage_tolerance,
distinct_tolerance,
constraint_mode,
initialization,
})
}
#[must_use]
pub const fn outer_iterations(self) -> usize {
self.outer_iterations
}
#[must_use]
pub const fn polish_iterations(self) -> usize {
self.polish_iterations
}
#[must_use]
pub const fn tolerance(self) -> f64 {
self.tolerance
}
#[must_use]
pub const fn arbitrage_tolerance(self) -> f64 {
self.arbitrage_tolerance
}
#[must_use]
pub const fn distinct_tolerance(self) -> f64 {
self.distinct_tolerance
}
#[must_use]
pub const fn constraint_mode(self) -> ConstraintMode {
self.constraint_mode
}
#[must_use]
pub const fn initialization(self) -> InitializationPolicy {
self.initialization
}
#[must_use]
pub const fn with_constraint_mode(mut self, mode: ConstraintMode) -> Self {
self.constraint_mode = mode;
self
}
}
impl Default for SliceCalibrationConfig {
fn default() -> Self {
Self {
outer_iterations: 2_000,
polish_iterations: 500,
tolerance: 1e-12,
arbitrage_tolerance: 1e-10,
distinct_tolerance: 1e-12,
constraint_mode: ConstraintMode::Constrained,
initialization: InitializationPolicy::DeterministicMultiStart,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SurfaceCalibrationConfig {
outer_iterations: usize,
tolerance: f64,
constraint_mode: ConstraintMode,
initialization: InitializationPolicy,
}
impl SurfaceCalibrationConfig {
pub fn new(
outer_iterations: usize,
tolerance: f64,
constraint_mode: ConstraintMode,
initialization: InitializationPolicy,
) -> Result<Self, CalibrationError> {
if outer_iterations == 0 {
return Err(CalibrationError::InvalidConfig {
field: "outer_iterations",
value: 0.0,
});
}
if !tolerance.is_finite() || tolerance <= 0.0 {
return Err(CalibrationError::InvalidConfig {
field: "tolerance",
value: tolerance,
});
}
Ok(Self {
outer_iterations,
tolerance,
constraint_mode,
initialization,
})
}
#[must_use]
pub const fn outer_iterations(self) -> usize {
self.outer_iterations
}
#[must_use]
pub const fn tolerance(self) -> f64 {
self.tolerance
}
#[must_use]
pub const fn constraint_mode(self) -> ConstraintMode {
self.constraint_mode
}
#[must_use]
pub const fn initialization(self) -> InitializationPolicy {
self.initialization
}
#[must_use]
pub const fn with_constraint_mode(mut self, mode: ConstraintMode) -> Self {
self.constraint_mode = mode;
self
}
}
impl Default for SurfaceCalibrationConfig {
fn default() -> Self {
Self {
outer_iterations: 3_000,
tolerance: 1e-12,
constraint_mode: ConstraintMode::Constrained,
initialization: InitializationPolicy::DeterministicMultiStart,
}
}
}
pub(crate) fn validate_effective_quotes(
quotes: &[Quote],
minimum: usize,
distinct_tolerance: f64,
) -> Result<(usize, usize), CalibrationError> {
let total_weight: f64 = quotes.iter().map(|quote| quote.weight()).sum();
if !total_weight.is_finite() {
return Err(CalibrationError::Param(ParamError::NonFinite {
name: "total quote weight",
}));
}
if quotes.is_empty() {
return Err(CalibrationError::EmptyQuotes);
}
let mut strikes: Vec<f64> = quotes
.iter()
.filter(|quote| quote.weight() > 0.0)
.map(|quote| quote.log_moneyness())
.collect();
if strikes.is_empty() {
return Err(CalibrationError::AllWeightsZero);
}
strikes.sort_by(f64::total_cmp);
let usable = strikes.len();
let mut distinct = 1;
let mut previous = strikes[0];
for strike in strikes.into_iter().skip(1) {
let scale = 1.0 + previous.abs().max(strike.abs());
if strike - previous > distinct_tolerance * scale {
distinct += 1;
previous = strike;
}
}
if usable < minimum || distinct < minimum {
return Err(CalibrationError::InsufficientEffectiveQuotes {
usable,
distinct,
need: minimum,
});
}
Ok((usable, distinct))
}
#[cfg(test)]
#[allow(clippy::expect_used)] mod tests {
use super::*;
#[test]
fn rejects_invalid_controls() {
assert!(matches!(
SliceCalibrationConfig::new(
0,
10,
1e-10,
1e-10,
1e-12,
ConstraintMode::Constrained,
InitializationPolicy::DeterministicMultiStart,
),
Err(CalibrationError::InvalidConfig {
field: "outer_iterations",
..
})
));
assert!(
SurfaceCalibrationConfig::new(
10,
f64::NAN,
ConstraintMode::Constrained,
InitializationPolicy::DeterministicMultiStart,
)
.is_err()
);
}
#[test]
fn effective_count_requires_positive_weight_and_distinct_strikes() {
let repeated = [
Quote::new(0.0, 0.04, 1.0).expect("valid test or documentation fixture"),
Quote::new(0.0, 0.04, 1.0).expect("valid test or documentation fixture"),
Quote::new(0.1, 0.05, 0.0).expect("valid test or documentation fixture"),
Quote::new(0.2, 0.06, 0.0).expect("valid test or documentation fixture"),
Quote::new(0.3, 0.07, 0.0).expect("valid test or documentation fixture"),
];
assert!(matches!(
validate_effective_quotes(&repeated, 5, 1e-12),
Err(CalibrationError::InsufficientEffectiveQuotes {
usable: 2,
distinct: 1,
need: 5,
})
));
}
}