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 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 #[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 #[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 #[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 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); 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}