surface_lib/models/linear_iv/temporal.rs
1//! Temporal interpolation for fixed time grid construction
2//!
3//! This module provides functionality to interpolate volatility metrics across multiple
4//! maturities to construct standardized time grids. It extends the single-maturity
5//! linear IV interpolation to handle multi-maturity option chains.
6//!
7//! # Overview
8//!
9//! The temporal interpolation module enables traders and quants to:
10//! - Build consistent volatility surfaces across standardized expiry ladders
11//! - Interpolate between observed maturities using different methodologies
12//! - Extrapolate to shorter/longer tenors when appropriate
13//! - Maintain mathematical consistency with strike-space interpolation
14//!
15//! # Interpolation Methods
16//!
17//! Three temporal interpolation methods are supported:
18//!
19//! ## LinearTte
20//! Direct linear interpolation on time-to-expiration vs metric value pairs.
21//! Simple and intuitive, suitable for most applications.
22//!
23//! ## LinearVariance
24//! Interpolates total variance (ω = σ²T) and converts back to implied volatility.
25//! Mathematically consistent with no-arbitrage conditions and variance swaps.
26//! Recommended for professional trading systems.
27//!
28//! ## SquareRootTime
29//! Scales volatility by √(T_target/T_base). Common approximation for
30//! short-term extrapolation when volatility is mean-reverting.
31//!
32//! # Usage Pattern
33//!
34//! 1. Collect multi-maturity option chain data
35//! 2. Configure `TemporalConfig` with desired fixed days and interpolation method
36//! 3. Call `build_fixed_time_metrics()` to generate standardized time grid
37//! 4. Use resulting metrics for pricing, Greeks, and risk management
38//!
39//! # Example
40//!
41//! ```rust,no_run
42//! use surface_lib::{MarketDataRow, LinearIvConfig, TemporalConfig, build_fixed_time_metrics};
43//!
44//! # let market_data: Vec<MarketDataRow> = vec![];
45//! # let forward = 100.0;
46//! let temporal_config = TemporalConfig {
47//! fixed_days: vec![1, 7, 14, 30],
48//! ..Default::default()
49//! };
50//! let strike_config = LinearIvConfig::default();
51//!
52//! let metrics = build_fixed_time_metrics(
53//! &market_data, forward, &temporal_config, &strike_config
54//! )?;
55//! # Ok::<(), Box<dyn std::error::Error>>(())
56//! ```
57
58use anyhow::{anyhow, Result};
59use std::collections::HashMap;
60
61use super::interp::build_linear_iv;
62use super::types::*;
63
64/// Floating point epsilon for temporal interpolation comparisons
65/// Generous tolerance to handle day/year conversions and accumulated rounding
66const TEMPORAL_EPSILON: f64 = 1e-8;
67
68/// Group market data by time-to-expiration, returning sorted groups
69/// Each group contains all market data for a single maturity
70fn group_by_tte(data: &[MarketDataRow]) -> Vec<(f64, Vec<MarketDataRow>)> {
71 let mut tte_to_data: HashMap<String, Vec<MarketDataRow>> = HashMap::new();
72
73 // Group by TTE with limited precision to handle floating point issues
74 for row in data {
75 let tte_key = format!("{:.8}", row.years_to_exp); // 8 decimal places precision
76 tte_to_data.entry(tte_key).or_default().push(row.clone());
77 }
78
79 // Convert to vector and sort by TTE
80 let mut groups: Vec<(f64, Vec<MarketDataRow>)> = tte_to_data
81 .into_iter()
82 .map(|(tte_str, data)| {
83 let tte: f64 = tte_str.parse().unwrap();
84 (tte, data)
85 })
86 .collect();
87
88 groups.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
89
90 groups
91}
92
93/// Linear interpolation helper for temporal interpolation
94///
95/// Performs piecewise linear interpolation on sorted (TTE, metric_value) pairs
96/// with configurable extrapolation behavior and floating point precision handling.
97///
98/// # Arguments
99///
100/// * `tte_metrics` - Sorted array of (time-to-expiration, metric_value) pairs
101/// * `target_tte` - Target time to interpolate at
102/// * `allow_short_extrap` - Allow extrapolation below minimum TTE
103/// * `allow_long_extrap` - Allow extrapolation above maximum TTE
104///
105/// # Returns
106///
107/// * `Some(value)` - Interpolated or extrapolated metric value
108/// * `None` - If extrapolation is required but disabled, or if data is insufficient
109///
110/// # Extrapolation Behavior
111///
112/// When extrapolation is enabled, linear extrapolation uses the slope from the
113/// nearest two points. Exact matches at boundaries return precise values to
114/// handle floating point precision issues.
115fn temporal_interp(
116 tte_metrics: &[(f64, f64)], // (tte, metric_value) pairs
117 target_tte: f64,
118 allow_short_extrap: bool,
119 allow_long_extrap: bool,
120) -> Option<f64> {
121 if tte_metrics.is_empty() {
122 return None;
123 }
124
125 if tte_metrics.len() == 1 {
126 return Some(tte_metrics[0].1);
127 }
128
129 let min_tte = tte_metrics[0].0;
130 let max_tte = tte_metrics[tte_metrics.len() - 1].0;
131
132 // Check extrapolation bounds with epsilon tolerance
133 if (target_tte - min_tte) < -TEMPORAL_EPSILON && !allow_short_extrap {
134 return None;
135 }
136 if (target_tte - max_tte) > TEMPORAL_EPSILON && !allow_long_extrap {
137 return None;
138 }
139
140 // Handle extrapolation cases
141 if target_tte <= min_tte {
142 // If exactly at min_tte, return the exact value
143 if (target_tte - min_tte).abs() < 1e-10 {
144 return Some(tte_metrics[0].1);
145 }
146
147 if tte_metrics.len() < 2 {
148 return Some(tte_metrics[0].1);
149 }
150 let (tte1, val1) = tte_metrics[0];
151 let (tte2, val2) = tte_metrics[1];
152 let slope = (val2 - val1) / (tte2 - tte1);
153 return Some(val1 + slope * (target_tte - tte1));
154 }
155
156 if target_tte >= max_tte {
157 // If exactly at max_tte, return the exact value
158 if (target_tte - max_tte).abs() < 1e-10 {
159 return Some(tte_metrics[tte_metrics.len() - 1].1);
160 }
161
162 let n = tte_metrics.len();
163 let (tte1, val1) = tte_metrics[n - 2];
164 let (tte2, val2) = tte_metrics[n - 1];
165 let slope = (val2 - val1) / (tte2 - tte1);
166 return Some(val2 + slope * (target_tte - tte2));
167 }
168
169 // Find interpolation interval
170 for i in 0..tte_metrics.len() - 1 {
171 let (tte1, val1) = tte_metrics[i];
172 let (tte2, val2) = tte_metrics[i + 1];
173
174 if target_tte >= tte1 && target_tte <= tte2 {
175 let t = (target_tte - tte1) / (tte2 - tte1);
176 return Some(val1 + t * (val2 - val1));
177 }
178 }
179
180 None
181}
182
183/// Interpolate a single metric value using the specified temporal method
184///
185/// This function extracts a specific metric from multiple maturity outputs and
186/// interpolates it temporally using one of three supported methods. It handles
187/// the conversion between interpolation methods transparently.
188///
189/// # Arguments
190///
191/// * `tte_metrics` - Array of (TTE, output) pairs from multiple maturities
192/// * `target_tte` - Target time-to-expiration for interpolation
193/// * `method` - Temporal interpolation method to use
194/// * `allow_short_extrap` - Enable extrapolation below minimum observed TTE
195/// * `allow_long_extrap` - Enable extrapolation above maximum observed TTE
196/// * `metric_extractor` - Closure to extract metric value from LinearIvOutput
197///
198/// # Method-Specific Behavior
199///
200/// * **LinearTte**: Direct interpolation on (TTE, metric) pairs
201/// * **LinearVariance**: Converts to total variance, interpolates, converts back
202/// * **SquareRootTime**: Scales metric by sqrt(target_tte/observed_tte) ratio
203fn interpolate_metric_value(
204 tte_metrics: &[(f64, LinearIvOutput)],
205 target_tte: f64,
206 method: TemporalInterpMethod,
207 allow_short_extrap: bool,
208 allow_long_extrap: bool,
209 metric_extractor: impl Fn(&LinearIvOutput) -> f64,
210) -> Option<f64> {
211 let metric_pairs: Vec<(f64, f64)> = tte_metrics
212 .iter()
213 .map(|(tte, output)| (*tte, metric_extractor(output)))
214 .collect();
215
216 match method {
217 TemporalInterpMethod::LinearTte => temporal_interp(
218 &metric_pairs,
219 target_tte,
220 allow_short_extrap,
221 allow_long_extrap,
222 ),
223 TemporalInterpMethod::LinearVariance => {
224 // Convert to total variance (w = iv^2 * t), interpolate, then back to IV
225 let variance_pairs: Vec<(f64, f64)> = metric_pairs
226 .iter()
227 .map(|(tte, iv)| (*tte, iv * iv * tte))
228 .collect();
229
230 let interpolated_variance = temporal_interp(
231 &variance_pairs,
232 target_tte,
233 allow_short_extrap,
234 allow_long_extrap,
235 )?;
236
237 if interpolated_variance <= 0.0 {
238 return None;
239 }
240
241 Some((interpolated_variance / target_tte).sqrt())
242 }
243 TemporalInterpMethod::SquareRootTime => {
244 // Scale by sqrt(t): iv_target = iv_base * sqrt(t_target / t_base)
245 // Handle edge case of zero TTE
246 if target_tte <= 0.0 {
247 return None;
248 }
249
250 // Scale values by 1/sqrt(tte) for interpolation
251 let scaled_pairs: Vec<(f64, f64)> = metric_pairs
252 .iter()
253 .filter_map(|(tte, iv)| {
254 if *tte > 0.0 {
255 Some((*tte, iv / tte.sqrt()))
256 } else {
257 None // Skip invalid TTE values
258 }
259 })
260 .collect();
261
262 if scaled_pairs.is_empty() {
263 return None;
264 }
265
266 let scaled_value = temporal_interp(
267 &scaled_pairs,
268 target_tte,
269 allow_short_extrap,
270 allow_long_extrap,
271 )?;
272
273 Some(scaled_value * target_tte.sqrt())
274 }
275 }
276}
277
278/// Build fixed time metrics by interpolating across multiple maturities
279///
280/// This is the main function for temporal interpolation, taking multi-maturity option
281/// data and producing interpolated volatility metrics at standardized time points.
282///
283/// # Arguments
284///
285/// * `data` - Multi-maturity option chain data with consistent underlying and forward
286/// * `forward` - Forward price for all contracts (should be consistent across maturities)
287/// * `temp_config` - Temporal interpolation configuration specifying target days and method
288/// * `strike_config` - Configuration for per-maturity linear IV interpolation
289///
290/// # Returns
291///
292/// A vector of `FixedTimeMetrics` containing interpolated ATM IV and delta metrics
293/// for each requested time point, sorted by time-to-expiration.
294///
295/// # Process
296///
297/// 1. **Group by maturity**: Market data is grouped by time-to-expiration with
298/// floating point precision handling
299/// 2. **Per-maturity interpolation**: Each maturity group is processed using
300/// standard linear IV interpolation to extract ATM IV and delta metrics
301/// 3. **Temporal interpolation**: Metrics are interpolated across time using
302/// the specified method (LinearTte, LinearVariance, or SquareRootTime)
303/// 4. **Extrapolation handling**: Points outside the observed range are handled
304/// according to the extrapolation settings
305/// 5. **Result assembly**: Final metrics are assembled and sorted by time
306///
307/// # Interpolation Methods
308///
309/// - **LinearTte**: Direct linear interpolation on (time, metric) pairs
310/// - **LinearVariance**: Interpolates total variance, then converts back to IV
311/// - **SquareRootTime**: Scales by sqrt(time) ratio for mean-reverting processes
312///
313/// # Error Conditions
314///
315/// * Insufficient maturities (< `min_maturities`)
316/// * Insufficient points per maturity for linear IV interpolation
317/// * Empty market data
318/// * Individual maturity processing failures (reported with context)
319///
320/// # Example
321///
322/// ```rust,no_run
323/// use surface_lib::{MarketDataRow, LinearIvConfig, TemporalConfig, TemporalInterpMethod, build_fixed_time_metrics};
324///
325/// # let market_data: Vec<MarketDataRow> = vec![];
326/// let forward = 100.0;
327/// let temp_config = TemporalConfig {
328/// fixed_days: vec![1, 7, 14, 30, 60],
329/// interp_method: TemporalInterpMethod::LinearVariance,
330/// allow_short_extrapolate: true,
331/// allow_long_extrapolate: false,
332/// min_maturities: 2,
333/// };
334/// let strike_config = LinearIvConfig::default();
335///
336/// let metrics = build_fixed_time_metrics(&market_data, forward, &temp_config, &strike_config)?;
337///
338/// for metric in &metrics {
339/// println!("{}d: ATM IV = {:.1}%", metric.tte_days, metric.atm_iv * 100.0);
340/// }
341/// # Ok::<(), Box<dyn std::error::Error>>(())
342/// ```
343///
344/// # Performance Notes
345///
346/// * Complexity scales linearly with number of maturities and target days
347/// * Each maturity requires full linear IV interpolation (O(n log n) per maturity)
348/// * Memory usage is proportional to the number of unique delta levels across all maturities
349pub fn build_fixed_time_metrics(
350 data: &[MarketDataRow],
351 forward: f64,
352 temp_config: &TemporalConfig,
353 strike_config: &LinearIvConfig,
354) -> Result<Vec<FixedTimeMetrics>> {
355 if data.is_empty() {
356 return Err(anyhow!("No market data provided"));
357 }
358
359 // Group data by time-to-expiration
360 let tte_groups = group_by_tte(data);
361
362 if tte_groups.len() < temp_config.min_maturities {
363 return Err(anyhow!(
364 "Insufficient maturities: {} < {}",
365 tte_groups.len(),
366 temp_config.min_maturities
367 ));
368 }
369
370 // Build LinearIvOutput for each maturity
371 let mut maturity_outputs = Vec::new();
372
373 for (tte, group_data) in &tte_groups {
374 match build_linear_iv(group_data, forward, *tte, strike_config) {
375 Ok(output) => {
376 maturity_outputs.push((*tte, output));
377 }
378 Err(e) => {
379 return Err(anyhow!("Failed to build linear IV for TTE {}: {}", tte, e));
380 }
381 }
382 }
383
384 if maturity_outputs.is_empty() {
385 return Err(anyhow!("No valid maturity outputs produced"));
386 }
387
388 // Sort by TTE for interpolation
389 maturity_outputs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
390
391 let min_tte = maturity_outputs[0].0;
392 let max_tte = maturity_outputs[maturity_outputs.len() - 1].0;
393
394 // Build metrics for each requested fixed day
395 let mut results = Vec::new();
396
397 for &fixed_days in &temp_config.fixed_days {
398 let target_tte = fixed_days as f64 / 365.0;
399
400 // Check if this point should be skipped due to extrapolation settings
401 // Use epsilon comparison for floating point precision
402 if (target_tte - min_tte) < -TEMPORAL_EPSILON && !temp_config.allow_short_extrapolate {
403 continue;
404 }
405 if (target_tte - max_tte) > TEMPORAL_EPSILON && !temp_config.allow_long_extrapolate {
406 continue;
407 }
408
409 // Interpolate ATM IV
410 let atm_iv = interpolate_metric_value(
411 &maturity_outputs,
412 target_tte,
413 temp_config.interp_method,
414 temp_config.allow_short_extrapolate,
415 temp_config.allow_long_extrapolate,
416 |output| output.atm_iv,
417 );
418
419 let atm_iv = match atm_iv {
420 Some(iv) if iv > 0.0 => iv,
421 _ => continue, // Skip this point if ATM IV interpolation fails
422 };
423
424 // Collect all unique delta levels across all maturities
425 let mut all_delta_levels = std::collections::HashSet::new();
426 for (_, output) in &maturity_outputs {
427 for delta_metric in &output.delta_metrics {
428 // Use limited precision for delta matching
429 let delta_key = format!("{:.6}", delta_metric.delta_level);
430 all_delta_levels.insert(delta_key);
431 }
432 }
433
434 let mut delta_metrics = Vec::new();
435
436 // Interpolate each delta level
437 for delta_key in all_delta_levels {
438 let delta_level: f64 = delta_key.parse().unwrap();
439
440 // Extract RR and BF values for this delta across all maturities
441 let rr_values: Vec<(f64, f64)> = maturity_outputs
442 .iter()
443 .filter_map(|(tte, output)| {
444 output
445 .delta_metrics
446 .iter()
447 .find(|dm| (dm.delta_level - delta_level).abs() < 1e-6)
448 .map(|dm| (*tte, dm.risk_reversal))
449 })
450 .collect();
451
452 let bf_values: Vec<(f64, f64)> = maturity_outputs
453 .iter()
454 .filter_map(|(tte, output)| {
455 output
456 .delta_metrics
457 .iter()
458 .find(|dm| (dm.delta_level - delta_level).abs() < 1e-6)
459 .map(|dm| (*tte, dm.butterfly))
460 })
461 .collect();
462
463 // Only proceed if we have sufficient data for this delta level
464 if rr_values.len() >= 2 && bf_values.len() >= 2 {
465 // Interpolate RR and BF for this delta level
466 let rr = temporal_interp(
467 &rr_values,
468 target_tte,
469 temp_config.allow_short_extrapolate,
470 temp_config.allow_long_extrapolate,
471 );
472
473 let bf = temporal_interp(
474 &bf_values,
475 target_tte,
476 temp_config.allow_short_extrapolate,
477 temp_config.allow_long_extrapolate,
478 );
479
480 if let (Some(rr_val), Some(bf_val)) = (rr, bf) {
481 delta_metrics.push(DeltaMetrics {
482 delta_level,
483 risk_reversal: rr_val,
484 butterfly: bf_val,
485 });
486 }
487 }
488 }
489
490 // Sort delta metrics by delta level for consistency
491 delta_metrics.sort_by(|a, b| a.delta_level.partial_cmp(&b.delta_level).unwrap());
492
493 results.push(FixedTimeMetrics {
494 tte_days: fixed_days,
495 tte_years: target_tte,
496 atm_iv,
497 delta_metrics,
498 });
499 }
500
501 // Sort results by TTE days
502 results.sort_by_key(|m| m.tte_days);
503
504 Ok(results)
505}