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