rateslib/fx/rates/
mod.rs

1//! Create objects related to the management and valuation of monetary amounts in different
2//! currencies, measured at different settlement dates in time.
3
4use 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/// A multi-currency FX market deriving all crosses from a vector of `FXRate`s.
27#[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        // Validations:
53        // 1. fx_rates is non-zero length
54        // 2. currencies are not under or over overspecified
55        // 3. settlement dates are all consistent.
56        // 4. No Dual2 data types are provided as input
57
58        // 1.
59        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        // 2.
76        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        // 3.
87        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(&currencies, &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        // validate that the input vector contains FX pairs that are already associated with the instance
135        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                // leave the NumberArray2 unchanged.
167                Ok(())
168            }
169            (ADOrder::One, NumberArray2::F64(_)) => {
170                // rebuild the derivatives
171                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                // rebuild the derivatives
177                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                // covert dual into f64
195                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                // covert dual into f64
208                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                // rebuild derivatives
221                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
229/// Return a one-hot mapping, in 2-d array form of the initial connections between currencies,
230/// given the pairs associated with the FX rates.
231fn 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
242/// Return a 2-d array containing all calculated FX rates as initially provided.
243///
244/// T will be an f64, Dual or Dual2
245fn 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    // check for stopping criteria if all edges, i.e. FX rates have been populated.
276    if edges.sum() == ((edges.len_of(Axis(0)) * edges.len_of(Axis(1))) as i16) {
277        return Ok(true);
278    }
279
280    // otherwise, find the number of edges connected with each currency
281    // that is not in the list of pre-checked values
282    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    // and from those find the index of the currency with the most edges
290    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            // The `prev_value` list contain every node and the `edges` matrix is not solved,
299            // hence this cannot be solved.
300            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    // `combinations` is a list of pairs that can be formed from the edges associated
309    // with `node`, but which have not yet been populated. These will be populated
310    // in the next stage.
311    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    // iterate through the unpopulated combinations and determine the FX rate between those
322    // nodes calculating via the FX rate with the central node.
323    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        // then that discovered node not yielded any results, so add it to the list of checked
334        // prev values checked and run again, recursively.
335        prev_value.insert(node);
336        return mut_arrays_remaining_elements(fx_array.view_mut(), edges.view_mut(), prev_value);
337    } else {
338        // a population has been successful. Re run the algorithm placing the most recently
339        // sampled node in the set of prev values, so that an infinite loop is avoide and a new
340        // node will be sampled next time.
341        return mut_arrays_remaining_elements(
342            fx_array.view_mut(),
343            edges.view_mut(),
344            HashSet::from([node]),
345        );
346    }
347}
348
349/// Creates an FX Array with the sparse graph network algorithm defining Dual variables directly.
350fn 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}