Skip to content

Commit

Permalink
REF: simplify FXRates construction algorithm (rs) (#679)
Browse files Browse the repository at this point in the history
Co-authored-by: JHM Darbyshire (win11) <[email protected]>
  • Loading branch information
attack68 and attack68 authored Feb 7, 2025
1 parent 3b6e85b commit 9f2b9e6
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 33 deletions.
36 changes: 36 additions & 0 deletions python/tests/test_fx.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions rust/dual/linalg/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
146 changes: 115 additions & 31 deletions rust/fx/rates/mod.rs
Original file line number Diff line number Diff line change
@@ -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::*;
Expand Down Expand Up @@ -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<usize>> = 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<Vec<usize>> = 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;
Expand All @@ -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(),
Expand Down Expand Up @@ -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<f64> = 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<f64> = 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(
Expand Down

0 comments on commit 9f2b9e6

Please sign in to comment.