Skip to main content

nautilus_model/python/data/
funding.rs

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