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}