nautilus_model/python/data/
funding.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Python bindings for funding rate data types.
17
18use std::{
19    collections::HashMap,
20    hash::{Hash, Hasher},
21    str::FromStr,
22};
23
24use nautilus_core::{
25    UnixNanos,
26    python::{IntoPyObjectNautilusExt, to_pyvalue_err},
27    serialization::Serializable,
28};
29use pyo3::{
30    exceptions::PyKeyError,
31    prelude::*,
32    pyclass::CompareOp,
33    types::{PyString, PyTuple},
34};
35use rust_decimal::Decimal;
36
37use crate::{data::FundingRateUpdate, identifiers::InstrumentId, python::common::PY_MODULE_MODEL};
38
39#[pymethods]
40impl FundingRateUpdate {
41    #[new]
42    #[pyo3(signature = (instrument_id, rate, ts_event, ts_init, next_funding_ns=None))]
43    fn py_new(
44        instrument_id: InstrumentId,
45        rate: Decimal,
46        ts_event: u64,
47        ts_init: u64,
48        next_funding_ns: Option<u64>,
49    ) -> Self {
50        let ts_event_nanos = UnixNanos::from(ts_event);
51        let ts_init_nanos = UnixNanos::from(ts_init);
52        let next_funding_nanos = next_funding_ns.map(UnixNanos::from);
53
54        Self::new(
55            instrument_id,
56            rate,
57            next_funding_nanos,
58            ts_event_nanos,
59            ts_init_nanos,
60        )
61    }
62
63    fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
64        match op {
65            CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
66            CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
67            _ => py.NotImplemented(),
68        }
69    }
70
71    fn __repr__(&self) -> String {
72        format!("{self:?}")
73    }
74
75    fn __str__(&self) -> String {
76        format!("{self}")
77    }
78
79    fn __hash__(&self) -> isize {
80        let mut hasher = std::collections::hash_map::DefaultHasher::new();
81        Hash::hash(self, &mut hasher);
82        Hasher::finish(&hasher) as isize
83    }
84
85    #[getter]
86    #[pyo3(name = "instrument_id")]
87    fn py_instrument_id(&self) -> InstrumentId {
88        self.instrument_id
89    }
90
91    #[getter]
92    #[pyo3(name = "rate")]
93    fn py_rate(&self) -> Decimal {
94        self.rate
95    }
96
97    #[getter]
98    #[pyo3(name = "next_funding_ns")]
99    fn py_next_funding_ns(&self) -> Option<u64> {
100        self.next_funding_ns.map(|ts| ts.as_u64())
101    }
102
103    #[getter]
104    #[pyo3(name = "ts_event")]
105    fn py_ts_event(&self) -> u64 {
106        self.ts_event.as_u64()
107    }
108
109    #[getter]
110    #[pyo3(name = "ts_init")]
111    fn py_ts_init(&self) -> u64 {
112        self.ts_init.as_u64()
113    }
114
115    #[staticmethod]
116    #[pyo3(name = "fully_qualified_name")]
117    fn py_fully_qualified_name() -> String {
118        format!("{}:{}", PY_MODULE_MODEL, stringify!(FundingRateUpdate))
119    }
120
121    #[staticmethod]
122    #[pyo3(name = "get_metadata")]
123    fn py_get_metadata(instrument_id: &InstrumentId) -> HashMap<String, String> {
124        Self::get_metadata(instrument_id)
125    }
126
127    #[staticmethod]
128    #[pyo3(name = "get_fields")]
129    fn py_get_fields() -> HashMap<String, String> {
130        Self::get_fields().into_iter().collect()
131    }
132
133    #[pyo3(name = "to_dict")]
134    fn py_to_dict(&self, py: Python<'_>) -> PyResult<PyObject> {
135        let mut dict = HashMap::new();
136        dict.insert(
137            "type".to_string(),
138            "FundingRateUpdate".into_py_any_unwrap(py),
139        );
140        dict.insert(
141            "instrument_id".to_string(),
142            self.instrument_id.to_string().into_py_any_unwrap(py),
143        );
144        dict.insert(
145            "rate".to_string(),
146            self.rate.to_string().into_py_any_unwrap(py),
147        );
148        if let Some(next_funding_ns) = self.next_funding_ns {
149            dict.insert(
150                "next_funding_ns".to_string(),
151                next_funding_ns.as_u64().into_py_any_unwrap(py),
152            );
153        }
154        dict.insert(
155            "ts_event".to_string(),
156            self.ts_event.as_u64().into_py_any_unwrap(py),
157        );
158        dict.insert(
159            "ts_init".to_string(),
160            self.ts_init.as_u64().into_py_any_unwrap(py),
161        );
162        Ok(dict.into_py_any_unwrap(py))
163    }
164
165    #[staticmethod]
166    #[pyo3(name = "from_dict")]
167    fn py_from_dict(py: Python<'_>, values: Py<PyAny>) -> PyResult<Self> {
168        let dict = values.downcast_bound::<pyo3::types::PyDict>(py)?;
169
170        let instrument_id_str: String = dict
171            .get_item("instrument_id")?
172            .ok_or_else(|| PyErr::new::<PyKeyError, _>("Missing 'instrument_id' field"))?
173            .extract()?;
174        let instrument_id = InstrumentId::from_str(&instrument_id_str).map_err(to_pyvalue_err)?;
175
176        let rate_str: String = dict
177            .get_item("rate")?
178            .ok_or_else(|| PyErr::new::<PyKeyError, _>("Missing 'rate' field"))?
179            .extract()?;
180        let rate = Decimal::from_str(&rate_str).map_err(to_pyvalue_err)?;
181
182        let ts_event: u64 = dict
183            .get_item("ts_event")?
184            .ok_or_else(|| PyErr::new::<PyKeyError, _>("Missing 'ts_event' field"))?
185            .extract()?;
186
187        let ts_init: u64 = dict
188            .get_item("ts_init")?
189            .ok_or_else(|| PyErr::new::<PyKeyError, _>("Missing 'ts_init' field"))?
190            .extract()?;
191
192        let next_funding_ns: Option<u64> = dict
193            .get_item("next_funding_ns")
194            .ok()
195            .flatten()
196            .and_then(|v| v.extract().ok());
197
198        Ok(Self::new(
199            instrument_id,
200            rate,
201            next_funding_ns.map(UnixNanos::from),
202            UnixNanos::from(ts_event),
203            UnixNanos::from(ts_init),
204        ))
205    }
206
207    #[pyo3(name = "from_json")]
208    #[staticmethod]
209    fn py_from_json(data: Vec<u8>) -> PyResult<Self> {
210        Self::from_json_bytes(&data).map_err(to_pyvalue_err)
211    }
212
213    #[pyo3(name = "from_msgpack")]
214    #[staticmethod]
215    fn py_from_msgpack(data: Vec<u8>) -> PyResult<Self> {
216        Self::from_msgpack_bytes(&data).map_err(to_pyvalue_err)
217    }
218
219    #[pyo3(name = "to_json")]
220    fn py_to_json(&self) -> PyResult<Vec<u8>> {
221        self.to_json_bytes()
222            .map(|b| b.to_vec())
223            .map_err(to_pyvalue_err)
224    }
225
226    #[pyo3(name = "to_msgpack")]
227    fn py_to_msgpack(&self) -> PyResult<Vec<u8>> {
228        self.to_msgpack_bytes()
229            .map(|b| b.to_vec())
230            .map_err(to_pyvalue_err)
231    }
232
233    fn __setstate__(&mut self, state: &Bound<'_, PyAny>) -> PyResult<()> {
234        let py_tuple: &Bound<'_, PyTuple> = state.downcast::<PyTuple>()?;
235
236        let item0 = py_tuple.get_item(0)?;
237        let instrument_id_str: String = item0.downcast::<PyString>()?.extract()?;
238
239        let item1 = py_tuple.get_item(1)?;
240        let rate_str: String = item1.downcast::<PyString>()?.extract()?;
241
242        let next_funding_ns: Option<u64> = py_tuple.get_item(2).ok().and_then(|item| {
243            if item.is_none() {
244                None
245            } else {
246                item.extract().ok()
247            }
248        });
249        let ts_event: u64 = py_tuple.get_item(3)?.extract()?;
250        let ts_init: u64 = py_tuple.get_item(4)?.extract()?;
251
252        self.instrument_id = InstrumentId::from_str(&instrument_id_str).map_err(to_pyvalue_err)?;
253        self.rate = Decimal::from_str(&rate_str).map_err(to_pyvalue_err)?;
254        self.next_funding_ns = next_funding_ns.map(UnixNanos::from);
255        self.ts_event = UnixNanos::from(ts_event);
256        self.ts_init = UnixNanos::from(ts_init);
257
258        Ok(())
259    }
260
261    fn __getstate__(&self, py: Python) -> PyResult<PyObject> {
262        Ok((
263            self.instrument_id.to_string(),
264            self.rate.to_string(),
265            self.next_funding_ns.map(|ts| ts.as_u64()),
266            self.ts_event.as_u64(),
267            self.ts_init.as_u64(),
268        )
269            .into_py_any_unwrap(py))
270    }
271
272    fn __reduce__(&self, py: Python) -> PyResult<PyObject> {
273        let safe_constructor = py.get_type::<Self>().getattr("_safe_constructor")?;
274        let state = self.__getstate__(py)?;
275        Ok((safe_constructor, PyTuple::empty(py), state).into_py_any_unwrap(py))
276    }
277
278    #[staticmethod]
279    #[pyo3(name = "_safe_constructor")]
280    fn py_safe_constructor() -> Self {
281        Self::new(
282            InstrumentId::from("NULL.NULL"),
283            Decimal::ZERO,
284            None,
285            UnixNanos::default(),
286            UnixNanos::default(),
287        )
288    }
289}
290
291impl FundingRateUpdate {
292    /// Creates a new [`FundingRateUpdate`] from a Python object.
293    ///
294    /// # Errors
295    ///
296    /// Returns a `PyErr` if extracting any attribute or converting types fails.
297    pub fn from_pyobject(obj: &Bound<'_, PyAny>) -> PyResult<Self> {
298        let instrument_id_obj: Bound<'_, PyAny> = obj.getattr("instrument_id")?.extract()?;
299        let instrument_id_str: String = instrument_id_obj.getattr("value")?.extract()?;
300        let instrument_id =
301            InstrumentId::from_str(instrument_id_str.as_str()).map_err(to_pyvalue_err)?;
302
303        let rate: Decimal = obj.getattr("rate")?.extract()?;
304        let ts_event: u64 = obj.getattr("ts_event")?.extract()?;
305        let ts_init: u64 = obj.getattr("ts_init")?.extract()?;
306
307        let next_funding_ns: Option<u64> = obj
308            .getattr("next_funding_ns")
309            .ok()
310            .and_then(|x| x.extract().ok());
311
312        Ok(Self::new(
313            instrument_id,
314            rate,
315            next_funding_ns.map(UnixNanos::from),
316            UnixNanos::from(ts_event),
317            UnixNanos::from(ts_init),
318        ))
319    }
320}
321
322////////////////////////////////////////////////////////////////////////////////
323// Tests
324////////////////////////////////////////////////////////////////////////////////
325
326#[cfg(test)]
327mod tests {
328    use rstest::rstest;
329
330    use super::*;
331
332    #[rstest]
333    fn test_py_funding_rate_update_new() {
334        pyo3::prepare_freethreaded_python();
335
336        Python::with_gil(|_py| {
337            let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
338            let rate = Decimal::new(1, 4); // 0.0001
339            let ts_event = UnixNanos::from(1_640_000_000_000_000_000_u64);
340            let ts_init = UnixNanos::from(1_640_000_000_000_000_000_u64);
341
342            let funding_rate = FundingRateUpdate::py_new(
343                instrument_id,
344                rate,
345                ts_event.as_u64(),
346                ts_init.as_u64(),
347                None,
348            );
349
350            assert_eq!(funding_rate.instrument_id, instrument_id);
351            assert_eq!(funding_rate.rate, rate);
352            assert_eq!(funding_rate.next_funding_ns, None);
353            assert_eq!(funding_rate.ts_event, ts_event);
354            assert_eq!(funding_rate.ts_init, ts_init);
355        });
356    }
357}