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