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