use anyhow::{anyhow, Result};
use std::collections::HashMap;
use super::interp::build_linear_iv;
use super::types::*;
const TEMPORAL_EPSILON: f64 = 1e-8;
fn group_by_tte(data: &[MarketDataRow]) -> Vec<(f64, Vec<MarketDataRow>)> {
let mut tte_to_data: HashMap<String, Vec<MarketDataRow>> = HashMap::new();
for row in data {
let tte_key = format!("{:.8}", row.years_to_exp); tte_to_data.entry(tte_key).or_default().push(row.clone());
}
let mut groups: Vec<(f64, Vec<MarketDataRow>)> = tte_to_data
.into_iter()
.map(|(tte_str, data)| {
let tte: f64 = tte_str.parse().unwrap();
(tte, data)
})
.collect();
groups.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
groups
}
fn temporal_interp(
tte_metrics: &[(f64, f64)], target_tte: f64,
allow_short_extrap: bool,
allow_long_extrap: bool,
) -> Option<f64> {
if tte_metrics.is_empty() {
return None;
}
if tte_metrics.len() == 1 {
return Some(tte_metrics[0].1);
}
let min_tte = tte_metrics[0].0;
let max_tte = tte_metrics[tte_metrics.len() - 1].0;
if (target_tte - min_tte) < -TEMPORAL_EPSILON && !allow_short_extrap {
return None;
}
if (target_tte - max_tte) > TEMPORAL_EPSILON && !allow_long_extrap {
return None;
}
if target_tte <= min_tte {
if (target_tte - min_tte).abs() < 1e-10 {
return Some(tte_metrics[0].1);
}
if tte_metrics.len() < 2 {
return Some(tte_metrics[0].1);
}
let (tte1, val1) = tte_metrics[0];
let (tte2, val2) = tte_metrics[1];
let slope = (val2 - val1) / (tte2 - tte1);
return Some(val1 + slope * (target_tte - tte1));
}
if target_tte >= max_tte {
if (target_tte - max_tte).abs() < 1e-10 {
return Some(tte_metrics[tte_metrics.len() - 1].1);
}
let n = tte_metrics.len();
let (tte1, val1) = tte_metrics[n - 2];
let (tte2, val2) = tte_metrics[n - 1];
let slope = (val2 - val1) / (tte2 - tte1);
return Some(val2 + slope * (target_tte - tte2));
}
for i in 0..tte_metrics.len() - 1 {
let (tte1, val1) = tte_metrics[i];
let (tte2, val2) = tte_metrics[i + 1];
if target_tte >= tte1 && target_tte <= tte2 {
let t = (target_tte - tte1) / (tte2 - tte1);
return Some(val1 + t * (val2 - val1));
}
}
None
}
fn interpolate_metric_value(
tte_metrics: &[(f64, LinearIvOutput)],
target_tte: f64,
method: TemporalInterpMethod,
allow_short_extrap: bool,
allow_long_extrap: bool,
metric_extractor: impl Fn(&LinearIvOutput) -> f64,
) -> Option<f64> {
let metric_pairs: Vec<(f64, f64)> = tte_metrics
.iter()
.map(|(tte, output)| (*tte, metric_extractor(output)))
.collect();
match method {
TemporalInterpMethod::LinearTte => temporal_interp(
&metric_pairs,
target_tte,
allow_short_extrap,
allow_long_extrap,
),
TemporalInterpMethod::LinearVariance => {
let variance_pairs: Vec<(f64, f64)> = metric_pairs
.iter()
.map(|(tte, iv)| (*tte, iv * iv * tte))
.collect();
let interpolated_variance = temporal_interp(
&variance_pairs,
target_tte,
allow_short_extrap,
allow_long_extrap,
)?;
if interpolated_variance <= 0.0 {
return None;
}
Some((interpolated_variance / target_tte).sqrt())
}
TemporalInterpMethod::SquareRootTime => {
if target_tte <= 0.0 {
return None;
}
let scaled_pairs: Vec<(f64, f64)> = metric_pairs
.iter()
.filter_map(|(tte, iv)| {
if *tte > 0.0 {
Some((*tte, iv / tte.sqrt()))
} else {
None }
})
.collect();
if scaled_pairs.is_empty() {
return None;
}
let scaled_value = temporal_interp(
&scaled_pairs,
target_tte,
allow_short_extrap,
allow_long_extrap,
)?;
Some(scaled_value * target_tte.sqrt())
}
}
}
pub fn build_fixed_time_metrics(
data: &[MarketDataRow],
forward: f64,
temp_config: &TemporalConfig,
strike_config: &LinearIvConfig,
) -> Result<Vec<FixedTimeMetrics>> {
if data.is_empty() {
return Err(anyhow!("No market data provided"));
}
let tte_groups = group_by_tte(data);
if tte_groups.len() < temp_config.min_maturities {
return Err(anyhow!(
"Insufficient maturities: {} < {}",
tte_groups.len(),
temp_config.min_maturities
));
}
let mut maturity_outputs = Vec::new();
for (tte, group_data) in &tte_groups {
match build_linear_iv(group_data, forward, *tte, strike_config) {
Ok(output) => {
maturity_outputs.push((*tte, output));
}
Err(e) => {
return Err(anyhow!("Failed to build linear IV for TTE {}: {}", tte, e));
}
}
}
if maturity_outputs.is_empty() {
return Err(anyhow!("No valid maturity outputs produced"));
}
maturity_outputs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
let min_tte = maturity_outputs[0].0;
let max_tte = maturity_outputs[maturity_outputs.len() - 1].0;
let mut results = Vec::new();
for &fixed_days in &temp_config.fixed_days {
let target_tte = fixed_days as f64 / 365.0;
if (target_tte - min_tte) < -TEMPORAL_EPSILON && !temp_config.allow_short_extrapolate {
continue;
}
if (target_tte - max_tte) > TEMPORAL_EPSILON && !temp_config.allow_long_extrapolate {
continue;
}
let atm_iv = interpolate_metric_value(
&maturity_outputs,
target_tte,
temp_config.interp_method,
temp_config.allow_short_extrapolate,
temp_config.allow_long_extrapolate,
|output| output.atm_iv,
);
let atm_iv = match atm_iv {
Some(iv) if iv > 0.0 => iv,
_ => continue, };
let mut all_delta_levels = std::collections::HashSet::new();
for (_, output) in &maturity_outputs {
for delta_metric in &output.delta_metrics {
let delta_key = format!("{:.6}", delta_metric.delta_level);
all_delta_levels.insert(delta_key);
}
}
let mut delta_metrics = Vec::new();
for delta_key in all_delta_levels {
let delta_level: f64 = delta_key.parse().unwrap();
let rr_values: Vec<(f64, f64)> = maturity_outputs
.iter()
.filter_map(|(tte, output)| {
output
.delta_metrics
.iter()
.find(|dm| (dm.delta_level - delta_level).abs() < 1e-6)
.map(|dm| (*tte, dm.risk_reversal))
})
.collect();
let bf_values: Vec<(f64, f64)> = maturity_outputs
.iter()
.filter_map(|(tte, output)| {
output
.delta_metrics
.iter()
.find(|dm| (dm.delta_level - delta_level).abs() < 1e-6)
.map(|dm| (*tte, dm.butterfly))
})
.collect();
if rr_values.len() >= 2 && bf_values.len() >= 2 {
let rr = temporal_interp(
&rr_values,
target_tte,
temp_config.allow_short_extrapolate,
temp_config.allow_long_extrapolate,
);
let bf = temporal_interp(
&bf_values,
target_tte,
temp_config.allow_short_extrapolate,
temp_config.allow_long_extrapolate,
);
if let (Some(rr_val), Some(bf_val)) = (rr, bf) {
delta_metrics.push(DeltaMetrics {
delta_level,
risk_reversal: rr_val,
butterfly: bf_val,
});
}
}
}
delta_metrics.sort_by(|a, b| a.delta_level.partial_cmp(&b.delta_level).unwrap());
results.push(FixedTimeMetrics {
tte_days: fixed_days,
tte_years: target_tte,
atm_iv,
delta_metrics,
});
}
results.sort_by_key(|m| m.tte_days);
Ok(results)
}