nautilus_model/python/data/
status.rs1use std::{
17 collections::{hash_map::DefaultHasher, HashMap},
18 hash::{Hash, Hasher},
19 str::FromStr,
20};
21
22use nautilus_core::{
23 python::{
24 serialization::{from_dict_pyo3, to_dict_pyo3},
25 to_pyvalue_err,
26 },
27 serialization::Serializable,
28};
29use pyo3::{prelude::*, pyclass::CompareOp, types::PyDict};
30use ustr::Ustr;
31
32use crate::{
33 data::status::InstrumentStatus,
34 enums::{FromU16, MarketStatusAction},
35 identifiers::InstrumentId,
36 python::common::PY_MODULE_MODEL,
37};
38
39impl InstrumentStatus {
40 pub fn from_pyobject(obj: &Bound<'_, PyAny>) -> PyResult<Self> {
42 let instrument_id_obj: Bound<'_, PyAny> = obj.getattr("instrument_id")?.extract()?;
43 let instrument_id_str: String = instrument_id_obj.getattr("value")?.extract()?;
44 let instrument_id =
45 InstrumentId::from_str(instrument_id_str.as_str()).map_err(to_pyvalue_err)?;
46
47 let action_obj: Bound<'_, PyAny> = obj.getattr("action")?.extract()?;
48 let action_u16: u16 = action_obj.getattr("value")?.extract()?;
49 let action = MarketStatusAction::from_u16(action_u16).unwrap();
50
51 let ts_event: u64 = obj.getattr("ts_event")?.extract()?;
52 let ts_init: u64 = obj.getattr("ts_init")?.extract()?;
53
54 let reason_str: Option<String> = obj.getattr("reason")?.extract()?;
55 let reason = reason_str.map(|reason_str| Ustr::from(&reason_str));
56
57 let trading_event_str: Option<String> = obj.getattr("trading_event")?.extract()?;
58 let trading_event =
59 trading_event_str.map(|trading_event_str| Ustr::from(&trading_event_str));
60
61 let is_trading: Option<bool> = obj.getattr("is_trading")?.extract()?;
62 let is_quoting: Option<bool> = obj.getattr("is_quoting")?.extract()?;
63 let is_short_sell_restricted: Option<bool> =
64 obj.getattr("is_short_sell_restricted")?.extract()?;
65
66 Ok(Self::new(
67 instrument_id,
68 action,
69 ts_event.into(),
70 ts_init.into(),
71 reason,
72 trading_event,
73 is_trading,
74 is_quoting,
75 is_short_sell_restricted,
76 ))
77 }
78}
79
80#[pymethods]
81impl InstrumentStatus {
82 #[new]
83 #[pyo3(signature = (instrument_id, action, ts_event, ts_init, reason=None, trading_event=None, is_trading=None, is_quoting=None, is_short_sell_restricted=None))]
84 #[allow(clippy::too_many_arguments)]
85 fn py_new(
86 instrument_id: InstrumentId,
87 action: MarketStatusAction,
88 ts_event: u64,
89 ts_init: u64,
90 reason: Option<String>,
91 trading_event: Option<String>,
92 is_trading: Option<bool>,
93 is_quoting: Option<bool>,
94 is_short_sell_restricted: Option<bool>,
95 ) -> Self {
96 Self::new(
97 instrument_id,
98 action,
99 ts_event.into(),
100 ts_init.into(),
101 reason.map(|s| Ustr::from(&s)),
102 trading_event.map(|s| Ustr::from(&s)),
103 is_trading,
104 is_quoting,
105 is_short_sell_restricted,
106 )
107 }
108
109 fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
110 match op {
111 CompareOp::Eq => self.eq(other).into_py(py),
112 CompareOp::Ne => self.ne(other).into_py(py),
113 _ => py.NotImplemented(),
114 }
115 }
116
117 fn __hash__(&self) -> isize {
118 let mut h = DefaultHasher::new();
119 self.hash(&mut h);
120 h.finish() as isize
121 }
122
123 fn __repr__(&self) -> String {
124 format!("{}({})", stringify!(InstrumentStatus), self)
125 }
126
127 fn __str__(&self) -> String {
128 self.to_string()
129 }
130
131 #[getter]
132 #[pyo3(name = "instrument_id")]
133 fn py_instrument_id(&self) -> InstrumentId {
134 self.instrument_id
135 }
136
137 #[getter]
138 #[pyo3(name = "action")]
139 fn py_action(&self) -> MarketStatusAction {
140 self.action
141 }
142
143 #[getter]
144 #[pyo3(name = "ts_event")]
145 fn py_ts_event(&self) -> u64 {
146 self.ts_event.as_u64()
147 }
148
149 #[getter]
150 #[pyo3(name = "ts_init")]
151 fn py_ts_init(&self) -> u64 {
152 self.ts_init.as_u64()
153 }
154
155 #[getter]
156 #[pyo3(name = "reason")]
157 fn py_reason(&self) -> Option<String> {
158 self.reason.map(|x| x.to_string())
159 }
160
161 #[getter]
162 #[pyo3(name = "trading_event")]
163 fn py_trading_event(&self) -> Option<String> {
164 self.trading_event.map(|x| x.to_string())
165 }
166
167 #[getter]
168 #[pyo3(name = "is_trading")]
169 fn py_is_trading(&self) -> Option<bool> {
170 self.is_trading
171 }
172
173 #[getter]
174 #[pyo3(name = "is_quoting")]
175 fn py_is_quoting(&self) -> Option<bool> {
176 self.is_quoting
177 }
178
179 #[getter]
180 #[pyo3(name = "is_short_sell_restricted")]
181 fn py_is_short_sell_restricted(&self) -> Option<bool> {
182 self.is_short_sell_restricted
183 }
184
185 #[staticmethod]
186 #[pyo3(name = "fully_qualified_name")]
187 fn py_fully_qualified_name() -> String {
188 format!("{}:{}", PY_MODULE_MODEL, stringify!(InstrumentStatus))
189 }
190
191 #[staticmethod]
193 #[pyo3(name = "from_dict")]
194 fn py_from_dict(py: Python<'_>, values: Py<PyDict>) -> PyResult<Self> {
195 from_dict_pyo3(py, values)
196 }
197
198 #[staticmethod]
199 #[pyo3(name = "get_metadata")]
200 fn py_get_metadata(instrument_id: &InstrumentId) -> PyResult<HashMap<String, String>> {
201 Ok(Self::get_metadata(instrument_id))
202 }
203
204 #[staticmethod]
205 #[pyo3(name = "from_json")]
206 fn py_from_json(data: Vec<u8>) -> PyResult<Self> {
207 Self::from_json_bytes(&data).map_err(to_pyvalue_err)
208 }
209
210 #[staticmethod]
211 #[pyo3(name = "from_msgpack")]
212 fn py_from_msgpack(data: Vec<u8>) -> PyResult<Self> {
213 Self::from_msgpack_bytes(&data).map_err(to_pyvalue_err)
214 }
215
216 #[pyo3(name = "as_dict")]
218 fn py_as_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
219 to_dict_pyo3(py, self)
220 }
221
222 #[pyo3(name = "as_json")]
224 fn py_as_json(&self, py: Python<'_>) -> Py<PyAny> {
225 self.as_json_bytes().unwrap().into_py(py)
227 }
228
229 #[pyo3(name = "as_msgpack")]
231 fn py_as_msgpack(&self, py: Python<'_>) -> Py<PyAny> {
232 self.as_msgpack_bytes().unwrap().into_py(py)
234 }
235}
236
237#[cfg(test)]
241mod tests {
242 use pyo3::{IntoPy, Python};
243 use rstest::rstest;
244
245 use crate::data::{status::InstrumentStatus, stubs::stub_instrument_status};
246
247 #[rstest]
248 fn test_as_dict(stub_instrument_status: InstrumentStatus) {
249 pyo3::prepare_freethreaded_python();
250
251 Python::with_gil(|py| {
252 let dict_string = stub_instrument_status.py_as_dict(py).unwrap().to_string();
253 let expected_string = r"{'type': 'InstrumentStatus', 'instrument_id': 'MSFT.XNAS', 'action': 'TRADING', 'ts_event': 1, 'ts_init': 2, 'reason': None, 'trading_event': None, 'is_trading': None, 'is_quoting': None, 'is_short_sell_restricted': None}";
254 assert_eq!(dict_string, expected_string);
255 });
256 }
257
258 #[rstest]
259 fn test_from_dict(stub_instrument_status: InstrumentStatus) {
260 pyo3::prepare_freethreaded_python();
261
262 Python::with_gil(|py| {
263 let dict = stub_instrument_status.py_as_dict(py).unwrap();
264 let parsed = InstrumentStatus::py_from_dict(py, dict).unwrap();
265 assert_eq!(parsed, stub_instrument_status);
266 });
267 }
268
269 #[rstest]
270 fn test_from_pyobject(stub_instrument_status: InstrumentStatus) {
271 pyo3::prepare_freethreaded_python();
272
273 Python::with_gil(|py| {
274 let status_pyobject = stub_instrument_status.into_py(py);
275 let parsed_status = InstrumentStatus::from_pyobject(status_pyobject.bind(py)).unwrap();
276 assert_eq!(parsed_status, stub_instrument_status);
277 });
278 }
279}