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