nautilus_model/python/data/
funding.rs1use 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 #[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 #[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 #[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); 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}