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::types::{PyBool, PyInt, PyString};
182 use rstest::rstest;
183 use serde_json::{Value, json};
184
185 use super::*;
186
187 #[derive(Debug, Clone, Copy)]
188 enum ExpectedNumber {
189 I64(i64),
190 U64(u64),
191 F64(f64),
192 }
193
194 #[rstest]
195 fn test_value_to_pydict() {
196 Python::initialize();
197 Python::attach(|py| {
198 let json_str = r#"
199 {
200 "type": "OrderAccepted",
201 "ts_event": 42,
202 "is_reconciliation": false
203 }
204 "#;
205
206 let val: Value = serde_json::from_str(json_str).unwrap();
207 let py_dict_ref = value_to_pydict(py, &val).unwrap();
208 let py_dict = py_dict_ref.bind(py);
209
210 assert_eq!(
211 py_dict
212 .get_item("type")
213 .unwrap()
214 .cast::<PyString>()
215 .unwrap()
216 .to_str()
217 .unwrap(),
218 "OrderAccepted"
219 );
220 assert_eq!(
221 py_dict
222 .get_item("ts_event")
223 .unwrap()
224 .cast::<PyInt>()
225 .unwrap()
226 .extract::<i64>()
227 .unwrap(),
228 42
229 );
230 assert!(
231 !py_dict
232 .get_item("is_reconciliation")
233 .unwrap()
234 .cast::<PyBool>()
235 .unwrap()
236 .is_true()
237 );
238 });
239 }
240
241 #[rstest]
242 #[case(json!(-100_i64), ExpectedNumber::I64(-100))]
243 #[case(json!(42_u64), ExpectedNumber::U64(42))]
244 #[case(json!(2.5_f64), ExpectedNumber::F64(2.5))]
245 fn test_value_to_pyobject_number_branches(
246 #[case] value: Value,
247 #[case] expected: ExpectedNumber,
248 ) {
249 Python::initialize();
250 Python::attach(|py| {
251 let py_obj = value_to_pyobject(py, &value).unwrap();
252
253 match expected {
254 ExpectedNumber::I64(expected) => {
255 assert_eq!(py_obj.extract::<i64>(py).unwrap(), expected);
256 }
257 ExpectedNumber::U64(expected) => {
258 assert_eq!(py_obj.extract::<u64>(py).unwrap(), expected);
259 }
260 ExpectedNumber::F64(expected) => {
261 let actual = py_obj.extract::<f64>(py).unwrap();
262 assert!((actual - expected).abs() < f64::EPSILON);
263 }
264 }
265 });
266 }
267
268 #[rstest]
269 fn test_value_to_pyobject_string() {
270 Python::initialize();
271 Python::attach(|py| {
272 let val = Value::String("Hello, world!".to_string());
273 let py_obj = value_to_pyobject(py, &val).unwrap();
274
275 assert_eq!(py_obj.extract::<&str>(py).unwrap(), "Hello, world!");
276 });
277 }
278
279 #[rstest]
280 fn test_value_to_pyobject_bool() {
281 Python::initialize();
282 Python::attach(|py| {
283 let val = Value::Bool(true);
284 let py_obj = value_to_pyobject(py, &val).unwrap();
285
286 assert!(py_obj.extract::<bool>(py).unwrap());
287 });
288 }
289
290 #[rstest]
291 fn test_value_to_pyobject_array() {
292 Python::initialize();
293 Python::attach(|py| {
294 let val = Value::Array(vec![
295 Value::String("item1".to_string()),
296 Value::String("item2".to_string()),
297 ]);
298 let binding = value_to_pyobject(py, &val).unwrap();
299 let py_list: &Bound<'_, PyList> = binding.bind(py).cast::<PyList>().unwrap();
300
301 assert_eq!(py_list.len(), 2);
302 assert_eq!(
303 py_list.get_item(0).unwrap().extract::<&str>().unwrap(),
304 "item1"
305 );
306 assert_eq!(
307 py_list.get_item(1).unwrap().extract::<&str>().unwrap(),
308 "item2"
309 );
310 });
311 }
312
313 #[rstest]
314 fn test_commissions_from_vec_empty_returns_none() {
315 Python::initialize();
316 Python::attach(|py| {
317 let value = commissions_from_vec(py, vec![]).unwrap();
318
319 assert!(value.is_none());
320 });
321 }
322
323 #[rstest]
324 fn test_commissions_from_vec_returns_sorted_list() {
325 Python::initialize();
326 Python::attach(|py| {
327 let value =
328 commissions_from_vec(py, vec![Money::from("2.00 USD"), Money::from("1.00 USD")])
329 .unwrap();
330 let py_list: &Bound<'_, PyList> = value.cast::<PyList>().unwrap();
331
332 assert_eq!(py_list.len(), 2);
333 assert_eq!(
334 py_list.get_item(0).unwrap().extract::<&str>().unwrap(),
335 "1.00 USD"
336 );
337 assert_eq!(
338 py_list.get_item(1).unwrap().extract::<&str>().unwrap(),
339 "2.00 USD"
340 );
341 });
342 }
343}