Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

REF: simplify FXRates construction algorithm (rs) #679

Merged
merged 8 commits into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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