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