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#[allow(missing_debug_implementations)]
33#[pyclass]
34pub struct EnumIterator {
35    // Type erasure for code reuse, generic types can't be exposed to Python
36    iter: Box<dyn Iterator<Item = PyObject> + Send + Sync>,
37}
38
39#[pymethods]
40impl EnumIterator {
41    fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
42        slf
43    }
44
45    fn __next__(mut slf: PyRefMut<'_, Self>) -> Option<PyObject> {
46        slf.iter.next()
47    }
48}
49
50impl EnumIterator {
51    #[must_use]
52    pub fn new<'py, E>(py: Python<'py>) -> Self
53    where
54        E: strum::IntoEnumIterator + IntoPyObject<'py>,
55        <E as IntoEnumIterator>::Iterator: Send,
56    {
57        Self {
58            iter: Box::new(
59                E::iter()
60                    .map(|var| var.into_py_any_unwrap(py))
61                    // Force eager evaluation because `py` isn't `Send`
62                    .collect::<Vec<_>>()
63                    .into_iter(),
64            ),
65        }
66    }
67}
68
69pub fn value_to_pydict(py: Python<'_>, val: &Value) -> PyResult<Py<PyAny>> {
70    let dict = PyDict::new(py);
71
72    match val {
73        Value::Object(map) => {
74            for (key, value) in map {
75                let py_value = value_to_pyobject(py, value)?;
76                dict.set_item(key, py_value)?;
77            }
78        }
79        // This shouldn't be reached in this function, but we include it for completeness
80        _ => return Err(PyValueError::new_err("Expected JSON object")),
81    }
82
83    dict.into_py_any(py)
84}
85
86pub fn value_to_pyobject(py: Python<'_>, val: &Value) -> PyResult<PyObject> {
87    match val {
88        Value::Null => Ok(py.None()),
89        Value::Bool(b) => b.into_py_any(py),
90        Value::String(s) => s.into_py_any(py),
91        Value::Number(n) => {
92            if n.is_i64() {
93                n.as_i64().unwrap().into_py_any(py)
94            } else if n.is_f64() {
95                n.as_f64().unwrap().into_py_any(py)
96            } else {
97                Err(PyValueError::new_err("Unsupported JSON number type"))
98            }
99        }
100        Value::Array(arr) => {
101            let py_list = PyList::new(py, &[] as &[PyObject]).expect("Invalid `ExactSizeIterator`");
102            for item in arr {
103                let py_item = value_to_pyobject(py, item)?;
104                py_list.append(py_item)?;
105            }
106            py_list.into_py_any(py)
107        }
108        Value::Object(_) => value_to_pydict(py, val),
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!(
184                !py_dict
185                    .get_item("is_reconciliation")
186                    .unwrap()
187                    .downcast::<PyBool>()
188                    .unwrap()
189                    .is_true()
190            );
191        });
192    }
193
194    #[rstest]
195    fn test_value_to_pyobject_string() {
196        prepare_freethreaded_python();
197        Python::with_gil(|py| {
198            let val = Value::String("Hello, world!".to_string());
199            let py_obj = value_to_pyobject(py, &val).unwrap();
200
201            assert_eq!(py_obj.extract::<&str>(py).unwrap(), "Hello, world!");
202        });
203    }
204
205    #[rstest]
206    fn test_value_to_pyobject_bool() {
207        prepare_freethreaded_python();
208        Python::with_gil(|py| {
209            let val = Value::Bool(true);
210            let py_obj = value_to_pyobject(py, &val).unwrap();
211
212            assert!(py_obj.extract::<bool>(py).unwrap());
213        });
214    }
215
216    #[rstest]
217    fn test_value_to_pyobject_array() {
218        prepare_freethreaded_python();
219        Python::with_gil(|py| {
220            let val = Value::Array(vec![
221                Value::String("item1".to_string()),
222                Value::String("item2".to_string()),
223            ]);
224            let binding = value_to_pyobject(py, &val).unwrap();
225            let py_list: &Bound<'_, PyList> = binding.bind(py).downcast::<PyList>().unwrap();
226
227            assert_eq!(py_list.len(), 2);
228            assert_eq!(
229                py_list.get_item(0).unwrap().extract::<&str>().unwrap(),
230                "item1"
231            );
232            assert_eq!(
233                py_list.get_item(1).unwrap().extract::<&str>().unwrap(),
234                "item2"
235            );
236        });
237    }
238}