1mod constantswaptionvol;
41mod interpolatedswaptionvolcube;
42mod sabrvolcube;
43mod swaptionvolcube;
44mod swaptionvoldiscrete;
45mod swaptionvolmatrix;
46
47pub use constantswaptionvol::ConstantSwaptionVolatility;
48pub use interpolatedswaptionvolcube::InterpolatedSwaptionVolatilityCube;
49pub use sabrvolcube::SabrSwaptionVolatilityCube;
50pub use swaptionvolcube::{SwaptionCubeSmileSection, SwaptionVolatilityCube};
51pub use swaptionvoldiscrete::SwaptionVolatilityDiscrete;
52pub use swaptionvolmatrix::SwaptionVolatilityMatrix;
53
54use crate::errors::QlResult;
55use crate::termstructures::volatility::{VolatilityTermStructure, VolatilityType};
56use crate::time::date::Date;
57use crate::time::period::Period;
58use crate::time::timeunit::TimeUnit;
59use crate::types::{Rate, Real, Time, Volatility};
60use crate::{fail, require};
61
62pub struct SwaptionVolatilityGrid {
74 pub option_times: Vec<Time>,
76 pub swap_lengths: Vec<Time>,
78 pub option_dates: Vec<Date>,
80 pub swap_tenors: Vec<Period>,
82}
83
84pub trait SwaptionVolatilityStructure: VolatilityTermStructure {
91 fn volatility_impl(
94 &self,
95 option_time: Time,
96 swap_length: Time,
97 strike: Rate,
98 ) -> QlResult<Volatility>;
99
100 fn max_swap_tenor(&self) -> Period;
102
103 fn volatility_type(&self) -> VolatilityType {
105 VolatilityType::ShiftedLognormal
106 }
107
108 fn shift_impl(&self, _option_time: Time, _swap_length: Time) -> QlResult<Real> {
111 require_lognormal_for_shift(self.volatility_type())?;
112 Ok(0.0)
113 }
114
115 fn discrete_grid(&self) -> QlResult<SwaptionVolatilityGrid> {
123 fail!(
124 "swaption vol structure is not grid-backed (not a SwaptionVolatilityDiscrete); the \
125 SABR cube dense ATM-calibration fill requires a discrete ATM surface"
126 )
127 }
128
129 fn max_swap_length(&self) -> QlResult<Time> {
131 self.swap_length_tenor(self.max_swap_tenor())
132 }
133
134 fn swap_length_tenor(&self, swap_tenor: Period) -> QlResult<Time> {
137 require!(
138 swap_tenor.length() > 0,
139 "non-positive swap tenor ({swap_tenor}) given"
140 );
141 match swap_tenor.units() {
142 TimeUnit::Months => Ok(swap_tenor.length() as Time / 12.0),
143 TimeUnit::Years => Ok(swap_tenor.length() as Time),
144 other => fail!("invalid time unit ({other}) for swap length"),
145 }
146 }
147
148 fn swap_length(&self, start: Date, end: Date) -> QlResult<Time> {
151 require!(
152 end > start,
153 "swap end date ({end}) must be greater than start ({start})"
154 );
155 let months = ((end - start) as Time / 365.25 * 12.0).round();
156 Ok(months / 12.0)
157 }
158
159 fn check_swap_tenor(&self, swap_tenor: Period, extrapolate: bool) -> QlResult<()> {
162 require!(
163 swap_tenor.length() > 0,
164 "non-positive swap tenor ({swap_tenor}) given"
165 );
166 require!(
167 extrapolate || self.allows_extrapolation() || swap_tenor <= self.max_swap_tenor(),
168 "swap tenor ({swap_tenor}) is past max tenor ({max})",
169 max = self.max_swap_tenor()
170 );
171 Ok(())
172 }
173
174 fn check_swap_length(&self, swap_length: Time, extrapolate: bool) -> QlResult<()> {
177 if swap_length <= 0.0 {
178 fail!("non-positive swap length ({swap_length}) given");
179 }
180 require!(
181 extrapolate || self.allows_extrapolation() || swap_length <= self.max_swap_length()?,
182 "swap length ({swap_length}) is past max length ({max})",
183 max = self.max_swap_length()?
184 );
185 Ok(())
186 }
187
188 fn volatility(
190 &self,
191 option_date: Date,
192 swap_length: Time,
193 strike: Rate,
194 extrapolate: bool,
195 ) -> QlResult<Volatility> {
196 self.check_swap_length(swap_length, extrapolate)?;
197 self.check_range_date(option_date, extrapolate)?;
198 self.check_strike(strike, extrapolate)?;
199 let option_time = self.time_from_reference(option_date)?;
200 self.volatility_impl(option_time, swap_length, strike)
201 }
202
203 fn volatility_time(
205 &self,
206 option_time: Time,
207 swap_length: Time,
208 strike: Rate,
209 extrapolate: bool,
210 ) -> QlResult<Volatility> {
211 self.check_swap_length(swap_length, extrapolate)?;
212 self.check_range_time(option_time, extrapolate)?;
213 self.check_strike(strike, extrapolate)?;
214 self.volatility_impl(option_time, swap_length, strike)
215 }
216
217 fn volatility_tenors(
219 &self,
220 option_tenor: Period,
221 swap_tenor: Period,
222 strike: Rate,
223 extrapolate: bool,
224 ) -> QlResult<Volatility> {
225 let option_date = self.option_date_from_tenor(option_tenor)?;
226 let swap_length = self.swap_length_tenor(swap_tenor)?;
227 self.volatility(option_date, swap_length, strike, extrapolate)
228 }
229
230 fn black_variance(
232 &self,
233 option_date: Date,
234 swap_length: Time,
235 strike: Rate,
236 extrapolate: bool,
237 ) -> QlResult<Real> {
238 let v = self.volatility(option_date, swap_length, strike, extrapolate)?;
239 let option_time = self.time_from_reference(option_date)?;
240 Ok(v * v * option_time)
241 }
242
243 fn black_variance_time(
245 &self,
246 option_time: Time,
247 swap_length: Time,
248 strike: Rate,
249 extrapolate: bool,
250 ) -> QlResult<Real> {
251 let v = self.volatility_time(option_time, swap_length, strike, extrapolate)?;
252 Ok(v * v * option_time)
253 }
254
255 fn black_variance_tenors(
257 &self,
258 option_tenor: Period,
259 swap_tenor: Period,
260 strike: Rate,
261 extrapolate: bool,
262 ) -> QlResult<Real> {
263 let option_date = self.option_date_from_tenor(option_tenor)?;
264 let swap_length = self.swap_length_tenor(swap_tenor)?;
265 self.black_variance(option_date, swap_length, strike, extrapolate)
266 }
267
268 fn shift(&self, option_date: Date, swap_length: Time, extrapolate: bool) -> QlResult<Real> {
270 self.check_swap_length(swap_length, extrapolate)?;
271 self.check_range_date(option_date, extrapolate)?;
272 let option_time = self.time_from_reference(option_date)?;
273 self.shift_impl(option_time, swap_length)
274 }
275
276 fn shift_time(
278 &self,
279 option_time: Time,
280 swap_length: Time,
281 extrapolate: bool,
282 ) -> QlResult<Real> {
283 self.check_swap_length(swap_length, extrapolate)?;
284 self.check_range_time(option_time, extrapolate)?;
285 self.shift_impl(option_time, swap_length)
286 }
287}
288
289fn require_lognormal_for_shift(volatility_type: VolatilityType) -> QlResult<()> {
290 require!(
291 volatility_type == VolatilityType::ShiftedLognormal,
292 "shift parameter only makes sense for lognormal volatilities"
293 );
294 Ok(())
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300 use crate::patterns::observable::{AsObservable, Observable};
301 use crate::termstructures::{TermStructure, TermStructureBase};
302 use crate::time::calendars::target::Target;
303 use crate::time::date::Month;
304 use crate::time::daycounters::actual360::Actual360;
305
306 struct MockSwaptionVol {
307 base: TermStructureBase,
308 vol: Volatility,
309 volatility_type: VolatilityType,
310 shift: Real,
311 }
312
313 impl MockSwaptionVol {
314 fn flat(vol: Volatility) -> MockSwaptionVol {
315 MockSwaptionVol {
316 base: TermStructureBase::with_reference_date(
317 Date::new(15, Month::June, 2026),
318 Some(Target::new()),
319 Some(Actual360::new()),
320 ),
321 vol,
322 volatility_type: VolatilityType::ShiftedLognormal,
323 shift: 0.0,
324 }
325 }
326 }
327
328 impl AsObservable for MockSwaptionVol {
329 fn observable(&self) -> &Observable {
330 self.base.observable()
331 }
332 }
333
334 impl TermStructure for MockSwaptionVol {
335 fn base(&self) -> &TermStructureBase {
336 &self.base
337 }
338
339 fn max_date(&self) -> Date {
340 Date::max_date()
341 }
342 }
343
344 impl VolatilityTermStructure for MockSwaptionVol {
345 fn business_day_convention(
346 &self,
347 ) -> crate::time::businessdayconvention::BusinessDayConvention {
348 crate::time::businessdayconvention::BusinessDayConvention::Following
349 }
350
351 fn min_strike(&self) -> Rate {
352 Rate::MIN
353 }
354
355 fn max_strike(&self) -> Rate {
356 Rate::MAX
357 }
358 }
359
360 impl SwaptionVolatilityStructure for MockSwaptionVol {
361 fn volatility_impl(&self, _t: Time, _l: Time, _strike: Rate) -> QlResult<Volatility> {
362 Ok(self.vol)
363 }
364
365 fn max_swap_tenor(&self) -> Period {
366 Period::new(100, TimeUnit::Years)
367 }
368
369 fn volatility_type(&self) -> VolatilityType {
370 self.volatility_type
371 }
372
373 fn shift_impl(&self, option_time: Time, swap_length: Time) -> QlResult<Real> {
374 require_lognormal_for_shift(self.volatility_type())?;
375 let _ = (option_time, swap_length);
376 Ok(self.shift)
377 }
378 }
379
380 #[test]
381 fn discrete_grid_defaults_to_err_for_a_non_grid_structure() {
382 let s = MockSwaptionVol::flat(0.2);
383 assert!(
384 s.discrete_grid().is_err(),
385 "a structure not built on SwaptionVolatilityDiscrete must not answer discrete_grid"
386 );
387 }
388
389 #[test]
390 fn swap_length_from_tenor_uses_months_and_years() {
391 let s = MockSwaptionVol::flat(0.2);
392 assert_eq!(
393 s.swap_length_tenor(Period::new(6, TimeUnit::Months))
394 .unwrap(),
395 0.5
396 );
397 assert_eq!(
398 s.swap_length_tenor(Period::new(5, TimeUnit::Years))
399 .unwrap(),
400 5.0
401 );
402 assert!(
403 s.swap_length_tenor(Period::new(0, TimeUnit::Years))
404 .is_err()
405 );
406 assert!(s.swap_length_tenor(Period::new(7, TimeUnit::Days)).is_err());
407 }
408
409 #[test]
410 fn swap_length_from_dates_rounds_to_whole_months() {
411 let s = MockSwaptionVol::flat(0.2);
412 let start = Date::new(15, Month::June, 2026);
413 let five_years = s.swap_length(start, start + 5 * 365).unwrap();
414 assert!((five_years - 5.0).abs() < 1e-12);
415 let one_month = s.swap_length(start, start + 30).unwrap();
416 assert!((one_month - 1.0 / 12.0).abs() < 1e-12);
417 assert!(s.swap_length(start, start).is_err());
418 }
419
420 #[test]
421 fn black_variance_is_vol_squared_times_option_time() {
422 let s = MockSwaptionVol::flat(0.25);
423 let var = s.black_variance_time(2.0, 5.0, 0.03, false).unwrap();
424 assert!((var - 0.25 * 0.25 * 2.0).abs() < 1e-15);
425
426 let date = s.reference_date().unwrap() + 180;
427 let t = s.time_from_reference(date).unwrap();
428 let by_date = s.black_variance(date, 5.0, 0.03, false).unwrap();
429 let by_time = s.black_variance_time(t, 5.0, 0.03, false).unwrap();
430 assert!((by_date - by_time).abs() < 1e-15);
431 assert!((by_date - 0.25 * 0.25 * t).abs() < 1e-15);
432 }
433
434 #[test]
435 fn tenor_forms_convert_both_axes() {
436 let s = MockSwaptionVol::flat(0.2);
437 let vol = s
438 .volatility_tenors(
439 Period::new(1, TimeUnit::Years),
440 Period::new(5, TimeUnit::Years),
441 0.03,
442 false,
443 )
444 .unwrap();
445 assert_eq!(vol, 0.2);
446 let var = s
447 .black_variance_tenors(
448 Period::new(1, TimeUnit::Years),
449 Period::new(5, TimeUnit::Years),
450 0.03,
451 false,
452 )
453 .unwrap();
454 assert!(var > 0.0);
455 }
456
457 #[test]
458 fn shift_is_gated_by_volatility_type() {
459 let mut s = MockSwaptionVol::flat(0.2);
460 s.shift = 0.01;
461 assert_eq!(
462 s.shift(s.reference_date().unwrap() + 90, 5.0, false)
463 .unwrap(),
464 0.01
465 );
466
467 s.volatility_type = VolatilityType::Normal;
468 assert!(s.shift_time(1.0, 5.0, false).is_err());
469 }
470
471 #[test]
472 fn non_positive_swap_length_is_rejected() {
473 let s = MockSwaptionVol::flat(0.2);
474 assert!(s.volatility_time(1.0, 0.0, 0.03, false).is_err());
475 assert!(s.volatility_time(1.0, -1.0, 0.03, false).is_err());
476 }
477
478 #[test]
479 fn max_swap_length_follows_max_swap_tenor() {
480 let s = MockSwaptionVol::flat(0.2);
481 assert_eq!(s.max_swap_length().unwrap(), 100.0);
482 }
483}