surface_lib/models/linear_iv/
interp.rs1use anyhow::{anyhow, Result};
2use roots::find_root_brent;
3use statrs::distribution::{ContinuousCDF, Normal};
4
5use super::types::*;
6
7pub fn prepare_points(points: &[MarketDataRow], forward: f64, tte: f64) -> Vec<(f64, f64)> {
10 use std::collections::HashMap;
11
12 let mut x_to_omegas: HashMap<String, Vec<f64>> = HashMap::new();
13
14 for point in points {
15 if point.market_iv <= 0.0 {
16 continue; }
18
19 let x = (point.strike_price / forward).ln(); let omega = point.market_iv * point.market_iv * tte; let x_key = format!("{:.8}", x); x_to_omegas.entry(x_key).or_default().push(omega);
25 }
26
27 let mut result: Vec<(f64, f64)> = x_to_omegas
29 .into_iter()
30 .map(|(x_str, omegas)| {
31 let x: f64 = x_str.parse().unwrap();
32 let avg_omega = omegas.iter().sum::<f64>() / omegas.len() as f64;
33 (x, avg_omega)
34 })
35 .collect();
36
37 result.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
39
40 result
41}
42
43pub fn linear_interp(sorted_points: &[(f64, f64)], query_x: f64) -> Option<f64> {
46 linear_interp_with_config(sorted_points, query_x, true)
47}
48
49pub fn linear_interp_with_config(
52 sorted_points: &[(f64, f64)],
53 query_x: f64,
54 allow_extrapolation: bool,
55) -> Option<f64> {
56 if sorted_points.is_empty() {
57 return None;
58 }
59
60 if sorted_points.len() == 1 {
61 return Some(sorted_points[0].1);
62 }
63
64 let first_x = sorted_points[0].0;
65 let last_x = sorted_points[sorted_points.len() - 1].0;
66
67 if query_x < first_x {
69 if !allow_extrapolation {
70 return None; }
72 if sorted_points.len() < 2 {
74 return Some(sorted_points[0].1);
75 }
76 let (x1, y1) = sorted_points[0];
77 let (x2, y2) = sorted_points[1];
78 let slope = (y2 - y1) / (x2 - x1);
79 let extrapolated = y1 + slope * (query_x - x1);
80 return if extrapolated > 0.0 {
81 Some(extrapolated)
82 } else {
83 None
84 };
85 }
86
87 if query_x > last_x {
88 if !allow_extrapolation {
89 return None; }
91 let n = sorted_points.len();
93 let (x1, y1) = sorted_points[n - 2];
94 let (x2, y2) = sorted_points[n - 1];
95 let slope = (y2 - y1) / (x2 - x1);
96 let extrapolated = y2 + slope * (query_x - x2);
97 return if extrapolated > 0.0 {
98 Some(extrapolated)
99 } else {
100 None
101 };
102 }
103
104 for i in 0..sorted_points.len() - 1 {
106 let (x1, y1) = sorted_points[i];
107 let (x2, y2) = sorted_points[i + 1];
108
109 if query_x >= x1 && query_x <= x2 {
110 let t = (query_x - x1) / (x2 - x1);
112 let interpolated = y1 + t * (y2 - y1);
113 return Some(interpolated);
114 }
115 }
116
117 None
118}
119
120pub fn compute_atm_iv(points: &[MarketDataRow], forward: f64, tte: f64) -> Result<f64> {
122 if tte <= 0.0 {
123 return Err(anyhow!("Time to expiration must be positive, got: {}", tte));
124 }
125
126 let sorted_points = prepare_points(points, forward, tte);
127
128 if sorted_points.len() < 2 {
129 return Err(anyhow!("Insufficient points for ATM IV interpolation"));
130 }
131
132 let omega_atm = linear_interp(&sorted_points, 0.0)
133 .ok_or_else(|| anyhow!("Failed to interpolate ATM variance"))?;
134
135 if omega_atm <= 0.0 {
136 return Err(anyhow!("Non-positive ATM variance: {}", omega_atm));
137 }
138
139 Ok((omega_atm / tte).sqrt())
140}
141
142pub fn bs_delta(x: f64, sigma: f64, tte: f64, is_call: bool, q: f64) -> f64 {
146 if sigma <= 0.0 || tte <= 0.0 {
147 return if is_call { 0.0 } else { -1.0 };
148 }
149
150 let d1 = -x / (sigma * tte.sqrt()) + 0.5 * sigma * tte.sqrt();
152 let normal = Normal::new(0.0, 1.0).unwrap();
153
154 let fwd_factor = (-q * tte).exp();
156
157 if is_call {
158 normal.cdf(d1) * fwd_factor
159 } else {
160 (normal.cdf(d1) - 1.0) * fwd_factor
161 }
162}
163
164pub fn compute_fixed_delta_iv(
167 target_delta: f64,
168 sorted_points: &[(f64, f64)],
169 tte: f64,
170 tol: f64,
171) -> Result<f64> {
172 compute_fixed_delta_iv_with_config(target_delta, sorted_points, tte, tol, true, 0.0)
173}
174
175pub fn compute_fixed_delta_iv_with_config(
178 target_delta: f64,
179 sorted_points: &[(f64, f64)],
180 tte: f64,
181 tol: f64,
182 allow_extrapolation: bool,
183 q: f64,
184) -> Result<f64> {
185 if sorted_points.is_empty() {
186 return Err(anyhow!("No points available for delta solving"));
187 }
188
189 let is_call = target_delta > 0.0;
190
191 let objective = |x: f64| -> f64 {
193 let omega = match linear_interp_with_config(sorted_points, x, allow_extrapolation) {
194 Some(w) if w > 0.0 => w,
195 _ => {
196 return if is_call {
199 if target_delta > 0.0 {
200 -10.0
201 } else {
202 10.0
203 }
204 } else if target_delta < 0.0 {
205 10.0
206 } else {
207 -10.0
208 };
209 }
210 };
211
212 let sigma = (omega / tte).sqrt();
213 bs_delta(x, sigma, tte, is_call, q) - target_delta
214 };
215
216 let min_x = sorted_points[0].0;
218 let max_x = sorted_points[sorted_points.len() - 1].0;
219
220 let search_min = min_x - 1.0;
222 let search_max = max_x + 1.0;
223
224 match find_root_brent(search_min, search_max, &objective, &mut tol.clone()) {
226 Ok(x_solution) => {
227 let omega =
229 linear_interp_with_config(sorted_points, x_solution, allow_extrapolation)
230 .ok_or_else(|| anyhow!("Failed to interpolate at solution x={}", x_solution))?;
231
232 if omega <= 0.0 {
233 return Err(anyhow!("Non-positive variance at solution: {}", omega));
234 }
235
236 Ok((omega / tte).sqrt())
237 }
238 Err(_) => Err(anyhow!(
239 "Root finding failed for target_delta={}",
240 target_delta
241 )),
242 }
243}
244
245pub fn compute_all_metrics(
247 delta_ivs: &[DeltaIv],
248 atm_iv: f64,
249) -> (Vec<DeltaMetrics>, Option<f64>, Option<f64>) {
250 let mut metrics = Vec::new();
251 let mut rr_25 = None;
252 let mut bf_25 = None;
253
254 let positive_deltas: Vec<f64> = delta_ivs
256 .iter()
257 .filter_map(|div| {
258 if div.delta > 0.0 {
259 Some(div.delta)
260 } else {
261 None
262 }
263 })
264 .collect();
265
266 for &pos_delta in &positive_deltas {
267 let neg_delta = -pos_delta;
268
269 let call_iv = delta_ivs
270 .iter()
271 .find(|div| (div.delta - pos_delta).abs() < 1e-10)
272 .map(|div| div.iv);
273
274 let put_iv = delta_ivs
275 .iter()
276 .find(|div| (div.delta - neg_delta).abs() < 1e-10)
277 .map(|div| div.iv);
278
279 if let (Some(call_vol), Some(put_vol)) = (call_iv, put_iv) {
280 let rr = call_vol - put_vol;
281 let bf = (call_vol + put_vol) / 2.0 - atm_iv;
282
283 metrics.push(DeltaMetrics {
284 delta_level: pos_delta,
285 risk_reversal: rr,
286 butterfly: bf,
287 });
288
289 if (pos_delta - 0.25).abs() < 1e-10 {
291 rr_25 = Some(rr);
292 bf_25 = Some(bf);
293 }
294 }
295 }
296
297 (metrics, rr_25, bf_25)
298}
299
300pub fn compute_metrics(delta_ivs: &[DeltaIv], atm_iv: f64) -> (Option<f64>, Option<f64>) {
302 let (_, rr_25, bf_25) = compute_all_metrics(delta_ivs, atm_iv);
303 (rr_25, bf_25)
304}
305
306fn check_point_coverage(points: &[MarketDataRow], config: &LinearIvConfig) {
309 let has_calls = points.iter().any(|p| p.option_type == "call");
310 let has_puts = points.iter().any(|p| p.option_type == "put");
311
312 let has_negative_deltas = config.deltas.iter().any(|&d| d < 0.0);
313 let has_positive_deltas = config.deltas.iter().any(|&d| d > 0.0);
314
315 if has_negative_deltas && !has_puts {
316 eprintln!("Warning: Negative deltas requested but no put options in data. Some delta calculations may fail.");
317 }
318
319 if has_positive_deltas && !has_calls {
320 eprintln!("Warning: Positive deltas requested but no call options in data. Some delta calculations may fail.");
321 }
322
323 if !has_calls && !has_puts {
324 eprintln!("Warning: No clear option type classification in data.");
325 }
326}
327
328pub fn build_linear_iv_from_market_data(
331 points: &[MarketDataRow],
332 config: &LinearIvConfig,
333) -> Result<LinearIvOutput> {
334 if points.is_empty() {
335 return Err(anyhow!("No market data provided"));
336 }
337
338 let forward = points[0].underlying_price;
339 let tte = points[0].years_to_exp;
340
341 build_linear_iv(points, forward, tte, config)
342}
343
344pub fn build_linear_iv(
347 points: &[MarketDataRow],
348 forward: f64,
349 tte: f64,
350 config: &LinearIvConfig,
351) -> Result<LinearIvOutput> {
352 if points.len() < config.min_points {
353 return Err(anyhow!(
354 "Insufficient points: {} < {}",
355 points.len(),
356 config.min_points
357 ));
358 }
359
360 check_point_coverage(points, config);
362
363 let atm_iv = compute_atm_iv(points, forward, tte)?;
365
366 let sorted_points = prepare_points(points, forward, tte);
368
369 let mut delta_ivs = Vec::new();
371
372 for &delta in &config.deltas {
373 match compute_fixed_delta_iv_with_config(
374 delta,
375 &sorted_points,
376 tte,
377 config.solver_tol,
378 config.allow_extrapolation,
379 config.dividend_yield,
380 ) {
381 Ok(iv) => {
382 delta_ivs.push(DeltaIv { delta, iv });
383 }
384 Err(_) => {
385 continue;
387 }
388 }
389 }
390
391 let (delta_metrics, rr_25, bf_25) = compute_all_metrics(&delta_ivs, atm_iv);
393
394 Ok(LinearIvOutput {
395 atm_iv,
396 delta_ivs,
397 rr_25,
398 bf_25,
399 delta_metrics,
400 tte,
401 })
402}