convex_math/extrapolation/
smith_wilson.rs1use super::Extrapolator;
7
8#[derive(Debug, Clone, Copy, PartialEq)]
55pub struct SmithWilson {
56 pub ultimate_forward_rate: f64,
58 pub convergence_speed: f64,
60 pub last_liquid_point: f64,
62}
63
64impl SmithWilson {
65 #[must_use]
77 pub fn new(ufr: f64, alpha: f64, llp: f64) -> Self {
78 assert!(alpha > 0.0, "Alpha must be positive");
79 assert!(llp > 0.0, "LLP must be positive");
80
81 Self {
82 ultimate_forward_rate: ufr,
83 convergence_speed: alpha,
84 last_liquid_point: llp,
85 }
86 }
87
88 #[must_use]
95 pub fn eiopa_eur() -> Self {
96 Self::new(0.0345, 0.126, 20.0)
97 }
98
99 #[must_use]
106 pub fn eiopa_gbp() -> Self {
107 Self::new(0.0345, 0.100, 50.0)
108 }
109
110 #[must_use]
117 pub fn eiopa_usd() -> Self {
118 Self::new(0.0345, 0.100, 30.0)
119 }
120
121 #[must_use]
128 pub fn eiopa_chf() -> Self {
129 Self::new(0.0345, 0.100, 25.0)
130 }
131
132 #[must_use]
134 pub fn ufr(&self) -> f64 {
135 self.ultimate_forward_rate
136 }
137
138 #[must_use]
140 pub fn alpha(&self) -> f64 {
141 self.convergence_speed
142 }
143
144 #[must_use]
146 pub fn llp(&self) -> f64 {
147 self.last_liquid_point
148 }
149
150 #[inline]
157 #[allow(dead_code)]
158 pub(crate) fn kernel(&self, t: f64, u: f64) -> f64 {
159 let alpha = self.convergence_speed;
160 let min_tu = t.min(u);
161
162 let term1 = (-alpha * (t + u)).exp();
164 let term2 = (alpha * min_tu).exp() - (-alpha * min_tu).exp();
165
166 alpha * min_tu - 0.5 * term1 * term2
167 }
168
169 #[inline]
174 #[allow(dead_code)]
175 pub(crate) fn convergence_weight(&self, t: f64) -> f64 {
176 if t <= self.last_liquid_point {
177 return 0.0;
178 }
179
180 let tau = t - self.last_liquid_point;
181 let alpha = self.convergence_speed;
182
183 1.0 - (-alpha * tau).exp()
186 }
187}
188
189impl Extrapolator for SmithWilson {
190 fn extrapolate(&self, t: f64, last_t: f64, last_value: f64, _last_derivative: f64) -> f64 {
191 if t <= last_t {
192 return last_value;
193 }
194
195 let alpha = self.convergence_speed;
199 let tau = t - last_t;
200
201 let convergence = 1.0 - (-alpha * tau).exp();
204
205 let ufr_implied = (last_value * last_t + self.ultimate_forward_rate * tau) / t;
212
213 last_value + convergence * (ufr_implied - last_value)
217 }
218
219 fn name(&self) -> &'static str {
220 "Smith-Wilson"
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227 use approx::assert_relative_eq;
228
229 #[test]
230 fn test_smith_wilson_creation() {
231 let sw = SmithWilson::new(0.042, 0.1, 20.0);
232 assert_relative_eq!(sw.ufr(), 0.042, epsilon = 1e-10);
233 assert_relative_eq!(sw.alpha(), 0.1, epsilon = 1e-10);
234 assert_relative_eq!(sw.llp(), 20.0, epsilon = 1e-10);
235 }
236
237 #[test]
238 fn test_eiopa_eur_parameters() {
239 let sw = SmithWilson::eiopa_eur();
240 assert_relative_eq!(sw.ufr(), 0.0345, epsilon = 1e-10);
241 assert_relative_eq!(sw.alpha(), 0.126, epsilon = 1e-10);
242 assert_relative_eq!(sw.llp(), 20.0, epsilon = 1e-10);
243 }
244
245 #[test]
246 fn test_eiopa_gbp_parameters() {
247 let sw = SmithWilson::eiopa_gbp();
248 assert_relative_eq!(sw.ufr(), 0.0345, epsilon = 1e-10);
249 assert_relative_eq!(sw.alpha(), 0.100, epsilon = 1e-10);
250 assert_relative_eq!(sw.llp(), 50.0, epsilon = 1e-10);
251 }
252
253 #[test]
254 fn test_eiopa_usd_parameters() {
255 let sw = SmithWilson::eiopa_usd();
256 assert_relative_eq!(sw.ufr(), 0.0345, epsilon = 1e-10);
257 assert_relative_eq!(sw.alpha(), 0.100, epsilon = 1e-10);
258 assert_relative_eq!(sw.llp(), 30.0, epsilon = 1e-10);
259 }
260
261 #[test]
262 fn test_smith_wilson_at_llp() {
263 let sw = SmithWilson::new(0.042, 0.1, 20.0);
264
265 let last_t = 20.0;
266 let last_value = 0.035;
267 let last_deriv = 0.001;
268
269 let value = sw.extrapolate(last_t, last_t, last_value, last_deriv);
271 assert_relative_eq!(value, last_value, epsilon = 1e-10);
272 }
273
274 #[test]
275 fn test_smith_wilson_convergence_towards_ufr() {
276 let ufr = 0.042;
277 let sw = SmithWilson::new(ufr, 0.1, 20.0);
278
279 let last_t = 20.0;
280 let last_value = 0.035; let last_deriv = 0.001;
282
283 let value_30 = sw.extrapolate(30.0, last_t, last_value, last_deriv);
285 let value_60 = sw.extrapolate(60.0, last_t, last_value, last_deriv);
286 let value_100 = sw.extrapolate(100.0, last_t, last_value, last_deriv);
287 let value_150 = sw.extrapolate(150.0, last_t, last_value, last_deriv);
288
289 assert!(value_30 > last_value, "30Y should be above LLP value");
291 assert!(value_60 > value_30, "60Y should be above 30Y");
292 assert!(value_100 > value_60, "100Y should be above 60Y");
293
294 assert!(
296 (value_150 - ufr).abs() < 0.005,
297 "150Y should be within 50bp of UFR"
298 );
299 }
300
301 #[test]
302 fn test_smith_wilson_convergence_from_above() {
303 let ufr = 0.03;
304 let sw = SmithWilson::new(ufr, 0.1, 20.0);
305
306 let last_t = 20.0;
307 let last_value = 0.045; let last_deriv = -0.001;
309
310 let value_30 = sw.extrapolate(30.0, last_t, last_value, last_deriv);
312 let value_60 = sw.extrapolate(60.0, last_t, last_value, last_deriv);
313 let value_100 = sw.extrapolate(100.0, last_t, last_value, last_deriv);
314
315 assert!(value_30 < last_value, "30Y should be below LLP value");
317 assert!(value_60 < value_30, "60Y should be below 30Y");
318 assert!(value_100 < value_60, "100Y should be below 60Y");
319
320 assert!((value_100 - ufr).abs() < (last_value - ufr).abs());
322 }
323
324 #[test]
325 fn test_smith_wilson_higher_alpha_faster_convergence() {
326 let ufr = 0.042;
327 let sw_slow = SmithWilson::new(ufr, 0.05, 20.0);
328 let sw_fast = SmithWilson::new(ufr, 0.20, 20.0);
329
330 let last_t = 20.0;
331 let last_value = 0.03;
332 let last_deriv = 0.001;
333
334 let slow_40 = sw_slow.extrapolate(40.0, last_t, last_value, last_deriv);
336 let fast_40 = sw_fast.extrapolate(40.0, last_t, last_value, last_deriv);
337
338 assert!(
339 (fast_40 - ufr).abs() < (slow_40 - ufr).abs(),
340 "Higher alpha should converge faster: slow_40={}, fast_40={}, ufr={}",
341 slow_40,
342 fast_40,
343 ufr
344 );
345 }
346
347 #[test]
348 fn test_smith_wilson_name() {
349 let sw = SmithWilson::new(0.042, 0.1, 20.0);
350 assert_eq!(sw.name(), "Smith-Wilson");
351 }
352
353 #[test]
354 fn test_smith_wilson_eiopa_convergence_criterion() {
355 let sw = SmithWilson::eiopa_eur();
357
358 let last_t = 20.0;
359 let last_value = 0.030; let last_deriv = 0.0;
361
362 let value_60 = sw.extrapolate(60.0, last_t, last_value, last_deriv);
364
365 let distance_to_ufr = (value_60 - sw.ufr()).abs();
369
370 let initial_distance = (last_value - sw.ufr()).abs();
372 assert!(
373 distance_to_ufr < initial_distance * 0.5,
374 "At LLP+40Y, should be at least 50% closer to UFR"
375 );
376 }
377
378 #[test]
379 #[should_panic(expected = "Alpha must be positive")]
380 fn test_smith_wilson_invalid_alpha() {
381 let _ = SmithWilson::new(0.042, 0.0, 20.0);
382 }
383
384 #[test]
385 #[should_panic(expected = "LLP must be positive")]
386 fn test_smith_wilson_invalid_llp() {
387 let _ = SmithWilson::new(0.042, 0.1, 0.0);
388 }
389
390 #[test]
391 fn test_kernel_function() {
392 let sw = SmithWilson::new(0.042, 0.1, 20.0);
393
394 let h_10_20 = sw.kernel(10.0, 20.0);
396 let h_20_10 = sw.kernel(20.0, 10.0);
397 assert_relative_eq!(h_10_20, h_20_10, epsilon = 1e-10);
398
399 let h_10_10 = sw.kernel(10.0, 10.0);
401 assert!(h_10_10 > 0.0);
402 }
403
404 #[test]
405 fn test_convergence_weight() {
406 let sw = SmithWilson::new(0.042, 0.1, 20.0);
407
408 let w_llp = sw.convergence_weight(20.0);
410 assert_relative_eq!(w_llp, 0.0, epsilon = 1e-10);
411
412 let w_30 = sw.convergence_weight(30.0);
414 let w_50 = sw.convergence_weight(50.0);
415 let w_100 = sw.convergence_weight(100.0);
416
417 assert!(w_30 > 0.0);
418 assert!(w_50 > w_30);
419 assert!(w_100 > w_50);
420
421 let w_500 = sw.convergence_weight(500.0);
423 assert!(w_500 > 0.99);
424 }
425}