use crate::error::{SanosError, SanosResult};
use crate::market::CompletionConfig;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SmoothingOrder {
#[default]
D1,
D2,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "mode"))]
#[derive(Debug, Clone, PartialEq, Default)]
pub enum RegularizationMode {
#[default]
None,
Ridge,
Smoothing {
#[cfg_attr(feature = "serde", serde(default))]
order: SmoothingOrder,
},
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Default)]
pub struct LambdaScaling {
#[cfg_attr(feature = "serde", serde(default))]
pub knots: Vec<(f64, f64)>,
}
impl LambdaScaling {
pub fn scale_at(&self, maturity: f64) -> f64 {
if self.knots.is_empty() {
return 1.0;
}
if self.knots.len() == 1 || maturity <= self.knots[0].0 {
return self.knots[0].1;
}
let last = self.knots.len() - 1;
if maturity >= self.knots[last].0 {
return self.knots[last].1;
}
for w in self.knots.windows(2) {
let (ta, sa) = w[0];
let (tb, sb) = w[1];
if ta <= maturity && maturity <= tb {
let frac = (maturity - ta) / (tb - ta);
return (1.0 - frac) * sa + frac * sb;
}
}
1.0
}
pub fn is_uniform(&self) -> bool {
self.knots.is_empty()
}
pub fn validate(&self) -> SanosResult<()> {
for (i, &(t, s)) in self.knots.iter().enumerate() {
if !t.is_finite() || t <= 0.0 {
return Err(SanosError::InvalidBound {
field: "regularization.lambda_scaling.maturity",
value: t,
min: f64::MIN_POSITIVE,
max: f64::INFINITY,
});
}
if !s.is_finite() || s <= 0.0 {
return Err(SanosError::InvalidBound {
field: "regularization.lambda_scaling.scale",
value: s,
min: f64::MIN_POSITIVE,
max: f64::INFINITY,
});
}
if i > 0 && t <= self.knots[i - 1].0 {
return Err(SanosError::InvalidOrdering {
msg: "lambda_scaling knots must be strictly increasing in maturity",
});
}
}
Ok(())
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct RegularizationConfig {
#[cfg_attr(feature = "serde", serde(default))]
pub mode: RegularizationMode,
pub lambda: f64,
#[cfg_attr(feature = "serde", serde(default))]
pub lambda_scaling: LambdaScaling,
}
impl Default for RegularizationConfig {
fn default() -> Self {
Self {
mode: RegularizationMode::None,
lambda: 1e-4,
lambda_scaling: LambdaScaling::default(),
}
}
}
impl RegularizationConfig {
pub fn is_active(&self) -> bool {
self.mode != RegularizationMode::None
}
pub fn validate(&self) -> SanosResult<()> {
if !self.is_active() {
return Ok(());
}
if !self.lambda.is_finite() {
return Err(SanosError::NonFinite {
field: "regularization.lambda",
value: self.lambda,
});
}
if self.lambda <= 0.0 {
return Err(SanosError::InvalidBound {
field: "regularization.lambda",
value: self.lambda,
min: f64::MIN_POSITIVE,
max: f64::INFINITY,
});
}
self.lambda_scaling.validate()?;
Ok(())
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OmegaConfig {
Zero,
#[default]
One,
Both,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Default)]
pub struct KernelConfig {
pub omega: OmegaConfig,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct ConstraintConfig {
pub enforce_simplex: bool, pub enforce_nonnegativity: bool, pub include_time_constraints: bool, }
impl Default for ConstraintConfig {
fn default() -> Self {
Self {
enforce_simplex: true,
enforce_nonnegativity: true,
include_time_constraints: true,
}
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum InitPriceProxyConfig {
#[default]
Mid,
Bid,
Ask,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WarmStartMode {
#[default]
None,
BackboneSynthetic,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct InitializationConfig {
#[cfg_attr(feature = "serde", serde(default))]
pub mode: WarmStartMode,
pub price_proxy: InitPriceProxyConfig,
pub feasibility_tol: f64,
#[cfg_attr(feature = "serde", serde(default))]
pub market_completion: CompletionConfig,
}
impl Default for InitializationConfig {
fn default() -> Self {
Self {
mode: WarmStartMode::default(),
price_proxy: InitPriceProxyConfig::Mid,
feasibility_tol: 1e-8,
market_completion: CompletionConfig::default(),
}
}
}
impl InitializationConfig {
pub fn uses_warm_start(&self) -> bool {
self.mode != WarmStartMode::None
}
pub fn validate(&self) -> SanosResult<()> {
if !self.feasibility_tol.is_finite() {
return Err(SanosError::NonFinite {
field: "initialization.feasibility_tol",
value: self.feasibility_tol,
});
}
if self.feasibility_tol < 0.0 {
return Err(SanosError::InvalidBound {
field: "initialization.feasibility_tol",
value: self.feasibility_tol,
min: 0.0,
max: f64::INFINITY,
});
}
if self.mode == WarmStartMode::BackboneSynthetic {
self.market_completion.validate()?;
}
Ok(())
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum QuoteWeightMode {
Identity,
#[default]
BidAskSpread,
Vega,
BidAskVega,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct QuoteWeightingConfig {
#[cfg_attr(feature = "serde", serde(default))]
pub mode: QuoteWeightMode,
pub spread_floor: f64,
pub vega_floor: f64,
}
impl Default for QuoteWeightingConfig {
fn default() -> Self {
Self {
mode: QuoteWeightMode::default(),
spread_floor: 1e-12,
vega_floor: 1e-12,
}
}
}
impl QuoteWeightingConfig {
pub fn validate(&self) -> SanosResult<()> {
if !self.spread_floor.is_finite() {
return Err(SanosError::NonFinite {
field: "weighting.spread_floor",
value: self.spread_floor,
});
}
if self.spread_floor <= 0.0 {
return Err(SanosError::InvalidBound {
field: "weighting.spread_floor",
value: self.spread_floor,
min: f64::MIN_POSITIVE,
max: f64::INFINITY,
});
}
if !self.vega_floor.is_finite() {
return Err(SanosError::NonFinite {
field: "weighting.vega_floor",
value: self.vega_floor,
});
}
if self.vega_floor <= 0.0 {
return Err(SanosError::InvalidBound {
field: "weighting.vega_floor",
value: self.vega_floor,
min: f64::MIN_POSITIVE,
max: f64::INFINITY,
});
}
Ok(())
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Default)]
pub struct FitConfig {
pub kernel: KernelConfig,
pub constraints: ConstraintConfig,
#[cfg_attr(feature = "serde", serde(default))]
pub initialization: InitializationConfig,
#[cfg_attr(feature = "serde", serde(default))]
pub weighting: QuoteWeightingConfig,
#[cfg_attr(feature = "serde", serde(default))]
pub regularization: RegularizationConfig,
}
impl FitConfig {
pub fn uses_warm_start(&self) -> bool {
self.initialization.uses_warm_start()
}
pub fn validate(&self) -> SanosResult<()> {
self.initialization.validate()?;
self.weighting.validate()?;
self.regularization.validate()?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fit_config_default_validates() {
let cfg = FitConfig::default();
assert!(cfg.validate().is_ok());
}
#[test]
fn weighting_defaults_match_legacy_behavior() {
let cfg = FitConfig::default();
assert_eq!(cfg.weighting.mode, QuoteWeightMode::BidAskSpread);
assert_eq!(cfg.weighting.spread_floor, 1e-12);
assert_eq!(cfg.weighting.vega_floor, 1e-12);
}
#[test]
fn invalid_weighting_floor_is_rejected() {
let mut cfg = FitConfig::default();
cfg.weighting.vega_floor = 0.0;
assert!(matches!(
cfg.validate(),
Err(SanosError::InvalidBound {
field: "weighting.vega_floor",
..
})
));
}
}