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