libitofin/termstructures/volatility/
localvolcurve.rs1use crate::errors::QlResult;
22use crate::handle::Handle;
23use crate::math::interpolations::Interpolator;
24use crate::math::interpolations::linear::Linear;
25use crate::patterns::observable::{AsObservable, Observable};
26use crate::termstructures::volatility::{
27 BlackVarianceCurve, BlackVolTermStructure, VolatilityTermStructure,
28};
29use crate::termstructures::{TermStructure, TermStructureBase};
30use crate::time::businessdayconvention::BusinessDayConvention;
31use crate::time::calendar::Calendar;
32use crate::time::date::Date;
33use crate::time::daycounter::DayCounter;
34use crate::types::{Rate, Real, Time, Volatility};
35
36use super::LocalVolTermStructure;
37
38pub struct LocalVolCurve<I: Interpolator + 'static = Linear> {
40 base: TermStructureBase,
41 curve: Handle<BlackVarianceCurve<I>>,
42}
43
44impl<I: Interpolator + 'static> LocalVolCurve<I> {
45 pub fn new(curve: Handle<BlackVarianceCurve<I>>) -> LocalVolCurve<I> {
48 let base = TermStructureBase::new(None);
49 curve.register_observer(&base.updater());
50 LocalVolCurve { base, curve }
51 }
52}
53
54impl<I: Interpolator + 'static> AsObservable for LocalVolCurve<I> {
55 fn observable(&self) -> &Observable {
56 self.base.observable()
57 }
58}
59
60impl<I: Interpolator + 'static> TermStructure for LocalVolCurve<I> {
61 fn base(&self) -> &TermStructureBase {
62 &self.base
63 }
64
65 fn reference_date(&self) -> QlResult<Date> {
66 self.curve.current_link()?.reference_date()
67 }
68
69 fn calendar(&self) -> Option<Calendar> {
70 self.curve.current_link().ok().and_then(|c| c.calendar())
71 }
72
73 fn day_counter(&self) -> Option<DayCounter> {
74 self.curve.current_link().ok().and_then(|c| c.day_counter())
75 }
76
77 fn max_date(&self) -> Date {
78 self.curve
79 .current_link()
80 .map(|c| c.max_date())
81 .unwrap_or_else(|_| Date::null())
82 }
83}
84
85impl<I: Interpolator + 'static> VolatilityTermStructure for LocalVolCurve<I> {
86 fn business_day_convention(&self) -> BusinessDayConvention {
87 self.curve
88 .current_link()
89 .map(|c| c.business_day_convention())
90 .unwrap_or(BusinessDayConvention::Following)
91 }
92
93 fn min_strike(&self) -> Rate {
94 Rate::MIN
95 }
96
97 fn max_strike(&self) -> Rate {
98 Rate::MAX
99 }
100}
101
102impl<I: Interpolator + 'static> LocalVolTermStructure for LocalVolCurve<I> {
103 fn local_vol_impl(&self, t: Time, strike: Real) -> QlResult<Volatility> {
104 let curve = self.curve.current_link()?;
105 let dt = 1.0 / 365.0;
106 let var1 = curve.black_variance(t, strike, true)?;
107 let var2 = curve.black_variance(t + dt, strike, true)?;
108 let derivative = (var2 - var1) / dt;
109 Ok(derivative.sqrt())
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use crate::handle::RelinkableHandle;
117 use crate::shared::shared;
118 use crate::test_support::{Flag, as_observer};
119 use crate::time::date::Month;
120 use crate::time::daycounters::actual365fixed::Actual365Fixed;
121
122 fn variance_curve() -> BlackVarianceCurve {
123 let reference = Date::new(15, Month::June, 2026);
124 BlackVarianceCurve::new(
125 reference,
126 &[reference + 365, reference + 730],
127 &[0.2, 0.25],
128 Actual365Fixed::new(),
129 true,
130 )
131 .unwrap()
132 }
133
134 fn local_curve() -> LocalVolCurve {
135 LocalVolCurve::new(Handle::new(shared(variance_curve())))
136 }
137
138 #[test]
139 fn local_vol_is_the_square_root_of_the_variance_slope() {
140 let local = local_curve();
141 assert!((local.local_vol(0.5, 100.0, false).unwrap() - 0.2).abs() < 1.0e-12);
144 let slope = 0.085_f64;
145 assert!((local.local_vol(1.5, 100.0, false).unwrap() - slope.sqrt()).abs() < 1.0e-12);
146 assert!((local.local_vol(1.0, 100.0, false).unwrap() - slope.sqrt()).abs() < 1.0e-12);
148 }
149
150 #[test]
151 fn beyond_the_last_node_flat_vol_extrapolation_gives_the_last_vol() {
152 let local = local_curve();
153 assert!((local.local_vol(2.0, 100.0, false).unwrap() - 0.25).abs() < 1.0e-12);
156 }
157
158 #[test]
159 fn local_vol_matches_the_one_day_forward_vol_of_the_underlying() {
160 let local = local_curve();
161 let underlying = variance_curve();
162 for t in [0.0, 0.3, 1.0, 1.7] {
165 let expected = underlying
166 .black_forward_vol(t, t + 1.0 / 365.0, 100.0, true)
167 .unwrap();
168 assert!((local.local_vol(t, 100.0, false).unwrap() - expected).abs() < 1.0e-10);
169 }
170 }
171
172 #[test]
173 fn inspectors_delegate_to_the_underlying_curve() {
174 let local = local_curve();
175 let underlying = variance_curve();
176 assert_eq!(
177 local.reference_date().unwrap(),
178 underlying.reference_date().unwrap()
179 );
180 assert_eq!(local.max_date(), underlying.max_date());
181 assert_eq!(
182 local.day_counter().unwrap().name(),
183 underlying.day_counter().unwrap().name()
184 );
185 assert_eq!(
186 local.business_day_convention(),
187 BusinessDayConvention::Following
188 );
189 assert_eq!(local.min_strike(), Rate::MIN);
190 assert_eq!(local.max_strike(), Rate::MAX);
191 }
192
193 #[test]
194 fn empty_handle_errors_instead_of_dereferencing_null() {
195 let local: LocalVolCurve = LocalVolCurve::new(Handle::empty());
196 assert!(local.reference_date().is_err());
197 assert!(local.day_counter().is_none());
198 assert_eq!(local.max_date(), Date::null());
199 assert!(local.local_vol(1.0, 100.0, true).is_err());
200 }
201
202 #[test]
203 fn relinking_the_underlying_notifies_observers() {
204 let relinkable = RelinkableHandle::new(shared(variance_curve()));
205 let local = LocalVolCurve::new(relinkable.handle());
206 let flag = Flag::new();
207 local.observable().register_observer(&as_observer(&flag));
208
209 let reference = Date::new(15, Month::June, 2026);
210 let steeper = BlackVarianceCurve::new(
211 reference,
212 &[reference + 365, reference + 730],
213 &[0.3, 0.35],
214 Actual365Fixed::new(),
215 true,
216 )
217 .unwrap();
218 relinkable.link_to(shared(steeper));
219
220 assert!(Flag::is_up(&flag));
221 assert!((local.local_vol(0.5, 100.0, false).unwrap() - 0.3).abs() < 1.0e-12);
222 }
223}