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