1use crate::dual::{set_order_clone, ADOrder, Dual, Dual2, Number, NumberArray2};
5use crate::json::JSON;
6use chrono::prelude::*;
7use indexmap::set::IndexSet;
8use itertools::Itertools;
9use ndarray::{Array2, ArrayViewMut2, Axis};
10use num_traits::{One, Zero};
11use pyo3::exceptions::PyValueError;
12use pyo3::{pyclass, PyErr};
13use serde::{Deserialize, Serialize};
14use std::collections::HashSet;
15use std::ops::{Div, Mul};
16
17pub(crate) mod ccy;
18pub use crate::fx::rates::ccy::Ccy;
19
20pub(crate) mod fxpair;
21pub use crate::fx::rates::fxpair::FXPair;
22
23pub(crate) mod fxrate;
24pub use crate::fx::rates::fxrate::FXRate;
25
26#[pyclass(module = "rateslib.rs")]
28#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
29#[serde(from = "FXRatesDataModel")]
30pub struct FXRates {
31 pub(crate) fx_rates: Vec<FXRate>,
32 pub(crate) currencies: IndexSet<Ccy>,
33 #[serde(skip)]
34 pub(crate) fx_array: NumberArray2,
35}
36
37#[derive(Deserialize)]
38struct FXRatesDataModel {
39 fx_rates: Vec<FXRate>,
40 currencies: IndexSet<Ccy>,
41}
42
43impl std::convert::From<FXRatesDataModel> for FXRates {
44 fn from(model: FXRatesDataModel) -> Self {
45 let base = model.currencies.first().unwrap();
46 Self::try_new(model.fx_rates, Some(*base)).expect("FXRates data model contains bad data.")
47 }
48}
49
50impl FXRates {
51 pub fn try_new(fx_rates: Vec<FXRate>, base: Option<Ccy>) -> Result<Self, PyErr> {
52 if fx_rates.is_empty() {
60 return Err(PyValueError::new_err(
61 "`fx_rates` must contain at least on fx rate.",
62 ));
63 }
64
65 let mut currencies: IndexSet<Ccy> = IndexSet::with_capacity(fx_rates.len() + 1_usize);
66 if let Some(ccy) = base {
67 currencies.insert(ccy);
68 }
69 for fxr in fx_rates.iter() {
70 currencies.insert(fxr.pair.0);
71 currencies.insert(fxr.pair.1);
72 }
73 let q = currencies.len();
74
75 if q > (fx_rates.len() + 1) {
77 return Err(PyValueError::new_err(
78 "FX Array cannot be solved. `fx_rates` is underspecified.",
79 ));
80 } else if q < (fx_rates.len() + 1) {
81 return Err(PyValueError::new_err(
82 "FX Array cannot be solved. `fx_rates` is overspecified.",
83 ));
84 }
85
86 let settlement: Option<NaiveDateTime> = fx_rates[0].settlement;
88 match settlement {
89 Some(date) => {
90 if !(&fx_rates
91 .iter()
92 .all(|d| d.settlement.map_or(false, |v| v == date)))
93 {
94 return Err(PyValueError::new_err(
95 "`fx_rates` must have consistent `settlement` dates across all rates.",
96 ));
97 }
98 }
99 None => {
100 if !(&fx_rates
101 .iter()
102 .all(|d| d.settlement.map_or(true, |_v| false)))
103 {
104 return Err(PyValueError::new_err(
105 "`fx_rates` must have consistent `settlement` dates across all rates.",
106 ));
107 }
108 }
109 }
110
111 let fx_array = create_fx_array(¤cies, &fx_rates, ADOrder::One)?;
112 Ok(FXRates {
113 fx_rates,
114 fx_array,
115 currencies,
116 })
117 }
118
119 pub fn get_ccy_index(&self, currency: &Ccy) -> Option<usize> {
120 self.currencies.get_index_of(currency)
121 }
122
123 pub fn rate(&self, lhs: &Ccy, rhs: &Ccy) -> Option<Number> {
124 let dom_idx = self.currencies.get_index_of(lhs)?;
125 let for_idx = self.currencies.get_index_of(rhs)?;
126 match &self.fx_array {
127 NumberArray2::F64(arr) => Some(Number::F64(arr[[dom_idx, for_idx]])),
128 NumberArray2::Dual(arr) => Some(Number::Dual(arr[[dom_idx, for_idx]].clone())),
129 NumberArray2::Dual2(arr) => Some(Number::Dual2(arr[[dom_idx, for_idx]].clone())),
130 }
131 }
132
133 pub fn update(&mut self, fx_rates: Vec<FXRate>) -> Result<(), PyErr> {
134 if !(fx_rates
136 .iter()
137 .all(|v| self.fx_rates.iter().any(|x| x.pair == v.pair)))
138 {
139 return Err(PyValueError::new_err(
140 "The given `fx_rates` pairs are not contained in the `FXRates` object.",
141 ));
142 }
143 let mut fx_rates_: Vec<FXRate> = self.fx_rates.clone();
144 for fxr in fx_rates.into_iter() {
145 let idx = fx_rates_.iter().enumerate().fold(0_usize, |a, (i, v)| {
146 if fxr.pair.eq(&v.pair) {
147 i
148 } else {
149 a
150 }
151 });
152 fx_rates_[idx] = fxr;
153 }
154 let new_fxr = FXRates::try_new(fx_rates_, Some(self.currencies[0]))?;
155 self.fx_rates.clone_from(&new_fxr.fx_rates);
156 self.currencies.clone_from(&new_fxr.currencies);
157 self.fx_array = new_fxr.fx_array.clone();
158 Ok(())
159 }
160
161 pub fn set_ad_order(&mut self, ad: ADOrder) -> Result<(), PyErr> {
162 match (ad, &self.fx_array) {
163 (ADOrder::Zero, NumberArray2::F64(_))
164 | (ADOrder::One, NumberArray2::Dual(_))
165 | (ADOrder::Two, NumberArray2::Dual2(_)) => {
166 Ok(())
168 }
169 (ADOrder::One, NumberArray2::F64(_)) => {
170 let fx_array = create_fx_array(&self.currencies, &self.fx_rates, ADOrder::One)?;
172 self.fx_array = fx_array;
173 Ok(())
174 }
175 (ADOrder::Two, NumberArray2::F64(_)) => {
176 let fx_array = create_fx_array(&self.currencies, &self.fx_rates, ADOrder::Two)?;
178 self.fx_array = fx_array;
179 Ok(())
180 }
181 (ADOrder::One, NumberArray2::Dual2(arr)) => {
182 let n: usize = arr.len_of(Axis(0));
183 let fx_array = NumberArray2::Dual(
184 Array2::<Dual>::from_shape_vec(
185 (n, n),
186 arr.clone().into_iter().map(|d| d.into()).collect(),
187 )
188 .unwrap(),
189 );
190 self.fx_array = fx_array;
191 Ok(())
192 }
193 (ADOrder::Zero, NumberArray2::Dual(arr)) => {
194 let n: usize = arr.len_of(Axis(0));
196 let fx_array = NumberArray2::F64(
197 Array2::<f64>::from_shape_vec(
198 (n, n),
199 arr.clone().into_iter().map(|d| d.real).collect(),
200 )
201 .unwrap(),
202 );
203 self.fx_array = fx_array;
204 Ok(())
205 }
206 (ADOrder::Zero, NumberArray2::Dual2(arr)) => {
207 let n: usize = arr.len_of(Axis(0));
209 let fx_array = NumberArray2::F64(
210 Array2::<f64>::from_shape_vec(
211 (n, n),
212 arr.clone().into_iter().map(|d| d.real).collect(),
213 )
214 .unwrap(),
215 );
216 self.fx_array = fx_array;
217 Ok(())
218 }
219 (ADOrder::Two, NumberArray2::Dual(_)) => {
220 let fx_array = create_fx_array(&self.currencies, &self.fx_rates, ADOrder::Two)?;
222 self.fx_array = fx_array;
223 Ok(())
224 }
225 }
226 }
227}
228
229fn create_initial_edges(currencies: &IndexSet<Ccy>, fx_pairs: &[FXPair]) -> Array2<i16> {
232 let mut edges: Array2<i16> = Array2::eye(currencies.len());
233 for pair in fx_pairs.iter() {
234 let row = currencies.get_index_of(&pair.0).unwrap();
235 let col = currencies.get_index_of(&pair.1).unwrap();
236 edges[[row, col]] = 1_i16;
237 edges[[col, row]] = 1_i16;
238 }
239 edges
240}
241
242fn create_initial_fx_array<T>(
246 currencies: &IndexSet<Ccy>,
247 fx_pairs: &[FXPair],
248 fx_rates: &[T],
249) -> Array2<T>
250where
251 T: Clone + One + Zero,
252 for<'a> f64: Div<&'a T, Output = T>,
253{
254 assert_eq!(fx_pairs.len(), fx_rates.len());
255 let mut fx_array: Array2<T> = Array2::eye(currencies.len());
256
257 for (i, pair) in fx_pairs.iter().enumerate() {
258 let row = currencies.get_index_of(&pair.0).unwrap();
259 let col = currencies.get_index_of(&pair.1).unwrap();
260 fx_array[[row, col]] = fx_rates[i].clone();
261 fx_array[[col, row]] = 1_f64 / &fx_array[[row, col]];
262 }
263 fx_array
264}
265
266fn mut_arrays_remaining_elements<T>(
267 mut fx_array: ArrayViewMut2<T>,
268 mut edges: ArrayViewMut2<i16>,
269 mut prev_value: HashSet<usize>,
270) -> Result<bool, PyErr>
271where
272 for<'a> &'a T: Mul<&'a T, Output = T>,
273 for<'a> f64: Div<&'a T, Output = T>,
274{
275 if edges.sum() == ((edges.len_of(Axis(0)) * edges.len_of(Axis(1))) as i16) {
277 return Ok(true);
278 }
279
280 let available_edges_and_nodes: Vec<(i16, usize)> = edges
283 .sum_axis(Axis(1))
284 .into_iter()
285 .zip(0_usize..)
286 .filter(|(_v, i)| !prev_value.contains(i))
287 .into_iter()
288 .collect();
289 let sampled_node = available_edges_and_nodes
291 .into_iter()
292 .max_by_key(|(value, _)| *value)
293 .map(|(_, idx)| idx);
294
295 let node: usize;
296 match sampled_node {
297 None => {
298 return Err(PyValueError::new_err(
301 "FX Array cannot be solved. There are degenerate FX rate pairs.\n\
302 For example ('eurusd' + 'usdeur') or ('usdeur', 'eurjpy', 'usdjpy').",
303 ));
304 }
305 Some(node_) => node = node_,
306 }
307
308 let combinations: Vec<Vec<usize>> = edges
312 .row(node)
313 .iter()
314 .zip(0_usize..)
315 .filter(|(v, i)| **v == 1_i16 && *i != node)
316 .map(|(_v, i)| i)
317 .combinations(2)
318 .filter(|v| edges[[v[0], v[1]]] == 0_i16)
319 .collect();
320
321 let mut counter: i16 = 0;
324 for c in combinations {
325 counter += 1_i16;
326 edges[[c[0], c[1]]] = 1_i16;
327 edges[[c[1], c[0]]] = 1_i16;
328 fx_array[[c[0], c[1]]] = &fx_array[[c[0], node]] * &fx_array[[node, c[1]]];
329 fx_array[[c[1], c[0]]] = 1.0_f64 / &fx_array[[c[0], c[1]]];
330 }
331
332 if counter == 0 {
333 prev_value.insert(node);
336 return mut_arrays_remaining_elements(fx_array.view_mut(), edges.view_mut(), prev_value);
337 } else {
338 return mut_arrays_remaining_elements(
342 fx_array.view_mut(),
343 edges.view_mut(),
344 HashSet::from([node]),
345 );
346 }
347}
348
349fn create_fx_array(
351 currencies: &IndexSet<Ccy>,
352 fx_rates: &[FXRate],
353 ad: ADOrder,
354) -> Result<NumberArray2, PyErr> {
355 let fx_pairs: Vec<FXPair> = fx_rates.iter().map(|x| x.pair).collect();
356 let vars: Vec<String> = fx_pairs.iter().map(|x| format!("fx_{}", x)).collect();
357 let mut edges = create_initial_edges(currencies, &fx_pairs);
358 let fx_rates_: Vec<Number> = fx_rates
359 .iter()
360 .enumerate()
361 .map(|(i, x)| set_order_clone(&x.rate, ad, vec![vars[i].clone()]))
362 .collect();
363 match ad {
364 ADOrder::Zero => {
365 let fx_rates__: Vec<f64> = fx_rates_.iter().map(f64::from).collect();
366 let mut fx_array_: Array2<f64> =
367 create_initial_fx_array(currencies, &fx_pairs, &fx_rates__);
368 let _ = mut_arrays_remaining_elements(
369 fx_array_.view_mut(),
370 edges.view_mut(),
371 HashSet::new(),
372 )?;
373 Ok(NumberArray2::F64(fx_array_))
374 }
375 ADOrder::One => {
376 let fx_rates__: Vec<Dual> = fx_rates_.iter().map(Dual::from).collect();
377 let mut fx_array_: Array2<Dual> =
378 create_initial_fx_array(currencies, &fx_pairs, &fx_rates__);
379 let _ = mut_arrays_remaining_elements(
380 fx_array_.view_mut(),
381 edges.view_mut(),
382 HashSet::new(),
383 )?;
384 Ok(NumberArray2::Dual(fx_array_))
385 }
386 ADOrder::Two => {
387 let fx_rates__: Vec<Dual2> = fx_rates_.iter().map(Dual2::from).collect();
388 let mut fx_array_: Array2<Dual2> =
389 create_initial_fx_array(currencies, &fx_pairs, &fx_rates__);
390 let _ = mut_arrays_remaining_elements(
391 fx_array_.view_mut(),
392 edges.view_mut(),
393 HashSet::new(),
394 )?;
395 Ok(NumberArray2::Dual2(fx_array_))
396 }
397 }
398}
399
400impl JSON for FXRates {}
401
402#[cfg(test)]
403mod tests {
404 use super::*;
405 use crate::scheduling::ndt;
406 use ndarray::arr2;
407
408 #[test]
409 fn fxrates_rate() {
410 let fxr = FXRates::try_new(
411 vec![
412 FXRate::try_new("eur", "usd", Number::F64(1.08), Some(ndt(2004, 1, 1))).unwrap(),
413 FXRate::try_new("usd", "jpy", Number::F64(110.0), Some(ndt(2004, 1, 1))).unwrap(),
414 ],
415 None,
416 )
417 .unwrap();
418
419 let expected = arr2(&[
420 [1.0, 1.08, 118.8],
421 [0.9259259, 1.0, 110.0],
422 [0.0084175, 0.0090909, 1.0],
423 ]);
424
425 let arr: Vec<f64> = match fxr.fx_array {
426 NumberArray2::Dual(arr) => arr.iter().map(|x| x.real()).collect(),
427 _ => panic!("unreachable"),
428 };
429 assert!(arr
430 .iter()
431 .zip(expected.iter())
432 .all(|(x, y)| (x - y).abs() < 1e-6))
433 }
434
435 #[test]
436 fn fxrates_multi_chain() {
437 let fxr = FXRates::try_new(
438 vec![
439 FXRate::try_new("eur", "usd", Number::F64(0.5), Some(ndt(2004, 1, 1))).unwrap(),
440 FXRate::try_new("usd", "gbp", Number::F64(1.25), Some(ndt(2004, 1, 1))).unwrap(),
441 FXRate::try_new("gbp", "jpy", Number::F64(100.0), Some(ndt(2004, 1, 1))).unwrap(),
442 FXRate::try_new("nok", "jpy", Number::F64(10.0), Some(ndt(2004, 1, 1))).unwrap(),
443 FXRate::try_new("nok", "brl", Number::F64(5.0), Some(ndt(2004, 1, 1))).unwrap(),
444 ],
445 Some(Ccy::try_new("usd").unwrap()),
446 )
447 .unwrap();
448 let expected = arr2(&[
449 [1.0, 2.0, 1.25, 125.0, 12.5, 62.5],
450 [0.5, 1.0, 0.625, 62.5, 6.25, 31.25],
451 [0.8, 1.6, 1.0, 100.0, 10.0, 50.0],
452 [0.008, 0.016, 0.01, 1.0, 0.1, 0.5],
453 [0.08, 0.16, 0.10, 10.0, 1.0, 5.0],
454 [0.016, 0.032, 0.02, 2.0, 0.2, 1.0],
455 ]);
456
457 let arr: Vec<f64> = match fxr.fx_array {
458 NumberArray2::Dual(arr) => arr.iter().map(|x| x.real()).collect(),
459 _ => panic!("unreachable"),
460 };
461 println!("arr: {:?}", arr);
462 assert!(arr
463 .iter()
464 .zip(expected.iter())
465 .all(|(x, y)| (x - y).abs() < 1e-6))
466 }
467
468 #[test]
469 fn fxrates_single_central_currency() {
470 let fxr = FXRates::try_new(
471 vec![
472 FXRate::try_new("eur", "usd", Number::F64(0.5), Some(ndt(2004, 1, 1))).unwrap(),
473 FXRate::try_new("usd", "gbp", Number::F64(1.25), Some(ndt(2004, 1, 1))).unwrap(),
474 FXRate::try_new("usd", "jpy", Number::F64(100.0), Some(ndt(2004, 1, 1))).unwrap(),
475 FXRate::try_new("usd", "nok", Number::F64(10.0), Some(ndt(2004, 1, 1))).unwrap(),
476 FXRate::try_new("usd", "brl", Number::F64(50.0), Some(ndt(2004, 1, 1))).unwrap(),
477 ],
478 Some(Ccy::try_new("usd").unwrap()),
479 )
480 .unwrap();
481 let expected = arr2(&[
482 [1.0, 2.0, 1.25, 100.0, 10.0, 50.0],
483 [0.5, 1.0, 0.625, 50.0, 5.0, 25.0],
484 [0.8, 1.6, 1.0, 80.0, 8.0, 40.0],
485 [0.01, 0.02, 0.0125, 1.0, 0.1, 0.5],
486 [0.1, 0.2, 0.125, 10.0, 1.0, 5.0],
487 [0.02, 0.04, 0.025, 2.0, 0.2, 1.0],
488 ]);
489
490 let arr: Vec<f64> = match fxr.fx_array {
491 NumberArray2::Dual(arr) => arr.iter().map(|x| x.real()).collect(),
492 _ => panic!("unreachable"),
493 };
494 println!("arr: {:?}", arr);
495 assert!(arr
496 .iter()
497 .zip(expected.iter())
498 .all(|(x, y)| (x - y).abs() < 1e-6))
499 }
500
501 #[test]
502 fn fxrates_creation_error() {
503 let fxr = FXRates::try_new(
504 vec![
505 FXRate::try_new("eur", "usd", Number::F64(1.0), Some(ndt(2004, 1, 1))).unwrap(),
506 FXRate::try_new("usd", "eur", Number::F64(1.0), Some(ndt(2004, 1, 1))).unwrap(),
507 FXRate::try_new("sek", "nok", Number::F64(1.0), Some(ndt(2004, 1, 1))).unwrap(),
508 ],
509 None,
510 );
511 match fxr {
512 Ok(_) => assert!(false),
513 Err(_) => assert!(true),
514 }
515 }
516
517 #[test]
518 fn fxrates_eq() {
519 let fxr = FXRates::try_new(
520 vec![
521 FXRate::try_new("eur", "usd", Number::F64(1.08), None).unwrap(),
522 FXRate::try_new("usd", "jpy", Number::F64(110.0), None).unwrap(),
523 ],
524 None,
525 )
526 .unwrap();
527
528 let fxr2 = FXRates::try_new(
529 vec![
530 FXRate::try_new("eur", "usd", Number::F64(1.08), None).unwrap(),
531 FXRate::try_new("usd", "jpy", Number::F64(110.0), None).unwrap(),
532 ],
533 None,
534 )
535 .unwrap();
536
537 assert_eq!(fxr, fxr2)
538 }
539
540 #[test]
541 fn fxrates_update() {
542 let mut fxr = FXRates::try_new(
543 vec![
544 FXRate::try_new("eur", "usd", Number::F64(1.08), None).unwrap(),
545 FXRate::try_new("usd", "jpy", Number::F64(110.0), None).unwrap(),
546 ],
547 None,
548 )
549 .unwrap();
550 let _ = fxr.update(vec![FXRate::try_new(
551 "usd",
552 "jpy",
553 Number::F64(120.0),
554 None,
555 )
556 .unwrap()]);
557 let rate = fxr
558 .rate(&Ccy::try_new("eur").unwrap(), &Ccy::try_new("usd").unwrap())
559 .unwrap();
560 match rate {
561 Number::Dual(d) => assert_eq!(d.real, 1.08),
562 _ => panic!("failure"),
563 };
564 let rate = fxr
565 .rate(&Ccy::try_new("usd").unwrap(), &Ccy::try_new("jpy").unwrap())
566 .unwrap();
567 match rate {
568 Number::Dual(d) => assert_eq!(d.real, 120.0),
569 _ => panic!("failure"),
570 }
571 }
572
573 #[test]
574 fn second_order_gradients_on_set_order() {
575 let mut fxr = FXRates::try_new(
576 vec![
577 FXRate::try_new("usd", "nok", Number::F64(10.0), None).unwrap(),
578 FXRate::try_new("eur", "nok", Number::F64(8.0), None).unwrap(),
579 ],
580 None,
581 )
582 .unwrap();
583 let _ = fxr.set_ad_order(ADOrder::Two);
584 let d1 = Dual2::new(10.0, vec!["fx_usdnok".to_string()]);
585 let d2 = Dual2::new(8.0, vec!["fx_eurnok".to_string()]);
586 let d3 = d1 / d2;
587 let rate: Dual2 = fxr
588 .rate(&Ccy::try_new("usd").unwrap(), &Ccy::try_new("eur").unwrap())
589 .unwrap()
590 .into();
591 assert_eq!(d3, rate)
592 }
593}