rateslib/fx/
rates_py.rs

1//! Wrapper module to export Rust FX rate data types to Python using pyo3 bindings.
2
3use crate::dual::{ADOrder, Number, NumberArray2};
4use crate::fx::rates::{Ccy, FXRate, FXRates};
5use bincode::config::legacy;
6use bincode::serde::{decode_from_slice, encode_to_vec};
7use chrono::prelude::*;
8use ndarray::Axis;
9use pyo3::prelude::*;
10// use std::collections::HashMap;
11use pyo3::exceptions::PyValueError;
12// use pyo3::exceptions::PyValueError;
13use crate::json::json_py::DeserializedObj;
14use crate::json::JSON;
15use pyo3::types::PyBytes;
16
17#[pymethods]
18impl Ccy {
19    #[new]
20    fn new_py(name: &str) -> PyResult<Self> {
21        Ccy::try_new(name)
22    }
23
24    #[getter]
25    #[pyo3(name = "name")]
26    fn name_py(&self) -> PyResult<String> {
27        Ok(self.name.to_string())
28    }
29
30    fn __repr__(&self) -> PyResult<String> {
31        Ok(format!("<Ccy: '{}'>", self.name))
32    }
33
34    fn __eq__(&self, other: &Self) -> bool {
35        self.name == other.name
36    }
37
38    // Pickling
39    pub fn __setstate__(&mut self, state: Bound<'_, PyBytes>) -> PyResult<()> {
40        *self = decode_from_slice(state.as_bytes(), legacy()).unwrap().0;
41        Ok(())
42    }
43    pub fn __getstate__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
44        Ok(PyBytes::new(py, &encode_to_vec(&self, legacy()).unwrap()))
45    }
46    pub fn __getnewargs__<'py>(&self) -> PyResult<(String,)> {
47        Ok(((*(self.name)).clone(),))
48    }
49}
50
51#[pymethods]
52impl FXRate {
53    #[new]
54    #[pyo3(signature = (lhs, rhs, rate, settlement=None))]
55    fn new_py(
56        lhs: &str,
57        rhs: &str,
58        rate: Number,
59        settlement: Option<NaiveDateTime>,
60    ) -> PyResult<Self> {
61        FXRate::try_new(lhs, rhs, rate, settlement)
62    }
63
64    #[getter]
65    #[pyo3(name = "rate")]
66    fn rate_py(&self) -> PyResult<Number> {
67        Ok(self.rate.clone())
68    }
69
70    #[getter]
71    #[pyo3(name = "ad")]
72    fn ad_py(&self) -> u8 {
73        match self.rate {
74            Number::F64(_) => 0,
75            Number::Dual(_) => 1,
76            Number::Dual2(_) => 2,
77        }
78    }
79
80    #[getter]
81    #[pyo3(name = "settlement")]
82    fn settlement_py(&self) -> PyResult<Option<NaiveDateTime>> {
83        Ok(self.settlement)
84    }
85
86    #[getter]
87    #[pyo3(name = "pair")]
88    fn pair_py(&self) -> PyResult<String> {
89        Ok(format!("{}", self.pair))
90    }
91
92    fn __repr__(&self) -> PyResult<String> {
93        match &self.rate {
94            Number::F64(f) => Ok(format!("<FXRate: '{}' {}>", self.pair, f)),
95            Number::Dual(d) => Ok(format!(
96                "<FXRate: '{}' <Dual: {}, ..>>",
97                self.pair,
98                d.real()
99            )),
100            Number::Dual2(d) => Ok(format!(
101                "<FXRate: '{}' <Dual2: {}, ..>>",
102                self.pair,
103                d.real()
104            )),
105        }
106    }
107
108    fn __eq__(&self, other: &Self) -> bool {
109        self == other
110    }
111
112    // Pickling
113    pub fn __setstate__(&mut self, state: Bound<'_, PyBytes>) -> PyResult<()> {
114        *self = decode_from_slice(state.as_bytes(), legacy()).unwrap().0;
115        Ok(())
116    }
117    pub fn __getstate__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
118        Ok(PyBytes::new(py, &encode_to_vec(&self, legacy()).unwrap()))
119    }
120    pub fn __getnewargs__<'py>(&self) -> PyResult<(String, String, Number, Option<NaiveDateTime>)> {
121        Ok((
122            (*(self.pair.0.name)).clone(),
123            (*(self.pair.1.name)).clone(),
124            self.rate.clone(),
125            self.settlement,
126        ))
127    }
128}
129
130#[pymethods]
131impl FXRates {
132    // #[new]
133    // fn new_py(
134    //     fx_rates: HashMap<String, Number>,
135    //     settlement: NaiveDateTime,
136    //     base: Option<String>,
137    // ) -> PyResult<Self> {
138    //     let base_ = match base {
139    //         None => None,
140    //         Some(v) => Some(Ccy::try_new(&v)?),
141    //     };
142    //     let fx_rates_ = fx_rates
143    //         .into_iter()
144    //         .map(|(k, v)| FXRate::try_new(&k[..3], &k[3..], v, Some(settlement)))
145    //         .collect::<Result<Vec<_>, _>>()?;
146    //     FXRates::try_new(fx_rates_, settlement, base_)
147    // }
148    #[new]
149    #[pyo3(signature = (fx_rates, base=None))]
150    fn new_py(fx_rates: Vec<FXRate>, base: Option<Ccy>) -> PyResult<Self> {
151        FXRates::try_new(fx_rates, base)
152    }
153
154    #[getter]
155    #[pyo3(name = "fx_rates")]
156    fn fx_rates_py(&self) -> PyResult<Vec<FXRate>> {
157        Ok(self.fx_rates.clone())
158    }
159
160    #[getter]
161    #[pyo3(name = "currencies")]
162    fn currencies_py(&self) -> PyResult<Vec<Ccy>> {
163        Ok(Vec::from_iter(self.currencies.iter().cloned()))
164    }
165
166    #[getter]
167    #[pyo3(name = "ad")]
168    fn ad_py(&self) -> PyResult<u8> {
169        match &self.fx_array {
170            NumberArray2::F64(_) => Ok(0),
171            NumberArray2::Dual(_) => Ok(1),
172            NumberArray2::Dual2(_) => Ok(2),
173        }
174    }
175
176    #[getter]
177    #[pyo3(name = "base")]
178    fn base_py(&self) -> PyResult<Ccy> {
179        Ok(self.currencies[0])
180    }
181
182    #[getter]
183    #[pyo3(name = "fx_vector")]
184    fn fx_vector_py(&self) -> PyResult<Vec<Number>> {
185        match &self.fx_array {
186            NumberArray2::F64(arr) => Ok(arr.row(0).iter().map(|x| Number::F64(*x)).collect()),
187            NumberArray2::Dual(arr) => {
188                Ok(arr.row(0).iter().map(|x| Number::Dual(x.clone())).collect())
189            }
190            NumberArray2::Dual2(arr) => Ok(arr
191                .row(0)
192                .iter()
193                .map(|x| Number::Dual2(x.clone()))
194                .collect()),
195        }
196    }
197
198    #[getter]
199    #[pyo3(name = "fx_array")]
200    fn fx_array_py(&self) -> PyResult<Vec<Vec<Number>>> {
201        match &self.fx_array {
202            NumberArray2::F64(arr) => Ok(arr
203                .lanes(Axis(1))
204                .into_iter()
205                .map(|row| row.iter().map(|d| Number::F64(*d)).collect())
206                .collect()),
207            NumberArray2::Dual(arr) => Ok(arr
208                .lanes(Axis(1))
209                .into_iter()
210                .map(|row| row.iter().map(|d| Number::Dual(d.clone())).collect())
211                .collect()),
212            NumberArray2::Dual2(arr) => Ok(arr
213                .lanes(Axis(1))
214                .into_iter()
215                .map(|row| row.iter().map(|d| Number::Dual2(d.clone())).collect())
216                .collect()),
217        }
218    }
219
220    #[pyo3(name = "get_ccy_index")]
221    fn get_ccy_index_py(&self, currency: Ccy) -> Option<usize> {
222        self.get_ccy_index(&currency)
223    }
224
225    #[pyo3(name = "rate")]
226    fn rate_py(&self, lhs: &Ccy, rhs: &Ccy) -> PyResult<Option<Number>> {
227        Ok(self.rate(lhs, rhs))
228    }
229
230    #[pyo3(name = "update")]
231    fn update_py(&mut self, fx_rates: Vec<FXRate>) -> PyResult<()> {
232        self.update(fx_rates)
233    }
234
235    #[pyo3(name = "set_ad_order")]
236    fn set_ad_order_py(&mut self, ad: ADOrder) -> PyResult<()> {
237        self.set_ad_order(ad)?;
238        Ok(())
239    }
240
241    // JSON
242    #[pyo3(name = "to_json")]
243    fn to_json_py(&self) -> PyResult<String> {
244        match DeserializedObj::FXRates(self.clone()).to_json() {
245            Ok(v) => Ok(v),
246            Err(_) => Err(PyValueError::new_err(
247                "Failed to serialize `UnionCal` to JSON.",
248            )),
249        }
250    }
251
252    // Pickling
253    pub fn __setstate__(&mut self, state: Bound<'_, PyBytes>) -> PyResult<()> {
254        *self = decode_from_slice(state.as_bytes(), legacy()).unwrap().0;
255        Ok(())
256    }
257    pub fn __getstate__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
258        Ok(PyBytes::new(py, &encode_to_vec(&self, legacy()).unwrap()))
259    }
260    pub fn __getnewargs__<'py>(&self) -> PyResult<(Vec<FXRate>, Option<Ccy>)> {
261        Ok((self.fx_rates.clone(), Some(self.currencies[0])))
262    }
263
264    // Equality
265    fn __eq__(&self, other: FXRates) -> bool {
266        self.eq(&other)
267    }
268
269    fn __copy__(&self) -> Self {
270        self.clone()
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277    use crate::scheduling::ndt;
278
279    #[test]
280    fn fxrates_eq() {
281        let fxr = FXRates::try_new(
282            vec![
283                FXRate::try_new("eur", "usd", Number::F64(1.08), Some(ndt(2004, 1, 1))).unwrap(),
284                FXRate::try_new("usd", "jpy", Number::F64(110.0), Some(ndt(2004, 1, 1))).unwrap(),
285            ],
286            None,
287        )
288        .unwrap();
289
290        let fxr2 = FXRates::try_new(
291            vec![
292                FXRate::try_new("eur", "usd", Number::F64(1.08), Some(ndt(2004, 1, 1))).unwrap(),
293                FXRate::try_new("usd", "jpy", Number::F64(110.0), Some(ndt(2004, 1, 1))).unwrap(),
294            ],
295            None,
296        )
297        .unwrap();
298
299        assert!(fxr.__eq__(fxr2))
300    }
301}