use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
pub struct DataFilter {
pub max_log_moneyness: Option<f64>,
pub min_vol: Option<f64>,
pub vol_cliff_filter: Option<bool>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum WeightingScheme {
#[default]
ModelDefault,
Vega,
Uniform,
}
pub fn apply_filter(
market_vols: &[(f64, f64)],
forward: f64,
filter: &DataFilter,
) -> Vec<(f64, f64)> {
market_vols
.iter()
.copied()
.filter(|&(strike, vol)| {
if !vol.is_finite() {
return false;
}
let lm = (strike / forward).ln();
if !lm.is_finite() {
return false;
}
if filter
.max_log_moneyness
.is_some_and(|max_k| lm.abs() > max_k)
{
return false;
}
if filter.min_vol.is_some_and(|min_v| vol < min_v) {
return false;
}
true
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_filter_passes_everything() {
let data = vec![(90.0, 0.25), (100.0, 0.20), (110.0, 0.22)];
let result = apply_filter(&data, 100.0, &DataFilter::default());
assert_eq!(result, data);
}
#[test]
fn max_log_moneyness_excludes_far_wings() {
let data = vec![
(50.0, 0.40), (90.0, 0.25), (100.0, 0.20), (110.0, 0.22), (200.0, 0.35), ];
let filter = DataFilter {
max_log_moneyness: Some(0.5),
..Default::default()
};
let result = apply_filter(&data, 100.0, &filter);
assert_eq!(result, vec![(90.0, 0.25), (100.0, 0.20), (110.0, 0.22)]);
}
#[test]
fn min_vol_excludes_cabinet_quotes() {
let data = vec![
(90.0, 0.25),
(95.0, 0.005), (100.0, 0.20),
(150.0, 0.008), ];
let filter = DataFilter {
min_vol: Some(0.01),
..Default::default()
};
let result = apply_filter(&data, 100.0, &filter);
assert_eq!(result, vec![(90.0, 0.25), (100.0, 0.20)]);
}
#[test]
fn combined_filter() {
let data = vec![
(50.0, 0.40), (95.0, 0.005), (100.0, 0.20), (105.0, 0.22), (200.0, 0.008), ];
let filter = DataFilter {
max_log_moneyness: Some(0.2),
min_vol: Some(0.01),
..Default::default()
};
let result = apply_filter(&data, 100.0, &filter);
assert_eq!(result, vec![(100.0, 0.20), (105.0, 0.22)]);
}
#[test]
fn empty_input() {
let result = apply_filter(&[], 100.0, &DataFilter::default());
assert!(result.is_empty());
}
#[test]
fn all_filtered_out() {
let data = vec![(50.0, 0.40), (200.0, 0.35)];
let filter = DataFilter {
max_log_moneyness: Some(0.1),
..Default::default()
};
let result = apply_filter(&data, 100.0, &filter);
assert!(result.is_empty());
}
#[test]
fn nan_strike_excluded() {
let data = vec![(f64::NAN, 0.20), (100.0, 0.20)];
let result = apply_filter(&data, 100.0, &DataFilter::default());
assert_eq!(result, vec![(100.0, 0.20)]);
}
#[test]
fn nan_vol_excluded_unconditionally() {
let data = vec![(100.0, f64::NAN), (100.0, 0.20)];
let result = apply_filter(&data, 100.0, &DataFilter::default());
assert_eq!(result, vec![(100.0, 0.20)]);
}
#[test]
fn inf_vol_excluded_unconditionally() {
let data = vec![(100.0, f64::INFINITY), (100.0, 0.20)];
let result = apply_filter(&data, 100.0, &DataFilter::default());
assert_eq!(result, vec![(100.0, 0.20)]);
}
#[test]
fn nan_vol_excluded_with_min_vol() {
let data = vec![(100.0, f64::NAN), (100.0, 0.20)];
let filter = DataFilter {
min_vol: Some(0.01),
..Default::default()
};
let result = apply_filter(&data, 100.0, &filter);
assert_eq!(result, vec![(100.0, 0.20)]);
}
#[test]
fn zero_forward_excludes_all() {
let data = vec![(100.0, 0.20), (110.0, 0.22)];
let filter = DataFilter {
max_log_moneyness: Some(0.5),
..Default::default()
};
let result = apply_filter(&data, 0.0, &filter);
assert!(result.is_empty());
}
#[test]
fn vol_cliff_filter_not_applied() {
let data = vec![(90.0, 0.25), (100.0, 0.02), (110.0, 0.22)];
let filter = DataFilter {
vol_cliff_filter: Some(true),
..Default::default()
};
let result = apply_filter(&data, 100.0, &filter);
assert_eq!(
result.len(),
3,
"apply_filter must not act on vol_cliff_filter"
);
}
#[test]
fn serde_round_trip_data_filter() {
let filter = DataFilter {
max_log_moneyness: Some(0.5),
min_vol: Some(0.01),
vol_cliff_filter: Some(false),
};
let json = serde_json::to_string(&filter).unwrap();
let roundtrip: DataFilter = serde_json::from_str(&json).unwrap();
assert_eq!(filter, roundtrip);
}
#[test]
fn serde_round_trip_weighting_scheme() {
for scheme in [
WeightingScheme::ModelDefault,
WeightingScheme::Vega,
WeightingScheme::Uniform,
] {
let json = serde_json::to_string(&scheme).unwrap();
let roundtrip: WeightingScheme = serde_json::from_str(&json).unwrap();
assert_eq!(scheme, roundtrip);
}
}
#[test]
fn defaults() {
let filter = DataFilter::default();
assert_eq!(filter.max_log_moneyness, None);
assert_eq!(filter.min_vol, None);
assert_eq!(filter.vol_cliff_filter, None);
assert_eq!(WeightingScheme::default(), WeightingScheme::ModelDefault);
}
#[test]
fn preserves_input_order() {
let data = vec![(110.0, 0.22), (90.0, 0.25), (100.0, 0.20)];
let result = apply_filter(&data, 100.0, &DataFilter::default());
assert_eq!(result, data);
}
}