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 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/// Python iterator over the variants of an enum.
30#[pyclass]
31pub struct EnumIterator {
32    // Type erasure for code reuse, generic types can't be exposed to Python
33    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                    // Force eager evaluation because `py` isn't `Send`
59                    .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        // This shouldn't be reached in this function, but we include it for completeness
77        _ => 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        // SAFETY: Reasonable to expect `ExactSizeIterator` should be correctly implemented
124        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}