Skip to main content

nautilus_model/python/instruments/
synthetic.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 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 std::collections::HashMap;
17
18use nautilus_core::python::{IntoPyObjectNautilusExt, to_pyvalue_err};
19use pyo3::{basic::CompareOp, prelude::*, types::PyDict};
20
21use crate::{
22    identifiers::{InstrumentId, Symbol},
23    instruments::SyntheticInstrument,
24    types::Price,
25};
26
27#[pymethods]
28#[pyo3_stub_gen::derive::gen_stub_pymethods]
29impl SyntheticInstrument {
30    /// Represents a synthetic instrument with prices derived from component instruments using a
31    /// formula.
32    ///
33    /// The `id` for the synthetic will become `{symbol}.{SYNTH}`.
34    #[new]
35    #[pyo3(signature = (symbol, price_precision, components, formula, ts_event, ts_init))]
36    fn py_new(
37        symbol: Symbol,
38        price_precision: u8,
39        components: Vec<InstrumentId>,
40        formula: &str,
41        ts_event: u64,
42        ts_init: u64,
43    ) -> PyResult<Self> {
44        Self::new_checked(
45            symbol,
46            price_precision,
47            components,
48            formula,
49            ts_event.into(),
50            ts_init.into(),
51        )
52        .map_err(to_pyvalue_err)
53    }
54
55    fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
56        match op {
57            CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
58            CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
59            _ => py.NotImplemented(),
60        }
61    }
62
63    #[getter]
64    #[pyo3(name = "id")]
65    fn py_id(&self) -> InstrumentId {
66        self.id
67    }
68
69    #[getter]
70    #[pyo3(name = "price_precision")]
71    fn py_price_precision(&self) -> u8 {
72        self.price_precision
73    }
74
75    #[getter]
76    #[pyo3(name = "price_increment")]
77    fn py_price_increment(&self) -> Price {
78        self.price_increment
79    }
80
81    #[getter]
82    #[pyo3(name = "components")]
83    fn py_components(&self) -> Vec<InstrumentId> {
84        self.components.clone()
85    }
86
87    #[getter]
88    #[pyo3(name = "formula")]
89    fn py_formula(&self) -> &str {
90        self.formula.as_str()
91    }
92
93    #[getter]
94    #[pyo3(name = "ts_event")]
95    fn py_ts_event(&self) -> u64 {
96        self.ts_event.as_u64()
97    }
98
99    #[getter]
100    #[pyo3(name = "ts_init")]
101    fn py_ts_init(&self) -> u64 {
102        self.ts_init.as_u64()
103    }
104
105    /// Returns whether the given formula compiles against this instrument's components.
106    #[pyo3(name = "is_valid_formula")]
107    fn py_is_valid_formula(&self, formula: &str) -> bool {
108        self.is_valid_formula(formula)
109    }
110
111    /// Replaces the derivation formula, recompiling it against the existing components.
112    ///
113    /// # Errors
114    ///
115    /// Returns an error if parsing the new formula fails.
116    #[pyo3(name = "change_formula")]
117    fn py_change_formula(&mut self, formula: &str) -> PyResult<()> {
118        self.change_formula(formula).map_err(to_pyvalue_err)
119    }
120
121    /// Calculates the price of the synthetic instrument based on the given component input prices
122    /// provided as an array of `f64` values.
123    ///
124    /// # Errors
125    ///
126    /// Returns an error if the input length does not match, any input is non-finite, or formula
127    /// evaluation fails.
128    #[pyo3(name = "calculate")]
129    #[expect(clippy::needless_pass_by_value)]
130    fn py_calculate(&mut self, inputs: Vec<f64>) -> PyResult<Price> {
131        self.calculate(&inputs).map_err(to_pyvalue_err)
132    }
133
134    /// Calculates the price of the synthetic instrument based on component input prices provided as a map.
135    #[pyo3(name = "calculate_from_map")]
136    fn py_calculate_from_map(
137        &mut self,
138        _py: Python<'_>,
139        inputs: &Bound<'_, PyDict>,
140    ) -> PyResult<Price> {
141        let mut map: HashMap<String, f64> = HashMap::new();
142        for (key, value) in inputs.iter() {
143            let k: String = key.extract()?;
144            let v: f64 = value.extract()?;
145            map.insert(k, v);
146        }
147        self.calculate_from_map(&map).map_err(to_pyvalue_err)
148    }
149}