diff --git a/python/tests/test_fx.py b/python/tests/test_fx.py index 2647920e..42033b27 100644 --- a/python/tests/test_fx.py +++ b/python/tests/test_fx.py @@ -72,6 +72,42 @@ def test_rates() -> None: assert fxr.rate("eurgbp") == Dual(1.25, ["fx_usdeur", "fx_usdgbp"], [-0.625, 0.50]) +def test_fxrates_multi_single_currency() -> None: + fxr = FXRates({"eurusd": 0.5, "usdgbp": 1.25, "usdjpy": 100.0, "usdnok": 10.0, "usdbrl": 50.0}) + fxr._set_ad_order(0) + expected = np.array( + [ + [1.0, 2.0, 1.25, 100.0, 10.0, 50.0], + [0.5, 1.0, 0.625, 50.0, 5.0, 25.0], + [0.8, 1.6, 1.0, 80.0, 8.0, 40.0], + [0.01, 0.02, 0.0125, 1.0, 0.1, 0.5], + [0.1, 0.2, 0.125, 10.0, 1.0, 5.0], + [0.02, 0.04, 0.025, 2.0, 0.2, 1.0], + ] + ) + for i in range(6): + for j in range(6): + assert abs(fxr.fx_array[i, j] - expected[i, j]) < 1e-8 + + +def test_fxrates_multi_chain() -> None: + fxr = FXRates({"eurusd": 0.5, "usdgbp": 1.25, "gbpjpy": 100.0, "nokjpy": 10.0, "nokbrl": 5.0}) + fxr._set_ad_order(0) + expected = np.array( + [ + [1.0, 2.0, 1.25, 125.0, 12.5, 62.5], + [0.5, 1.0, 0.625, 62.5, 6.25, 31.25], + [0.8, 1.6, 1.0, 100.0, 10.0, 50.0], + [0.008, 0.016, 0.01, 1.0, 0.1, 0.5], + [0.08, 0.16, 0.10, 10.0, 1.0, 5.0], + [0.016, 0.032, 0.02, 2.0, 0.2, 1.0], + ] + ) + for i in range(6): + for j in range(6): + assert abs(fxr.fx_array[i, j] - expected[i, j]) < 1e-8 + + def test_fxrates_pickle(): fxr = FXRates({"usdeur": 2.0, "usdgbp": 2.5}, settlement=dt(2002, 1, 1)) import pickle diff --git a/rust/dual/linalg/mod.rs b/rust/dual/linalg/mod.rs index 09cd06b2..86b6cc04 100644 --- a/rust/dual/linalg/mod.rs +++ b/rust/dual/linalg/mod.rs @@ -7,5 +7,3 @@ pub use crate::dual::linalg::linalg_dual::{dmul11_, dmul21_, dmul22_, douter11_, pub use crate::dual::linalg::linalg_f64::{ dfmul21_, dfmul22_, fdmul11_, fdmul21_, fdmul22_, fdsolve, fouter11_, }; - -pub(crate) use crate::dual::linalg::linalg_dual::argabsmax; diff --git a/rust/fx/rates/mod.rs b/rust/fx/rates/mod.rs index 91cc62b0..3492c61f 100644 --- a/rust/fx/rates/mod.rs +++ b/rust/fx/rates/mod.rs @@ -1,7 +1,6 @@ //! Create objects related to the management and valuation of monetary amounts in different //! currencies, measured at different settlement dates in time. -use crate::dual::linalg::argabsmax; use crate::dual::{set_order_clone, ADOrder, Dual, Dual2, Number, NumberArray2}; use crate::json::JSON; use chrono::prelude::*; @@ -273,42 +272,56 @@ where for<'a> &'a T: Mul<&'a T, Output = T>, for<'a> f64: Div<&'a T, Output = T>, { - if prev_value.len() == edges.len_of(Axis(0)) { - return Err(PyValueError::new_err( - "FX Array cannot be solved. There are degenerate FX rate pairs.\n\ - For example ('eurusd' + 'usdeur') or ('usdeur', 'eurjpy', 'usdjpy').", - )); - } + // check for stopping criteria if all edges, i.e. FX rates have been populated. if edges.sum() == ((edges.len_of(Axis(0)) * edges.len_of(Axis(1))) as i16) { - return Ok(true); // all edges and values have already been populated. + return Ok(true); } - let mut row_edges = edges.sum_axis(Axis(1)); - - let mut node: usize = edges.len_of(Axis(1)) + 1_usize; - let mut combinations_: Vec> = Vec::new(); - let mut start_flag = true; - - while start_flag || prev_value.contains(&node) { - start_flag = false; - - // find node with most outgoing edges - node = argabsmax(row_edges.view()); - row_edges[node] = 0_i16; - // filter by combinations that are not already populated - combinations_ = edges - .row(node) - .iter() - .zip(0_usize..) - .filter(|(v, i)| **v == 1_i16 && *i != node) - .map(|(_v, i)| i) - .combinations(2) - .filter(|v| edges[[v[0], v[1]]] == 0_i16) - .collect(); + // otherwise, find the number of edges connected with each currency + // that is not in the list of pre-checked values + let available_edges_and_nodes: Vec<(i16, usize)> = edges + .sum_axis(Axis(1)) + .into_iter() + .zip(0_usize..) + .filter(|(_v, i)| !prev_value.contains(i)) + .into_iter() + .collect(); + // and from those find the index of the currency with the most edges + let sampled_node = available_edges_and_nodes + .into_iter() + .max_by_key(|(value, _)| *value) + .map(|(_, idx)| idx); + + let node: usize; + match sampled_node { + None => { + // The `prev_value` list contain every node and the `edges` matrix is not solved, + // hence this cannot be solved. + return Err(PyValueError::new_err( + "FX Array cannot be solved. There are degenerate FX rate pairs.\n\ + For example ('eurusd' + 'usdeur') or ('usdeur', 'eurjpy', 'usdjpy').", + )); + } + Some(node_) => node = usize::try_from(node_).unwrap(), } + // `combinations` is a list of pairs that can be formed from the edges associated + // with `node`, but which have not yet been populated. These will be populated + // in the next stage. + let combinations: Vec> = edges + .row(node) + .iter() + .zip(0_usize..) + .filter(|(v, i)| **v == 1_i16 && *i != node) + .map(|(_v, i)| i) + .combinations(2) + .filter(|v| edges[[v[0], v[1]]] == 0_i16) + .collect(); + + // iterate through the unpopulated combinations and determine the FX rate between those + // nodes calculating via the FX rate with the central node. let mut counter: i16 = 0; - for c in combinations_ { + for c in combinations { counter += 1_i16; edges[[c[0], c[1]]] = 1_i16; edges[[c[1], c[0]]] = 1_i16; @@ -317,9 +330,14 @@ where } if counter == 0 { + // then that discovered node not yielded any results, so add it to the list of checked + // prev values checked and run again, recursively. prev_value.insert(node); return mut_arrays_remaining_elements(fx_array.view_mut(), edges.view_mut(), prev_value); } else { + // a population has been successful. Re run the algorithm placing the most recently + // sampled node in the set of prev values, so that an infinite loop is avoide and a new + // node will be sampled next time. return mut_arrays_remaining_elements( fx_array.view_mut(), edges.view_mut(), @@ -414,6 +432,72 @@ mod tests { .all(|(x, y)| (x - y).abs() < 1e-6)) } + #[test] + fn fxrates_multi_chain() { + let fxr = FXRates::try_new( + vec![ + FXRate::try_new("eur", "usd", Number::F64(0.5), Some(ndt(2004, 1, 1))).unwrap(), + FXRate::try_new("usd", "gbp", Number::F64(1.25), Some(ndt(2004, 1, 1))).unwrap(), + FXRate::try_new("gbp", "jpy", Number::F64(100.0), Some(ndt(2004, 1, 1))).unwrap(), + FXRate::try_new("nok", "jpy", Number::F64(10.0), Some(ndt(2004, 1, 1))).unwrap(), + FXRate::try_new("nok", "brl", Number::F64(5.0), Some(ndt(2004, 1, 1))).unwrap(), + ], + Some(Ccy::try_new("usd").unwrap()), + ) + .unwrap(); + let expected = arr2(&[ + [1.0, 2.0, 1.25, 125.0, 12.5, 62.5], + [0.5, 1.0, 0.625, 62.5, 6.25, 31.25], + [0.8, 1.6, 1.0, 100.0, 10.0, 50.0], + [0.008, 0.016, 0.01, 1.0, 0.1, 0.5], + [0.08, 0.16, 0.10, 10.0, 1.0, 5.0], + [0.016, 0.032, 0.02, 2.0, 0.2, 1.0], + ]); + + let arr: Vec = match fxr.fx_array { + NumberArray2::Dual(arr) => arr.iter().map(|x| x.real()).collect(), + _ => panic!("unreachable"), + }; + println!("arr: {:?}", arr); + assert!(arr + .iter() + .zip(expected.iter()) + .all(|(x, y)| (x - y).abs() < 1e-6)) + } + + #[test] + fn fxrates_single_central_currency() { + let fxr = FXRates::try_new( + vec![ + FXRate::try_new("eur", "usd", Number::F64(0.5), Some(ndt(2004, 1, 1))).unwrap(), + FXRate::try_new("usd", "gbp", Number::F64(1.25), Some(ndt(2004, 1, 1))).unwrap(), + FXRate::try_new("usd", "jpy", Number::F64(100.0), Some(ndt(2004, 1, 1))).unwrap(), + FXRate::try_new("usd", "nok", Number::F64(10.0), Some(ndt(2004, 1, 1))).unwrap(), + FXRate::try_new("usd", "brl", Number::F64(50.0), Some(ndt(2004, 1, 1))).unwrap(), + ], + Some(Ccy::try_new("usd").unwrap()), + ) + .unwrap(); + let expected = arr2(&[ + [1.0, 2.0, 1.25, 100.0, 10.0, 50.0], + [0.5, 1.0, 0.625, 50.0, 5.0, 25.0], + [0.8, 1.6, 1.0, 80.0, 8.0, 40.0], + [0.01, 0.02, 0.0125, 1.0, 0.1, 0.5], + [0.1, 0.2, 0.125, 10.0, 1.0, 5.0], + [0.02, 0.04, 0.025, 2.0, 0.2, 1.0], + ]); + + let arr: Vec = match fxr.fx_array { + NumberArray2::Dual(arr) => arr.iter().map(|x| x.real()).collect(), + _ => panic!("unreachable"), + }; + println!("arr: {:?}", arr); + assert!(arr + .iter() + .zip(expected.iter()) + .all(|(x, y)| (x - y).abs() < 1e-6)) + } + #[test] fn fxrates_creation_error() { let fxr = FXRates::try_new(