nautilus_model/python/data/
status.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use 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    /// Create a new [`InstrumentStatus`] extracted from the given [`PyAny`].
41    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    /// Returns a new object from the given dictionary representation.
192    #[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    /// Return a dictionary representation of the object.
217    #[pyo3(name = "as_dict")]
218    fn py_as_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
219        to_dict_pyo3(py, self)
220    }
221
222    /// Return JSON encoded bytes representation of the object.
223    #[pyo3(name = "as_json")]
224    fn py_as_json(&self, py: Python<'_>) -> Py<PyAny> {
225        // Unwrapping is safe when serializing a valid object
226        self.as_json_bytes().unwrap().into_py(py)
227    }
228
229    /// Return MsgPack encoded bytes representation of the object.
230    #[pyo3(name = "as_msgpack")]
231    fn py_as_msgpack(&self, py: Python<'_>) -> Py<PyAny> {
232        // Unwrapping is safe when serializing a valid object
233        self.as_msgpack_bytes().unwrap().into_py(py)
234    }
235}
236
237////////////////////////////////////////////////////////////////////////////////
238// Tests
239////////////////////////////////////////////////////////////////////////////////
240#[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}