Skip to main content

libitofin/termstructures/volatility/optionlet/
constantoptionletvol.rs

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