Skip to main content

libitofin/termstructures/volatility/swaption/
constantswaptionvol.rs

1//! Constant swaption volatility.
2//!
3//! Port of `ql/termstructures/volatility/swaption/swaptionconstantvol.{hpp,cpp}`:
4//! [`ConstantSwaptionVolatility`] implements
5//! [`SwaptionVolatilityStructure`](super::SwaptionVolatilityStructure) with a
6//! single volatility, no option-time, swap-length or strike dependence. The
7//! volatility is either a fixed value (wrapped in an unobservable
8//! [`SimpleQuote`](crate::quotes::SimpleQuote), as in C++) or a quote handle
9//! whose changes propagate to the structure's observers. The business-day
10//! convention, volatility type and shift are pinned by the constructor; the
11//! strike domain spans all of `Real` and the swap-tenor domain spans 100 years.
12//!
13//! The moving constructors take the shared [`Settings`] handle explicitly, per
14//! D5.
15
16use crate::errors::QlResult;
17use crate::handle::Handle;
18use crate::patterns::observable::{AsObservable, Observable};
19use crate::quotes::{Quote, make_quote_handle};
20use crate::settings::Settings;
21use crate::shared::Shared;
22use crate::termstructures::volatility::{VolatilityTermStructure, VolatilityType};
23use crate::termstructures::{TermStructure, TermStructureBase};
24use crate::time::businessdayconvention::BusinessDayConvention;
25use crate::time::calendar::Calendar;
26use crate::time::date::Date;
27use crate::time::daycounter::DayCounter;
28use crate::time::period::Period;
29use crate::time::timeunit::TimeUnit;
30use crate::types::{Natural, Rate, Real, Time, Volatility};
31
32use super::SwaptionVolatilityStructure;
33
34/// Constant swaption volatility, no time-strike dependence.
35pub struct ConstantSwaptionVolatility {
36    base: TermStructureBase,
37    business_day_convention: BusinessDayConvention,
38    volatility: Handle<dyn Quote>,
39    max_swap_tenor: Period,
40    volatility_type: VolatilityType,
41    shift: Real,
42}
43
44impl ConstantSwaptionVolatility {
45    fn wrap(volatility: Volatility) -> Handle<dyn Quote> {
46        make_quote_handle(volatility).handle()
47    }
48
49    fn assemble(
50        base: TermStructureBase,
51        business_day_convention: BusinessDayConvention,
52        volatility: Handle<dyn Quote>,
53        volatility_type: VolatilityType,
54        shift: Real,
55        observe: bool,
56    ) -> ConstantSwaptionVolatility {
57        if observe {
58            volatility.register_observer(&base.updater());
59        }
60        ConstantSwaptionVolatility {
61            base,
62            business_day_convention,
63            volatility,
64            max_swap_tenor: Period::new(100, TimeUnit::Years),
65            volatility_type,
66            shift,
67        }
68    }
69
70    /// Fixed reference date, fixed market data.
71    pub fn new(
72        reference_date: Date,
73        calendar: Calendar,
74        business_day_convention: BusinessDayConvention,
75        volatility: Volatility,
76        day_counter: DayCounter,
77        volatility_type: VolatilityType,
78        shift: Real,
79    ) -> ConstantSwaptionVolatility {
80        Self::assemble(
81            TermStructureBase::with_reference_date(
82                reference_date,
83                Some(calendar),
84                Some(day_counter),
85            ),
86            business_day_convention,
87            Self::wrap(volatility),
88            volatility_type,
89            shift,
90            false,
91        )
92    }
93
94    /// Fixed reference date, quote-backed market data; quote changes notify the
95    /// structure's observers.
96    pub fn with_quote(
97        reference_date: Date,
98        calendar: Calendar,
99        business_day_convention: BusinessDayConvention,
100        volatility: Handle<dyn Quote>,
101        day_counter: DayCounter,
102        volatility_type: VolatilityType,
103        shift: Real,
104    ) -> ConstantSwaptionVolatility {
105        Self::assemble(
106            TermStructureBase::with_reference_date(
107                reference_date,
108                Some(calendar),
109                Some(day_counter),
110            ),
111            business_day_convention,
112            volatility,
113            volatility_type,
114            shift,
115            true,
116        )
117    }
118
119    /// Reference date moving off the evaluation date, fixed market data.
120    #[allow(clippy::too_many_arguments)]
121    pub fn moving(
122        settlement_days: Natural,
123        calendar: Calendar,
124        business_day_convention: BusinessDayConvention,
125        volatility: Volatility,
126        day_counter: DayCounter,
127        volatility_type: VolatilityType,
128        shift: Real,
129        settings: Shared<Settings<Date>>,
130    ) -> ConstantSwaptionVolatility {
131        Self::assemble(
132            TermStructureBase::moving(settlement_days, calendar, Some(day_counter), settings),
133            business_day_convention,
134            Self::wrap(volatility),
135            volatility_type,
136            shift,
137            false,
138        )
139    }
140
141    /// Reference date moving off the evaluation date, quote-backed market data;
142    /// quote changes notify the structure's observers.
143    #[allow(clippy::too_many_arguments)]
144    pub fn moving_with_quote(
145        settlement_days: Natural,
146        calendar: Calendar,
147        business_day_convention: BusinessDayConvention,
148        volatility: Handle<dyn Quote>,
149        day_counter: DayCounter,
150        volatility_type: VolatilityType,
151        shift: Real,
152        settings: Shared<Settings<Date>>,
153    ) -> ConstantSwaptionVolatility {
154        Self::assemble(
155            TermStructureBase::moving(settlement_days, calendar, Some(day_counter), settings),
156            business_day_convention,
157            volatility,
158            volatility_type,
159            shift,
160            true,
161        )
162    }
163}
164
165impl AsObservable for ConstantSwaptionVolatility {
166    fn observable(&self) -> &Observable {
167        self.base.observable()
168    }
169}
170
171impl TermStructure for ConstantSwaptionVolatility {
172    fn base(&self) -> &TermStructureBase {
173        &self.base
174    }
175
176    fn max_date(&self) -> Date {
177        Date::max_date()
178    }
179}
180
181impl VolatilityTermStructure for ConstantSwaptionVolatility {
182    fn business_day_convention(&self) -> BusinessDayConvention {
183        self.business_day_convention
184    }
185
186    fn min_strike(&self) -> Rate {
187        Rate::MIN
188    }
189
190    fn max_strike(&self) -> Rate {
191        Rate::MAX
192    }
193}
194
195impl SwaptionVolatilityStructure for ConstantSwaptionVolatility {
196    fn volatility_impl(
197        &self,
198        _option_time: Time,
199        _swap_length: Time,
200        _strike: Rate,
201    ) -> QlResult<Volatility> {
202        self.volatility.current_link()?.value()
203    }
204
205    fn max_swap_tenor(&self) -> Period {
206        self.max_swap_tenor
207    }
208
209    fn volatility_type(&self) -> VolatilityType {
210        self.volatility_type
211    }
212
213    fn shift_impl(&self, _option_time: Time, _swap_length: Time) -> QlResult<Real> {
214        super::require_lognormal_for_shift(self.volatility_type)?;
215        Ok(self.shift)
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222    use crate::quotes::SimpleQuote;
223    use crate::shared::{Shared, shared};
224    use crate::test_support::{Flag, as_observer};
225    use crate::time::calendars::target::Target;
226    use crate::time::date::Month;
227    use crate::time::daycounters::actual360::Actual360;
228
229    fn flat_surface(vol: Volatility) -> (Date, ConstantSwaptionVolatility) {
230        let reference = Date::new(15, Month::June, 2026);
231        let surface = ConstantSwaptionVolatility::new(
232            reference,
233            Target::new(),
234            BusinessDayConvention::Following,
235            vol,
236            Actual360::new(),
237            VolatilityType::ShiftedLognormal,
238            0.0,
239        );
240        (reference, surface)
241    }
242
243    #[test]
244    fn volatility_is_constant_across_option_and_swap_axes() {
245        let (reference, surface) = flat_surface(0.2);
246        for option_time in [0.0, 0.25, 1.0, 10.0] {
247            for swap_length in [0.5, 1.0, 30.0] {
248                for strike in [-0.01, 0.0, 0.03, 1.0e6] {
249                    assert_eq!(
250                        surface
251                            .volatility_time(option_time, swap_length, strike, false)
252                            .unwrap(),
253                        0.2
254                    );
255                }
256            }
257        }
258        assert_eq!(
259            surface
260                .volatility(reference + 180, 5.0, 0.03, false)
261                .unwrap(),
262            0.2
263        );
264    }
265
266    #[test]
267    fn black_variance_is_vol_squared_times_option_time() {
268        let (reference, surface) = flat_surface(0.25);
269        let var = surface.black_variance_time(2.0, 5.0, 0.03, false).unwrap();
270        assert!((var - 0.125).abs() < 1e-15);
271
272        let date = reference + 180;
273        let t = surface.time_from_reference(date).unwrap();
274        assert_eq!(t, 0.5);
275        let by_date = surface.black_variance(date, 5.0, 0.03, false).unwrap();
276        let by_time = surface.black_variance_time(t, 5.0, 0.03, false).unwrap();
277        assert_eq!(by_date, by_time);
278        assert!((by_date - 0.25 * 0.25 * 0.5).abs() < 1e-15);
279    }
280
281    #[test]
282    fn max_swap_tenor_and_length_span_a_century() {
283        let (_, surface) = flat_surface(0.2);
284        assert_eq!(surface.max_swap_tenor(), Period::new(100, TimeUnit::Years));
285        assert_eq!(surface.max_swap_length().unwrap(), 100.0);
286    }
287
288    #[test]
289    fn shifted_lognormal_reports_its_shift() {
290        let reference = Date::new(15, Month::June, 2026);
291        let surface = ConstantSwaptionVolatility::new(
292            reference,
293            Target::new(),
294            BusinessDayConvention::Following,
295            0.2,
296            Actual360::new(),
297            VolatilityType::ShiftedLognormal,
298            0.01,
299        );
300        assert_eq!(surface.volatility_type(), VolatilityType::ShiftedLognormal);
301        assert_eq!(surface.shift(reference + 90, 5.0, false).unwrap(), 0.01);
302    }
303
304    #[test]
305    fn normal_surface_rejects_a_shift_query() {
306        let reference = Date::new(15, Month::June, 2026);
307        let surface = ConstantSwaptionVolatility::new(
308            reference,
309            Target::new(),
310            BusinessDayConvention::Following,
311            0.2,
312            Actual360::new(),
313            VolatilityType::Normal,
314            0.0,
315        );
316        assert_eq!(surface.volatility_type(), VolatilityType::Normal);
317        assert!(surface.shift(reference + 90, 5.0, false).is_err());
318    }
319
320    #[test]
321    fn defaults_report_shifted_lognormal_without_shift() {
322        let (reference, surface) = flat_surface(0.2);
323        assert_eq!(surface.volatility_type(), VolatilityType::ShiftedLognormal);
324        assert_eq!(surface.shift(reference + 90, 5.0, false).unwrap(), 0.0);
325    }
326
327    #[test]
328    fn engine_facing_constructor_uses_null_calendar_settlement_zero() {
329        use crate::time::calendars::nullcalendar::NullCalendar;
330        let settings = shared(Settings::new());
331        settings.set_evaluation_date(Date::new(15, Month::January, 2026));
332        let surface = ConstantSwaptionVolatility::moving(
333            0,
334            NullCalendar::new(),
335            BusinessDayConvention::Following,
336            0.2,
337            Actual360::new(),
338            VolatilityType::ShiftedLognormal,
339            0.0,
340            settings.clone(),
341        );
342        assert_eq!(
343            surface.reference_date().unwrap(),
344            Date::new(15, Month::January, 2026)
345        );
346        let variance = surface
347            .black_variance(Date::new(15, Month::January, 2027), 5.0, 0.03, false)
348            .unwrap();
349        assert!(variance > 0.0);
350    }
351
352    #[test]
353    fn quote_changes_propagate_and_notify() {
354        let reference = Date::new(15, Month::June, 2026);
355        let handle = make_quote_handle(0.18);
356        let surface = ConstantSwaptionVolatility::with_quote(
357            reference,
358            Target::new(),
359            BusinessDayConvention::Following,
360            handle.handle(),
361            Actual360::new(),
362            VolatilityType::ShiftedLognormal,
363            0.0,
364        );
365        assert_eq!(
366            surface.volatility_time(1.0, 5.0, 0.03, false).unwrap(),
367            0.18
368        );
369
370        let flag = Flag::new();
371        surface.observable().register_observer(&as_observer(&flag));
372
373        let quote = shared(SimpleQuote::new(0.23));
374        handle.link_to(quote.clone() as Shared<dyn Quote>);
375        assert!(Flag::is_up(&flag));
376        assert_eq!(
377            surface.volatility_time(1.0, 5.0, 0.03, false).unwrap(),
378            0.23
379        );
380    }
381
382    #[test]
383    fn moving_reference_date_follows_the_evaluation_date() {
384        let settings = shared(Settings::new());
385        settings.set_evaluation_date(Date::new(15, Month::January, 2026));
386        let surface = ConstantSwaptionVolatility::moving(
387            2,
388            Target::new(),
389            BusinessDayConvention::Following,
390            0.2,
391            Actual360::new(),
392            VolatilityType::ShiftedLognormal,
393            0.0,
394            settings.clone(),
395        );
396        assert_eq!(
397            surface.reference_date().unwrap(),
398            Date::new(19, Month::January, 2026)
399        );
400        assert_eq!(surface.volatility_time(1.0, 5.0, 0.03, false).unwrap(), 0.2);
401
402        settings.set_evaluation_date(Date::new(16, Month::January, 2026));
403        assert_eq!(
404            surface.reference_date().unwrap(),
405            Date::new(20, Month::January, 2026)
406        );
407    }
408}