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