nautilus_model/python/
common.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use 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/// Python iterator over the variants of an enum.
32#[pyclass]
33pub struct EnumIterator {
34    // Type erasure for code reuse, generic types can't be exposed to Python
35    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                    // Force eager evaluation because `py` isn't `Send`
61                    .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        // This shouldn't be reached in this function, but we include it for completeness
79        _ => 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        // SAFETY: Reasonable to expect `ExactSizeIterator` should be correctly implemented
123        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}