1use crate::errors::QlResult;
17use crate::handle::Handle;
18use crate::interestrate::{Compounding, InterestRate};
19use crate::patterns::observable::{AsObservable, Observable, Observer, ResetThenNotify};
20use crate::quotes::{Quote, SimpleQuote};
21use crate::settings::Settings;
22use crate::shared::{Shared, SharedMut, shared, shared_mut};
23use crate::termstructures::yieldtermstructure::YieldTermStructure;
24use crate::termstructures::{TermStructure, TermStructureBase};
25use crate::time::calendar::Calendar;
26use crate::time::date::Date;
27use crate::time::daycounter::DayCounter;
28use crate::time::frequency::Frequency;
29use crate::types::{DiscountFactor, Natural, Rate, Time};
30
31pub struct FlatForward {
33 base: TermStructureBase,
34 forward: Handle<dyn Quote>,
35 compounding: Compounding,
36 frequency: Frequency,
37 rate: SharedMut<Option<InterestRate>>,
38 _listener: SharedMut<ResetThenNotify>,
39}
40
41impl FlatForward {
42 fn assemble(
43 base: TermStructureBase,
44 forward: Handle<dyn Quote>,
45 compounding: Compounding,
46 frequency: Frequency,
47 ) -> FlatForward {
48 let rate = shared_mut(None);
49 let listener = ResetThenNotify::delivering(base.updater(), {
50 let rate = SharedMut::clone(&rate);
51 move || {
52 rate.borrow_mut().take();
53 }
54 });
55 forward.register_observer(&(listener.clone() as SharedMut<dyn Observer>));
56 FlatForward {
57 base,
58 forward,
59 compounding,
60 frequency,
61 rate,
62 _listener: listener,
63 }
64 }
65
66 fn wrap(value: Rate) -> Handle<dyn Quote> {
67 Handle::new(shared(SimpleQuote::new(value)) as Shared<dyn Quote>)
68 }
69
70 pub fn new(
72 reference_date: Date,
73 forward: Handle<dyn Quote>,
74 day_counter: DayCounter,
75 compounding: Compounding,
76 frequency: Frequency,
77 ) -> FlatForward {
78 let base = TermStructureBase::with_reference_date(reference_date, None, Some(day_counter));
79 Self::assemble(base, forward, compounding, frequency)
80 }
81
82 pub fn with_rate(
84 reference_date: Date,
85 forward: Rate,
86 day_counter: DayCounter,
87 compounding: Compounding,
88 frequency: Frequency,
89 ) -> FlatForward {
90 Self::new(
91 reference_date,
92 Self::wrap(forward),
93 day_counter,
94 compounding,
95 frequency,
96 )
97 }
98
99 pub fn moving(
101 settlement_days: Natural,
102 calendar: Calendar,
103 forward: Handle<dyn Quote>,
104 day_counter: DayCounter,
105 compounding: Compounding,
106 frequency: Frequency,
107 settings: Shared<Settings<Date>>,
108 ) -> FlatForward {
109 let base =
110 TermStructureBase::moving(settlement_days, calendar, Some(day_counter), settings);
111 Self::assemble(base, forward, compounding, frequency)
112 }
113
114 pub fn moving_with_rate(
116 settlement_days: Natural,
117 calendar: Calendar,
118 forward: Rate,
119 day_counter: DayCounter,
120 compounding: Compounding,
121 frequency: Frequency,
122 settings: Shared<Settings<Date>>,
123 ) -> FlatForward {
124 Self::moving(
125 settlement_days,
126 calendar,
127 Self::wrap(forward),
128 day_counter,
129 compounding,
130 frequency,
131 settings,
132 )
133 }
134
135 pub fn compounding(&self) -> Compounding {
137 self.compounding
138 }
139
140 pub fn compounding_frequency(&self) -> Frequency {
142 self.frequency
143 }
144
145 fn flat_rate(&self) -> QlResult<InterestRate> {
146 if let Some(rate) = self.rate.borrow().clone() {
147 return Ok(rate);
148 }
149 let value = self.forward.current_link()?.value()?;
150 let day_counter = self
151 .base
152 .day_counter()
153 .expect("a flat forward curve is constructed with a day counter");
154 let rate = InterestRate::new(value, day_counter, self.compounding, self.frequency)?;
155 *self.rate.borrow_mut() = Some(rate.clone());
156 Ok(rate)
157 }
158}
159
160impl AsObservable for FlatForward {
161 fn observable(&self) -> &Observable {
162 self.base.observable()
163 }
164}
165
166impl TermStructure for FlatForward {
167 fn base(&self) -> &TermStructureBase {
168 &self.base
169 }
170
171 fn max_date(&self) -> Date {
172 Date::max_date()
173 }
174}
175
176impl YieldTermStructure for FlatForward {
177 fn discount_impl(&self, t: Time) -> QlResult<DiscountFactor> {
178 self.flat_rate()?.discount_factor(t)
179 }
180}
181#[cfg(test)]
182mod tests {
183 use super::*;
184 use crate::shared::shared;
185 use crate::test_support::{Flag, as_observer};
186 use crate::time::calendars::target::Target;
187 use crate::time::date::Month;
188 use crate::time::daycounters::actual360::Actual360;
189
190 fn today() -> Date {
191 Date::new(17, Month::May, 1998)
192 }
193
194 #[test]
195 fn flat_curve_reproduces_the_continuous_discounts_european_options_use() {
196 let q = FlatForward::with_rate(
197 today(),
198 0.04,
199 Actual360::new(),
200 Compounding::Continuous,
201 Frequency::Annual,
202 );
203 let r = FlatForward::with_rate(
204 today(),
205 0.06,
206 Actual360::new(),
207 Compounding::Continuous,
208 Frequency::Annual,
209 );
210 for days in [90, 180, 360, 720] {
211 let t = Time::from(days) / 360.0;
212 let df_q = q.discount_date(today() + days, false).unwrap();
213 let df_r = r.discount_date(today() + days, false).unwrap();
214 assert!((df_q - (-0.04 * t).exp()).abs() < 1.0e-15);
215 assert!((df_r - (-0.06 * t).exp()).abs() < 1.0e-15);
216 }
217 assert_eq!(q.discount(0.0, false).unwrap(), 1.0);
218 }
219
220 #[test]
221 fn zero_and_forward_rates_are_flat_at_the_quoted_rate() {
222 let curve = FlatForward::with_rate(
223 today(),
224 0.06,
225 Actual360::new(),
226 Compounding::Continuous,
227 Frequency::Annual,
228 );
229 for t in [0.25, 1.0, 7.5] {
230 let zero = curve
231 .zero_rate(t, Compounding::Continuous, Frequency::Annual, false)
232 .unwrap();
233 assert!((zero.rate() - 0.06).abs() < 1.0e-12);
234 }
235 let forward = curve
236 .forward_rate(0.5, 2.5, Compounding::Continuous, Frequency::Annual, false)
237 .unwrap();
238 assert!((forward.rate() - 0.06).abs() < 1.0e-12);
239 let instantaneous = curve
240 .forward_rate(1.0, 1.0, Compounding::Continuous, Frequency::Annual, false)
241 .unwrap();
242 assert!((instantaneous.rate() - 0.06).abs() < 1.0e-9);
243 }
244
245 #[test]
246 fn compounded_quotes_discount_with_their_own_convention() {
247 let curve = FlatForward::with_rate(
248 today(),
249 0.06,
250 Actual360::new(),
251 Compounding::Compounded,
252 Frequency::Semiannual,
253 );
254 assert_eq!(curve.compounding(), Compounding::Compounded);
255 assert_eq!(curve.compounding_frequency(), Frequency::Semiannual);
256 let df = curve.discount(1.0, false).unwrap();
257 assert!((df - 1.0 / (1.0_f64 + 0.06 / 2.0).powi(2)).abs() < 1.0e-15);
258 }
259
260 #[test]
261 fn quote_change_notifies_observers_and_refreshes_the_rate() {
262 let quote = shared(SimpleQuote::new(0.05));
263 let curve = FlatForward::new(
264 today(),
265 Handle::new(quote.clone() as Shared<dyn Quote>),
266 Actual360::new(),
267 Compounding::Continuous,
268 Frequency::Annual,
269 );
270 assert!((curve.discount(2.0, false).unwrap() - (-0.10_f64).exp()).abs() < 1.0e-15);
271
272 let flag = Flag::new();
273 curve.observable().register_observer(&as_observer(&flag));
274 quote.set_value(0.07);
275
276 assert!(
277 Flag::is_up(&flag),
278 "quote change must reach curve observers"
279 );
280 assert!((curve.discount(2.0, false).unwrap() - (-0.14_f64).exp()).abs() < 1.0e-15);
281 }
282
283 #[test]
284 fn observers_reading_during_the_notification_see_the_fresh_rate() {
285 struct Reader {
286 curve: Shared<FlatForward>,
287 seen: SharedMut<Option<DiscountFactor>>,
288 }
289 impl Observer for Reader {
290 fn update(&mut self) {
291 *self.seen.borrow_mut() = Some(self.curve.discount(1.0, false).unwrap());
292 }
293 }
294
295 let quote = shared(SimpleQuote::new(0.05));
296 let curve = shared(FlatForward::new(
297 today(),
298 Handle::new(quote.clone() as Shared<dyn Quote>),
299 Actual360::new(),
300 Compounding::Continuous,
301 Frequency::Annual,
302 ));
303 curve.discount(1.0, false).unwrap();
304
305 let seen = shared_mut(None);
306 let reader = shared_mut(Reader {
307 curve: curve.clone(),
308 seen: SharedMut::clone(&seen),
309 });
310 curve
311 .observable()
312 .register_observer(&(reader.clone() as SharedMut<dyn Observer>));
313
314 quote.set_value(0.07);
315
316 let seen = seen.borrow().expect("reader must have been notified");
317 assert!(
318 (seen - (-0.07_f64).exp()).abs() < 1.0e-15,
319 "mid-notification read returned a stale discount ({seen})"
320 );
321 }
322
323 #[test]
324 fn relinking_the_handle_switches_the_curve_to_the_new_quote() {
325 let relinkable = crate::quotes::make_quote_handle(0.05);
326 let curve = FlatForward::new(
327 today(),
328 relinkable.handle(),
329 Actual360::new(),
330 Compounding::Continuous,
331 Frequency::Annual,
332 );
333 assert!((curve.discount(1.0, false).unwrap() - (-0.05_f64).exp()).abs() < 1.0e-15);
334
335 let flag = Flag::new();
336 curve.observable().register_observer(&as_observer(&flag));
337 relinkable.link_to(shared(SimpleQuote::new(0.08)));
338
339 assert!(Flag::is_up(&flag), "relink must reach curve observers");
340 assert!((curve.discount(1.0, false).unwrap() - (-0.08_f64).exp()).abs() < 1.0e-15);
341 }
342
343 #[test]
344 fn moving_curve_follows_the_evaluation_date() {
345 let settings = shared(Settings::new());
346 settings.set_evaluation_date(Date::new(15, Month::January, 2026));
347 let curve = FlatForward::moving_with_rate(
348 2,
349 Target::new(),
350 0.05,
351 Actual360::new(),
352 Compounding::Continuous,
353 Frequency::Annual,
354 settings.clone(),
355 );
356 assert_eq!(
357 curve.reference_date().unwrap(),
358 Date::new(19, Month::January, 2026)
359 );
360
361 let flag = Flag::new();
362 curve.observable().register_observer(&as_observer(&flag));
363 settings.set_evaluation_date(Date::new(16, Month::January, 2026));
364
365 assert!(Flag::is_up(&flag));
366 assert_eq!(
367 curve.reference_date().unwrap(),
368 Date::new(20, Month::January, 2026)
369 );
370 let df = curve
371 .discount_date(Date::new(20, Month::January, 2027), false)
372 .unwrap();
373 assert!((df - (-0.05_f64 * 365.0 / 360.0).exp()).abs() < 1.0e-15);
374 }
375
376 #[test]
377 fn quote_write_back_during_an_evaluation_date_wave_defers_instead_of_panicking() {
378 struct WriteBack {
379 quote: Shared<SimpleQuote>,
380 armed: bool,
381 notifications: usize,
382 }
383 impl Observer for WriteBack {
384 fn update(&mut self) {
385 self.notifications += 1;
386 if self.armed {
387 self.armed = false;
388 self.quote.set_value(0.09);
389 }
390 }
391 }
392
393 let settings = shared(Settings::new());
394 settings.set_evaluation_date(Date::new(15, Month::January, 2026));
395 let quote = shared(SimpleQuote::new(0.05));
396 let curve = FlatForward::moving(
397 2,
398 Target::new(),
399 Handle::new(quote.clone() as Shared<dyn Quote>),
400 Actual360::new(),
401 Compounding::Continuous,
402 Frequency::Annual,
403 settings.clone(),
404 );
405 curve.discount(1.0, false).unwrap();
406
407 let writer = shared_mut(WriteBack {
408 quote: quote.clone(),
409 armed: true,
410 notifications: 0,
411 });
412 curve
413 .observable()
414 .register_observer(&(writer.clone() as SharedMut<dyn Observer>));
415
416 settings.set_evaluation_date(Date::new(16, Month::January, 2026));
417
418 assert_eq!(
419 writer.borrow().notifications,
420 2,
421 "the write-back wave must re-notify curve observers exactly once more"
422 );
423 assert!((curve.discount(1.0, false).unwrap() - (-0.09_f64).exp()).abs() < 1.0e-15);
424 }
425
426 #[test]
427 fn empty_or_invalid_quotes_error_instead_of_pricing() {
428 let curve = FlatForward::new(
429 today(),
430 Handle::empty(),
431 Actual360::new(),
432 Compounding::Continuous,
433 Frequency::Annual,
434 );
435 assert!(curve.discount(1.0, false).is_err());
436
437 let unset = shared(SimpleQuote::default());
438 let curve = FlatForward::new(
439 today(),
440 Handle::new(unset as Shared<dyn Quote>),
441 Actual360::new(),
442 Compounding::Continuous,
443 Frequency::Annual,
444 );
445 assert!(curve.discount(1.0, false).is_err());
446 }
447}