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