Skip to main content

nautilus_data/python/
config.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
16//! Python bindings for data engine configuration.
17
18use std::{collections::HashMap, time::Duration};
19
20use nautilus_core::{datetime::get_timezone, python::to_pyvalue_err};
21use nautilus_model::{
22    enums::{BarAggregation, BarIntervalType},
23    identifiers::ClientId,
24};
25use pyo3::{Py, PyAny, PyResult, Python, prelude::PyAnyMethods, pymethods};
26
27use crate::engine::config::DataEngineConfig;
28
29// Coerces a PyO3 input into `BarIntervalType`, accepting both the enum (modern
30// Rust surface) and the legacy Python v1 string form
31// (`"left-open"` / `"right-open"`), matching `LiveDataEngineConfig`.
32fn coerce_bar_interval_type(value: &Py<PyAny>) -> PyResult<BarIntervalType> {
33    Python::attach(|py| {
34        let bound = value.bind(py);
35        if let Ok(variant) = bound.extract::<BarIntervalType>() {
36            return Ok(variant);
37        }
38
39        let raw = bound.extract::<String>().map_err(|_| {
40            to_pyvalue_err("`time_bars_interval_type` must be a string or BarIntervalType")
41        })?;
42
43        match raw.to_ascii_uppercase().replace('-', "_").as_str() {
44            "LEFT_OPEN" => Ok(BarIntervalType::LeftOpen),
45            "RIGHT_OPEN" => Ok(BarIntervalType::RightOpen),
46            _ => Err(to_pyvalue_err(format!(
47                "invalid `time_bars_interval_type`: {raw:?} (expected 'left-open' or 'right-open')"
48            ))),
49        }
50    })
51}
52
53#[pyo3_stub_gen::derive::gen_stub_pymethods]
54#[pymethods]
55impl DataEngineConfig {
56    /// Configuration for `DataEngine` instances.
57    #[new]
58    #[expect(clippy::too_many_arguments)]
59    #[pyo3(signature = (
60        time_bars_build_with_no_updates = None,
61        time_bars_timestamp_on_close = None,
62        time_bars_skip_first_non_full_bar = None,
63        time_bars_interval_type = None,
64        time_bars_build_delay = None,
65        time_bars_origin_offset = None,
66        validate_data_sequence = None,
67        buffer_deltas = None,
68        emit_quotes_from_book = None,
69        emit_quotes_from_book_depths = None,
70        external_clients = None,
71        debug = None,
72        disable_historical_cache = None,
73        time_bars_time_zone = None,
74    ))]
75    fn py_new(
76        time_bars_build_with_no_updates: Option<bool>,
77        time_bars_timestamp_on_close: Option<bool>,
78        time_bars_skip_first_non_full_bar: Option<bool>,
79        time_bars_interval_type: Option<Py<PyAny>>,
80        time_bars_build_delay: Option<u64>,
81        time_bars_origin_offset: Option<HashMap<BarAggregation, u64>>,
82        validate_data_sequence: Option<bool>,
83        buffer_deltas: Option<bool>,
84        emit_quotes_from_book: Option<bool>,
85        emit_quotes_from_book_depths: Option<bool>,
86        external_clients: Option<Vec<ClientId>>,
87        debug: Option<bool>,
88        disable_historical_cache: Option<bool>,
89        time_bars_time_zone: Option<String>,
90    ) -> PyResult<Self> {
91        if let Some(name) = &time_bars_time_zone {
92            get_timezone(name).map_err(to_pyvalue_err)?;
93        }
94
95        let time_bars_interval_type = match time_bars_interval_type {
96            Some(value) => Some(coerce_bar_interval_type(&value)?),
97            None => None,
98        };
99        let time_bars_origin_offset = time_bars_origin_offset.map(|map| {
100            map.into_iter()
101                .map(|(agg, nanos)| (agg, Duration::from_nanos(nanos)))
102                .collect()
103        });
104        Ok(Self::builder()
105            .maybe_time_bars_build_with_no_updates(time_bars_build_with_no_updates)
106            .maybe_time_bars_timestamp_on_close(time_bars_timestamp_on_close)
107            .maybe_time_bars_skip_first_non_full_bar(time_bars_skip_first_non_full_bar)
108            .maybe_time_bars_interval_type(time_bars_interval_type)
109            .maybe_time_bars_build_delay(time_bars_build_delay)
110            .maybe_time_bars_origin_offset(time_bars_origin_offset)
111            .maybe_validate_data_sequence(validate_data_sequence)
112            .maybe_buffer_deltas(buffer_deltas)
113            .maybe_emit_quotes_from_book(emit_quotes_from_book)
114            .maybe_emit_quotes_from_book_depths(emit_quotes_from_book_depths)
115            .maybe_disable_historical_cache(disable_historical_cache)
116            .maybe_time_bars_time_zone(time_bars_time_zone)
117            .maybe_external_clients(external_clients)
118            .maybe_debug(debug)
119            .build())
120    }
121
122    #[getter]
123    #[pyo3(name = "time_bars_build_with_no_updates")]
124    const fn py_time_bars_build_with_no_updates(&self) -> bool {
125        self.time_bars_build_with_no_updates
126    }
127
128    #[getter]
129    #[pyo3(name = "time_bars_timestamp_on_close")]
130    const fn py_time_bars_timestamp_on_close(&self) -> bool {
131        self.time_bars_timestamp_on_close
132    }
133
134    #[getter]
135    #[pyo3(name = "time_bars_skip_first_non_full_bar")]
136    const fn py_time_bars_skip_first_non_full_bar(&self) -> bool {
137        self.time_bars_skip_first_non_full_bar
138    }
139
140    #[getter]
141    #[pyo3(name = "time_bars_interval_type")]
142    const fn py_time_bars_interval_type(&self) -> BarIntervalType {
143        self.time_bars_interval_type
144    }
145
146    #[getter]
147    #[pyo3(name = "time_bars_build_delay")]
148    const fn py_time_bars_build_delay(&self) -> u64 {
149        self.time_bars_build_delay
150    }
151
152    #[getter]
153    #[pyo3(name = "time_bars_origin_offset")]
154    fn py_time_bars_origin_offset(&self) -> HashMap<BarAggregation, u64> {
155        self.time_bars_origin_offset
156            .iter()
157            .map(|(aggregation, offset)| (*aggregation, offset.as_nanos() as u64))
158            .collect()
159    }
160
161    #[getter]
162    #[pyo3(name = "validate_data_sequence")]
163    const fn py_validate_data_sequence(&self) -> bool {
164        self.validate_data_sequence
165    }
166
167    #[getter]
168    #[pyo3(name = "buffer_deltas")]
169    const fn py_buffer_deltas(&self) -> bool {
170        self.buffer_deltas
171    }
172
173    #[getter]
174    #[pyo3(name = "emit_quotes_from_book")]
175    const fn py_emit_quotes_from_book(&self) -> bool {
176        self.emit_quotes_from_book
177    }
178
179    #[getter]
180    #[pyo3(name = "emit_quotes_from_book_depths")]
181    const fn py_emit_quotes_from_book_depths(&self) -> bool {
182        self.emit_quotes_from_book_depths
183    }
184
185    #[getter]
186    #[pyo3(name = "disable_historical_cache")]
187    const fn py_disable_historical_cache(&self) -> bool {
188        self.disable_historical_cache
189    }
190
191    #[getter]
192    #[pyo3(name = "external_clients")]
193    fn py_external_clients(&self) -> Option<Vec<ClientId>> {
194        self.external_clients.clone()
195    }
196
197    #[getter]
198    #[pyo3(name = "debug")]
199    const fn py_debug(&self) -> bool {
200        self.debug
201    }
202
203    fn __repr__(&self) -> String {
204        format!("{self:?}")
205    }
206
207    fn __str__(&self) -> String {
208        format!("{self:?}")
209    }
210    #[getter]
211    #[pyo3(name = "time_bars_time_zone")]
212    fn py_time_bars_time_zone(&self) -> Option<String> {
213        self.time_bars_time_zone.clone()
214    }
215}