nautilus_model/python/
common.rs1use indexmap::IndexMap;
17use nautilus_core::python::{IntoPyObjectNautilusExt, to_pyvalue_err};
18use pyo3::{
19 conversion::IntoPyObjectExt,
20 prelude::*,
21 types::{PyDict, PyList, PyNone},
22};
23use serde_json::Value;
24use strum::IntoEnumIterator;
25
26use crate::types::{Currency, Money};
27
28pub const PY_MODULE_MODEL: &str = "nautilus_trader.core.nautilus_pyo3.model";
29
30#[allow(missing_debug_implementations)]
32#[pyclass]
33#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")]
34pub struct EnumIterator {
35 iter: Box<dyn Iterator<Item = Py<PyAny>> + Send + Sync>,
37}
38
39#[pymethods]
40#[pyo3_stub_gen::derive::gen_stub_pymethods]
41impl EnumIterator {
42 fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
43 slf
44 }
45
46 fn __next__(mut slf: PyRefMut<'_, Self>) -> Option<Py<PyAny>> {
47 slf.iter.next()
48 }
49}
50
51impl EnumIterator {
52 #[must_use]
58 pub fn new<'py, E>(py: Python<'py>) -> Self
59 where
60 E: strum::IntoEnumIterator + IntoPyObjectExt<'py>,
61 <E as IntoEnumIterator>::Iterator: Send,
62 {
63 Self {
64 iter: Box::new(
65 E::iter()
66 .map(|var| var.into_py_any_unwrap(py))
67 .collect::<Vec<_>>()
69 .into_iter(),
70 ),
71 }
72 }
73}
74
75pub fn value_to_pydict(py: Python<'_>, val: &Value) -> PyResult<Py<PyAny>> {
83 let dict = PyDict::new(py);
84
85 match val {
86 Value::Object(map) => {
87 for (key, value) in map {
88 let py_value = value_to_pyobject(py, value)?;
89 dict.set_item(key, py_value)?;
90 }
91 }
92 _ => return Err(to_pyvalue_err("Expected JSON object")),
94 }
95
96 dict.into_py_any(py)
97}
98
99pub fn value_to_pyobject(py: Python<'_>, val: &Value) -> PyResult<Py<PyAny>> {
108 match val {
109 Value::Null => Ok(py.None()),
110 Value::Bool(b) => b.into_py_any(py),
111 Value::String(s) => s.into_py_any(py),
112 Value::Number(n) => {
113 if n.is_i64() {
114 n.as_i64()
115 .ok_or_else(|| to_pyvalue_err("JSON number could not be read as i64"))?
116 .into_py_any(py)
117 } else if n.is_u64() {
118 n.as_u64()
119 .ok_or_else(|| to_pyvalue_err("JSON number could not be read as u64"))?
120 .into_py_any(py)
121 } else if n.is_f64() {
122 n.as_f64()
123 .ok_or_else(|| to_pyvalue_err("JSON number could not be read as f64"))?
124 .into_py_any(py)
125 } else {
126 Err(to_pyvalue_err("Unsupported JSON number type"))
127 }
128 }
129 Value::Array(arr) => {
130 let py_list = PyList::new(py, &[] as &[Py<PyAny>])?;
131 for item in arr {
132 let py_item = value_to_pyobject(py, item)?;
133 py_list.append(py_item)?;
134 }
135 py_list.into_py_any(py)
136 }
137 Value::Object(_) => value_to_pydict(py, val),
138 }
139}
140
141pub use nautilus_core::{
144 from_pydict as pydict_to_params, from_pydict, python::params::params_to_pydict,
145};
146
147pub fn commissions_from_vec(py: Python<'_>, commissions: Vec<Money>) -> PyResult<Bound<'_, PyAny>> {
153 let mut values = Vec::new();
154
155 for value in commissions {
156 values.push(value.to_string());
157 }
158
159 if values.is_empty() {
160 Ok(PyNone::get(py).to_owned().into_any())
161 } else {
162 values.sort();
163 Ok(PyList::new(py, &values)?.into_any())
164 }
165}
166
167pub fn commissions_from_indexmap<'py>(
173 py: Python<'py>,
174 commissions: &IndexMap<Currency, Money>,
175) -> PyResult<Bound<'py, PyAny>> {
176 commissions_from_vec(py, commissions.values().copied().collect())
177}
178
179#[cfg(test)]
180mod tests {
181 use pyo3::{
182 prelude::*,
183 types::{PyBool, PyInt, PyString},
184 };
185 use rstest::rstest;
186 use serde_json::{Value, json};
187
188 use super::*;
189
190 #[derive(Debug, Clone, Copy)]
191 enum ExpectedNumber {
192 I64(i64),
193 U64(u64),
194 F64(f64),
195 }
196
197 #[rstest]
198 fn test_value_to_pydict() {
199 Python::initialize();
200 Python::attach(|py| {
201 let json_str = r#"
202 {
203 "type": "OrderAccepted",
204 "ts_event": 42,
205 "is_reconciliation": false
206 }
207 "#;
208
209 let val: Value = serde_json::from_str(json_str).unwrap();
210 let py_dict_ref = value_to_pydict(py, &val).unwrap();
211 let py_dict = py_dict_ref.bind(py);
212
213 assert_eq!(
214 py_dict
215 .get_item("type")
216 .unwrap()
217 .cast::<PyString>()
218 .unwrap()
219 .to_str()
220 .unwrap(),
221 "OrderAccepted"
222 );
223 assert_eq!(
224 py_dict
225 .get_item("ts_event")
226 .unwrap()
227 .cast::<PyInt>()
228 .unwrap()
229 .extract::<i64>()
230 .unwrap(),
231 42
232 );
233 assert!(
234 !py_dict
235 .get_item("is_reconciliation")
236 .unwrap()
237 .cast::<PyBool>()
238 .unwrap()
239 .is_true()
240 );
241 });
242 }
243
244 #[rstest]
245 #[case(json!(-100_i64), ExpectedNumber::I64(-100))]
246 #[case(json!(42_u64), ExpectedNumber::U64(42))]
247 #[case(json!(2.5_f64), ExpectedNumber::F64(2.5))]
248 fn test_value_to_pyobject_number_branches(
249 #[case] value: Value,
250 #[case] expected: ExpectedNumber,
251 ) {
252 Python::initialize();
253 Python::attach(|py| {
254 let py_obj = value_to_pyobject(py, &value).unwrap();
255
256 match expected {
257 ExpectedNumber::I64(expected) => {
258 assert_eq!(py_obj.extract::<i64>(py).unwrap(), expected);
259 }
260 ExpectedNumber::U64(expected) => {
261 assert_eq!(py_obj.extract::<u64>(py).unwrap(), expected);
262 }
263 ExpectedNumber::F64(expected) => {
264 let actual = py_obj.extract::<f64>(py).unwrap();
265 assert!((actual - expected).abs() < f64::EPSILON);
266 }
267 }
268 });
269 }
270
271 #[rstest]
272 fn test_value_to_pyobject_string() {
273 Python::initialize();
274 Python::attach(|py| {
275 let val = Value::String("Hello, world!".to_string());
276 let py_obj = value_to_pyobject(py, &val).unwrap();
277
278 assert_eq!(py_obj.extract::<&str>(py).unwrap(), "Hello, world!");
279 });
280 }
281
282 #[rstest]
283 fn test_value_to_pyobject_bool() {
284 Python::initialize();
285 Python::attach(|py| {
286 let val = Value::Bool(true);
287 let py_obj = value_to_pyobject(py, &val).unwrap();
288
289 assert!(py_obj.extract::<bool>(py).unwrap());
290 });
291 }
292
293 #[rstest]
294 fn test_value_to_pyobject_array() {
295 Python::initialize();
296 Python::attach(|py| {
297 let val = Value::Array(vec![
298 Value::String("item1".to_string()),
299 Value::String("item2".to_string()),
300 ]);
301 let binding = value_to_pyobject(py, &val).unwrap();
302 let py_list: &Bound<'_, PyList> = binding.bind(py).cast::<PyList>().unwrap();
303
304 assert_eq!(py_list.len(), 2);
305 assert_eq!(
306 py_list.get_item(0).unwrap().extract::<&str>().unwrap(),
307 "item1"
308 );
309 assert_eq!(
310 py_list.get_item(1).unwrap().extract::<&str>().unwrap(),
311 "item2"
312 );
313 });
314 }
315
316 #[rstest]
317 fn test_commissions_from_vec_empty_returns_none() {
318 Python::initialize();
319 Python::attach(|py| {
320 let value = commissions_from_vec(py, vec![]).unwrap();
321
322 assert!(value.is_none());
323 });
324 }
325
326 #[rstest]
327 fn test_commissions_from_vec_returns_sorted_list() {
328 Python::initialize();
329 Python::attach(|py| {
330 let value =
331 commissions_from_vec(py, vec![Money::from("2.00 USD"), Money::from("1.00 USD")])
332 .unwrap();
333 let py_list: &Bound<'_, PyList> = value.cast::<PyList>().unwrap();
334
335 assert_eq!(py_list.len(), 2);
336 assert_eq!(
337 py_list.get_item(0).unwrap().extract::<&str>().unwrap(),
338 "1.00 USD"
339 );
340 assert_eq!(
341 py_list.get_item(1).unwrap().extract::<&str>().unwrap(),
342 "2.00 USD"
343 );
344 });
345 }
346}