1use 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
34pub 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 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 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 #[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 #[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}