nautilus_model/python/
common.rs1use indexmap::IndexMap;
17use nautilus_core::python::IntoPyObjectNautilusExt;
18use pyo3::{
19 conversion::{IntoPyObject, IntoPyObjectExt},
20 exceptions::PyValueError,
21 prelude::*,
22 types::{PyDict, PyList, PyNone},
23};
24use serde_json::Value;
25use strum::IntoEnumIterator;
26
27use crate::types::{Currency, Money};
28
29pub const PY_MODULE_MODEL: &str = "nautilus_trader.core.nautilus_pyo3.model";
30
31#[allow(missing_debug_implementations)]
33#[pyclass]
34pub struct EnumIterator {
35 iter: Box<dyn Iterator<Item = PyObject> + Send + Sync>,
37}
38
39#[pymethods]
40impl EnumIterator {
41 fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
42 slf
43 }
44
45 fn __next__(mut slf: PyRefMut<'_, Self>) -> Option<PyObject> {
46 slf.iter.next()
47 }
48}
49
50impl EnumIterator {
51 #[must_use]
57 pub fn new<'py, E>(py: Python<'py>) -> Self
58 where
59 E: strum::IntoEnumIterator + IntoPyObject<'py>,
60 <E as IntoEnumIterator>::Iterator: Send,
61 {
62 Self {
63 iter: Box::new(
64 E::iter()
65 .map(|var| var.into_py_any_unwrap(py))
66 .collect::<Vec<_>>()
68 .into_iter(),
69 ),
70 }
71 }
72}
73
74pub fn value_to_pydict(py: Python<'_>, val: &Value) -> PyResult<Py<PyAny>> {
86 let dict = PyDict::new(py);
87
88 match val {
89 Value::Object(map) => {
90 for (key, value) in map {
91 let py_value = value_to_pyobject(py, value)?;
92 dict.set_item(key, py_value)?;
93 }
94 }
95 _ => return Err(PyValueError::new_err("Expected JSON object")),
97 }
98
99 dict.into_py_any(py)
100}
101
102pub fn value_to_pyobject(py: Python<'_>, val: &Value) -> PyResult<PyObject> {
114 match val {
115 Value::Null => Ok(py.None()),
116 Value::Bool(b) => b.into_py_any(py),
117 Value::String(s) => s.into_py_any(py),
118 Value::Number(n) => {
119 if n.is_i64() {
120 n.as_i64().unwrap().into_py_any(py)
121 } else if n.is_f64() {
122 n.as_f64().unwrap().into_py_any(py)
123 } else {
124 Err(PyValueError::new_err("Unsupported JSON number type"))
125 }
126 }
127 Value::Array(arr) => {
128 let py_list = PyList::new(py, &[] as &[PyObject]).expect("Invalid `ExactSizeIterator`");
129 for item in arr {
130 let py_item = value_to_pyobject(py, item)?;
131 py_list.append(py_item)?;
132 }
133 py_list.into_py_any(py)
134 }
135 Value::Object(_) => value_to_pydict(py, val),
136 }
137}
138
139pub fn commissions_from_vec(py: Python<'_>, commissions: Vec<Money>) -> PyResult<Bound<'_, PyAny>> {
149 let mut values = Vec::new();
150
151 for value in commissions {
152 values.push(value.to_string());
153 }
154
155 if values.is_empty() {
156 Ok(PyNone::get(py).to_owned().into_any())
157 } else {
158 values.sort();
159 Ok(PyList::new(py, &values).unwrap().into_any())
161 }
162}
163
164pub fn commissions_from_indexmap(
170 py: Python<'_>,
171 commissions: IndexMap<Currency, Money>,
172) -> PyResult<Bound<'_, PyAny>> {
173 commissions_from_vec(py, commissions.values().cloned().collect())
174}
175
176#[cfg(test)]
177mod tests {
178 use pyo3::{
179 prelude::*,
180 prepare_freethreaded_python,
181 types::{PyBool, PyInt, PyString},
182 };
183 use rstest::rstest;
184 use serde_json::Value;
185
186 use super::*;
187
188 #[rstest]
189 fn test_value_to_pydict() {
190 prepare_freethreaded_python();
191 Python::with_gil(|py| {
192 let json_str = r#"
193 {
194 "type": "OrderAccepted",
195 "ts_event": 42,
196 "is_reconciliation": false
197 }
198 "#;
199
200 let val: Value = serde_json::from_str(json_str).unwrap();
201 let py_dict_ref = value_to_pydict(py, &val).unwrap();
202 let py_dict = py_dict_ref.bind(py);
203
204 assert_eq!(
205 py_dict
206 .get_item("type")
207 .unwrap()
208 .downcast::<PyString>()
209 .unwrap()
210 .to_str()
211 .unwrap(),
212 "OrderAccepted"
213 );
214 assert_eq!(
215 py_dict
216 .get_item("ts_event")
217 .unwrap()
218 .downcast::<PyInt>()
219 .unwrap()
220 .extract::<i64>()
221 .unwrap(),
222 42
223 );
224 assert!(
225 !py_dict
226 .get_item("is_reconciliation")
227 .unwrap()
228 .downcast::<PyBool>()
229 .unwrap()
230 .is_true()
231 );
232 });
233 }
234
235 #[rstest]
236 fn test_value_to_pyobject_string() {
237 prepare_freethreaded_python();
238 Python::with_gil(|py| {
239 let val = Value::String("Hello, world!".to_string());
240 let py_obj = value_to_pyobject(py, &val).unwrap();
241
242 assert_eq!(py_obj.extract::<&str>(py).unwrap(), "Hello, world!");
243 });
244 }
245
246 #[rstest]
247 fn test_value_to_pyobject_bool() {
248 prepare_freethreaded_python();
249 Python::with_gil(|py| {
250 let val = Value::Bool(true);
251 let py_obj = value_to_pyobject(py, &val).unwrap();
252
253 assert!(py_obj.extract::<bool>(py).unwrap());
254 });
255 }
256
257 #[rstest]
258 fn test_value_to_pyobject_array() {
259 prepare_freethreaded_python();
260 Python::with_gil(|py| {
261 let val = Value::Array(vec![
262 Value::String("item1".to_string()),
263 Value::String("item2".to_string()),
264 ]);
265 let binding = value_to_pyobject(py, &val).unwrap();
266 let py_list: &Bound<'_, PyList> = binding.bind(py).downcast::<PyList>().unwrap();
267
268 assert_eq!(py_list.len(), 2);
269 assert_eq!(
270 py_list.get_item(0).unwrap().extract::<&str>().unwrap(),
271 "item1"
272 );
273 assert_eq!(
274 py_list.get_item(1).unwrap().extract::<&str>().unwrap(),
275 "item2"
276 );
277 });
278 }
279}