nautilus_model/python/
common.rs1use indexmap::IndexMap;
17use pyo3::{
18 exceptions::PyValueError,
19 prelude::*,
20 types::{PyDict, PyList, PyNone},
21};
22use serde_json::Value;
23use strum::IntoEnumIterator;
24
25use crate::types::{Currency, Money};
26
27pub const PY_MODULE_MODEL: &str = "nautilus_trader.core.nautilus_pyo3.model";
28
29#[pyclass]
31pub struct EnumIterator {
32 iter: Box<dyn Iterator<Item = PyObject> + Send + Sync>,
34}
35
36#[pymethods]
37impl EnumIterator {
38 fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
39 slf
40 }
41
42 fn __next__(mut slf: PyRefMut<'_, Self>) -> Option<PyObject> {
43 slf.iter.next()
44 }
45}
46
47impl EnumIterator {
48 #[must_use]
49 pub fn new<E>(py: Python<'_>) -> Self
50 where
51 E: strum::IntoEnumIterator + IntoPy<Py<PyAny>>,
52 <E as IntoEnumIterator>::Iterator: Send,
53 {
54 Self {
55 iter: Box::new(
56 E::iter()
57 .map(|var| var.into_py(py))
58 .collect::<Vec<_>>()
60 .into_iter(),
61 ),
62 }
63 }
64}
65
66pub fn value_to_pydict(py: Python<'_>, val: &Value) -> PyResult<Py<PyAny>> {
67 let dict = PyDict::new(py);
68
69 match val {
70 Value::Object(map) => {
71 for (key, value) in map {
72 let py_value = value_to_pyobject(py, value)?;
73 dict.set_item(key, py_value)?;
74 }
75 }
76 _ => return Err(PyValueError::new_err("Expected JSON object")),
78 }
79
80 Ok(dict.into_py(py))
81}
82
83pub fn value_to_pyobject(py: Python<'_>, val: &Value) -> PyResult<PyObject> {
84 match val {
85 Value::Null => Ok(py.None()),
86 Value::Bool(b) => Ok(b.into_py(py)),
87 Value::String(s) => Ok(s.into_py(py)),
88 Value::Number(n) => {
89 if n.is_i64() {
90 Ok(n.as_i64().unwrap().into_py(py))
91 } else if n.is_f64() {
92 Ok(n.as_f64().unwrap().into_py(py))
93 } else {
94 Err(PyValueError::new_err("Unsupported JSON number type"))
95 }
96 }
97 Value::Array(arr) => {
98 let py_list = PyList::new(py, &[] as &[PyObject]).expect("Invalid `ExactSizeIterator`");
99 for item in arr {
100 let py_item = value_to_pyobject(py, item)?;
101 py_list.append(py_item)?;
102 }
103 Ok(py_list.into())
104 }
105 Value::Object(_) => {
106 let py_dict = value_to_pydict(py, val)?;
107 Ok(py_dict)
108 }
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!(!py_dict
184 .get_item("is_reconciliation")
185 .unwrap()
186 .downcast::<PyBool>()
187 .unwrap()
188 .is_true());
189 });
190 }
191
192 #[rstest]
193 fn test_value_to_pyobject_string() {
194 prepare_freethreaded_python();
195 Python::with_gil(|py| {
196 let val = Value::String("Hello, world!".to_string());
197 let py_obj = value_to_pyobject(py, &val).unwrap();
198
199 assert_eq!(py_obj.extract::<&str>(py).unwrap(), "Hello, world!");
200 });
201 }
202
203 #[rstest]
204 fn test_value_to_pyobject_bool() {
205 prepare_freethreaded_python();
206 Python::with_gil(|py| {
207 let val = Value::Bool(true);
208 let py_obj = value_to_pyobject(py, &val).unwrap();
209
210 assert!(py_obj.extract::<bool>(py).unwrap());
211 });
212 }
213
214 #[rstest]
215 fn test_value_to_pyobject_array() {
216 prepare_freethreaded_python();
217 Python::with_gil(|py| {
218 let val = Value::Array(vec![
219 Value::String("item1".to_string()),
220 Value::String("item2".to_string()),
221 ]);
222 let binding = value_to_pyobject(py, &val).unwrap();
223 let py_list: &Bound<'_, PyList> = binding.bind(py).downcast::<PyList>().unwrap();
224
225 assert_eq!(py_list.len(), 2);
226 assert_eq!(
227 py_list.get_item(0).unwrap().extract::<&str>().unwrap(),
228 "item1"
229 );
230 assert_eq!(
231 py_list.get_item(1).unwrap().extract::<&str>().unwrap(),
232 "item2"
233 );
234 });
235 }
236}