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]
52 pub fn new<'py, E>(py: Python<'py>) -> Self
53 where
54 E: strum::IntoEnumIterator + IntoPyObject<'py>,
55 <E as IntoEnumIterator>::Iterator: Send,
56 {
57 Self {
58 iter: Box::new(
59 E::iter()
60 .map(|var| var.into_py_any_unwrap(py))
61 .collect::<Vec<_>>()
63 .into_iter(),
64 ),
65 }
66 }
67}
68
69pub fn value_to_pydict(py: Python<'_>, val: &Value) -> PyResult<Py<PyAny>> {
70 let dict = PyDict::new(py);
71
72 match val {
73 Value::Object(map) => {
74 for (key, value) in map {
75 let py_value = value_to_pyobject(py, value)?;
76 dict.set_item(key, py_value)?;
77 }
78 }
79 _ => return Err(PyValueError::new_err("Expected JSON object")),
81 }
82
83 dict.into_py_any(py)
84}
85
86pub fn value_to_pyobject(py: Python<'_>, val: &Value) -> PyResult<PyObject> {
87 match val {
88 Value::Null => Ok(py.None()),
89 Value::Bool(b) => b.into_py_any(py),
90 Value::String(s) => s.into_py_any(py),
91 Value::Number(n) => {
92 if n.is_i64() {
93 n.as_i64().unwrap().into_py_any(py)
94 } else if n.is_f64() {
95 n.as_f64().unwrap().into_py_any(py)
96 } else {
97 Err(PyValueError::new_err("Unsupported JSON number type"))
98 }
99 }
100 Value::Array(arr) => {
101 let py_list = PyList::new(py, &[] as &[PyObject]).expect("Invalid `ExactSizeIterator`");
102 for item in arr {
103 let py_item = value_to_pyobject(py, item)?;
104 py_list.append(py_item)?;
105 }
106 py_list.into_py_any(py)
107 }
108 Value::Object(_) => value_to_pydict(py, val),
109 }
110}
111
112pub fn commissions_from_vec(py: Python<'_>, commissions: Vec<Money>) -> PyResult<Bound<'_, PyAny>> {
113 let mut values = Vec::new();
114
115 for value in commissions {
116 values.push(value.to_string());
117 }
118
119 if values.is_empty() {
120 Ok(PyNone::get(py).to_owned().into_any())
121 } else {
122 values.sort();
123 Ok(PyList::new(py, &values).unwrap().into_any())
125 }
126}
127
128pub fn commissions_from_indexmap(
129 py: Python<'_>,
130 commissions: IndexMap<Currency, Money>,
131) -> PyResult<Bound<'_, PyAny>> {
132 commissions_from_vec(py, commissions.values().cloned().collect())
133}
134
135#[cfg(test)]
136mod tests {
137 use pyo3::{
138 prelude::*,
139 prepare_freethreaded_python,
140 types::{PyBool, PyInt, PyString},
141 };
142 use rstest::rstest;
143 use serde_json::Value;
144
145 use super::*;
146
147 #[rstest]
148 fn test_value_to_pydict() {
149 prepare_freethreaded_python();
150 Python::with_gil(|py| {
151 let json_str = r#"
152 {
153 "type": "OrderAccepted",
154 "ts_event": 42,
155 "is_reconciliation": false
156 }
157 "#;
158
159 let val: Value = serde_json::from_str(json_str).unwrap();
160 let py_dict_ref = value_to_pydict(py, &val).unwrap();
161 let py_dict = py_dict_ref.bind(py);
162
163 assert_eq!(
164 py_dict
165 .get_item("type")
166 .unwrap()
167 .downcast::<PyString>()
168 .unwrap()
169 .to_str()
170 .unwrap(),
171 "OrderAccepted"
172 );
173 assert_eq!(
174 py_dict
175 .get_item("ts_event")
176 .unwrap()
177 .downcast::<PyInt>()
178 .unwrap()
179 .extract::<i64>()
180 .unwrap(),
181 42
182 );
183 assert!(
184 !py_dict
185 .get_item("is_reconciliation")
186 .unwrap()
187 .downcast::<PyBool>()
188 .unwrap()
189 .is_true()
190 );
191 });
192 }
193
194 #[rstest]
195 fn test_value_to_pyobject_string() {
196 prepare_freethreaded_python();
197 Python::with_gil(|py| {
198 let val = Value::String("Hello, world!".to_string());
199 let py_obj = value_to_pyobject(py, &val).unwrap();
200
201 assert_eq!(py_obj.extract::<&str>(py).unwrap(), "Hello, world!");
202 });
203 }
204
205 #[rstest]
206 fn test_value_to_pyobject_bool() {
207 prepare_freethreaded_python();
208 Python::with_gil(|py| {
209 let val = Value::Bool(true);
210 let py_obj = value_to_pyobject(py, &val).unwrap();
211
212 assert!(py_obj.extract::<bool>(py).unwrap());
213 });
214 }
215
216 #[rstest]
217 fn test_value_to_pyobject_array() {
218 prepare_freethreaded_python();
219 Python::with_gil(|py| {
220 let val = Value::Array(vec![
221 Value::String("item1".to_string()),
222 Value::String("item2".to_string()),
223 ]);
224 let binding = value_to_pyobject(py, &val).unwrap();
225 let py_list: &Bound<'_, PyList> = binding.bind(py).downcast::<PyList>().unwrap();
226
227 assert_eq!(py_list.len(), 2);
228 assert_eq!(
229 py_list.get_item(0).unwrap().extract::<&str>().unwrap(),
230 "item1"
231 );
232 assert_eq!(
233 py_list.get_item(1).unwrap().extract::<&str>().unwrap(),
234 "item2"
235 );
236 });
237 }
238}