Skip to main content

nautilus_testkit/testers/data/
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
16use nautilus_common::{
17    actor::DataActorConfig,
18    config::{ConfigError, ConfigErrorCollector, ConfigResult},
19};
20use nautilus_core::Params;
21use nautilus_model::{
22    data::bar::BarType,
23    enums::BookType,
24    identifiers::{ClientId, InstrumentId},
25};
26use serde::{Deserialize, Serialize};
27
28/// Configuration for the data tester actor.
29#[derive(Debug, Clone, Deserialize, Serialize, bon::Builder)]
30#[builder(finish_fn(name = build_inner, vis = ""))]
31#[serde(default, deny_unknown_fields)]
32#[cfg_attr(
33    feature = "python",
34    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.testkit", from_py_object)
35)]
36#[cfg_attr(
37    feature = "python",
38    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.testkit")
39)]
40#[expect(
41    clippy::struct_excessive_bools,
42    reason = "tester configuration exposes independent scenario toggles"
43)]
44#[allow(
45    clippy::unsafe_derive_deserialize,
46    reason = "config type deserializes plain field values; unsafe PyO3 methods are unrelated"
47)]
48pub struct DataTesterConfig {
49    /// Base data actor configuration.
50    #[builder(default)]
51    pub base: DataActorConfig,
52    /// Instrument IDs to subscribe to.
53    #[builder(default)]
54    pub instrument_ids: Vec<InstrumentId>,
55    /// Client ID to use for subscriptions.
56    pub client_id: Option<ClientId>,
57    /// Bar types to subscribe to.
58    pub bar_types: Option<Vec<BarType>>,
59    /// Whether to subscribe to order book deltas.
60    #[builder(default = false)]
61    pub subscribe_book_deltas: bool,
62    /// Whether to subscribe to order book depth snapshots.
63    #[builder(default = false)]
64    pub subscribe_book_depth: bool,
65    /// Whether to subscribe to order book at interval.
66    #[builder(default = false)]
67    pub subscribe_book_at_interval: bool,
68    /// Whether to subscribe to quotes.
69    #[builder(default = false)]
70    pub subscribe_quotes: bool,
71    /// Whether to subscribe to trades.
72    #[builder(default = false)]
73    pub subscribe_trades: bool,
74    /// Whether to subscribe to mark prices.
75    #[builder(default = false)]
76    pub subscribe_mark_prices: bool,
77    /// Whether to subscribe to index prices.
78    #[builder(default = false)]
79    pub subscribe_index_prices: bool,
80    /// Whether to subscribe to funding rates.
81    #[builder(default = false)]
82    pub subscribe_funding_rates: bool,
83    /// Whether to subscribe to bars.
84    #[builder(default = false)]
85    pub subscribe_bars: bool,
86    /// Whether to subscribe to instrument updates.
87    #[builder(default = false)]
88    pub subscribe_instrument: bool,
89    /// Whether to subscribe to instrument status.
90    #[builder(default = false)]
91    pub subscribe_instrument_status: bool,
92    /// Whether to subscribe to instrument close.
93    #[builder(default = false)]
94    pub subscribe_instrument_close: bool,
95    /// Whether to subscribe to option greeks.
96    #[builder(default = false)]
97    pub subscribe_option_greeks: bool,
98    /// Optional parameters passed to all subscribe calls.
99    pub subscribe_params: Option<Params>,
100    /// Optional parameters passed to all request calls.
101    pub request_params: Option<Params>,
102    /// Whether unsubscribe is supported on stop.
103    #[builder(default = true)]
104    pub can_unsubscribe: bool,
105    /// Whether to request instruments on start.
106    #[builder(default = false)]
107    pub request_instruments: bool,
108    /// Whether to request historical quotes.
109    #[builder(default = false)]
110    pub request_quotes: bool,
111    // TODO: Support request_trades when historical data requests are available
112    /// Whether to request historical trades (not yet implemented).
113    #[builder(default = false)]
114    pub request_trades: bool,
115    /// Whether to request historical bars.
116    #[builder(default = false)]
117    pub request_bars: bool,
118    /// Whether to request order book snapshots.
119    #[builder(default = false)]
120    pub request_book_snapshot: bool,
121    // TODO: Support request_book_deltas when Rust data engine has RequestBookDeltas
122    /// Whether to request historical order book deltas (not yet implemented).
123    #[builder(default = false)]
124    pub request_book_deltas: bool,
125    /// Whether to request historical funding rates.
126    #[builder(default = false)]
127    pub request_funding_rates: bool,
128    // TODO: Support requests_start_delta when we implement historical data requests
129    /// Book type for order book subscriptions.
130    #[builder(default = BookType::L2_MBP)]
131    pub book_type: BookType,
132    /// Order book depth for subscriptions.
133    pub book_depth: Option<usize>,
134    // TODO: Support book_group_size when order book grouping is implemented
135    /// Order book interval in milliseconds for `at_interval` subscriptions.
136    #[builder(default = 1000)]
137    pub book_interval_ms: usize,
138    /// Number of order book levels to print when logging.
139    #[builder(default = 10)]
140    pub book_levels_to_print: usize,
141    /// Whether to manage local order book from deltas.
142    #[builder(default = true)]
143    pub manage_book: bool,
144    /// Whether to log received data.
145    #[builder(default = true)]
146    pub log_data: bool,
147    /// Stats logging interval in seconds (0 to disable).
148    #[builder(default = 5)]
149    pub stats_interval_secs: u64,
150}
151
152impl<S: data_tester_config_builder::IsComplete> DataTesterConfigBuilder<S> {
153    /// Validates and builds the [`DataTesterConfig`].
154    ///
155    /// # Errors
156    ///
157    /// Returns a [`ConfigError`] if any field fails validation
158    /// (see [`DataTesterConfig::validate`]).
159    pub fn build(self) -> ConfigResult<DataTesterConfig> {
160        let config = self.build_inner();
161        config.validate()?;
162        Ok(config)
163    }
164}
165
166impl DataTesterConfig {
167    /// Validates the data tester configuration, collecting every field violation.
168    ///
169    /// # Errors
170    ///
171    /// Returns a [`ConfigError`] (a [`ConfigError::Multiple`] when more than one field is
172    /// invalid) if any field fails validation.
173    pub fn validate(&self) -> ConfigResult<()> {
174        let mut errors = ConfigErrorCollector::new();
175
176        errors.check(
177            self.book_interval_ms > 0,
178            ConfigError::range("book_interval_ms", "must be positive, was 0"),
179        );
180
181        if let Some(book_depth) = self.book_depth {
182            errors.check(
183                book_depth > 0,
184                ConfigError::range("book_depth", "must be positive, was 0"),
185            );
186        }
187
188        errors.into_result()
189    }
190
191    /// Creates a new [`DataTesterConfig`] instance with minimal settings.
192    #[must_use]
193    pub fn new(client_id: ClientId, instrument_ids: Vec<InstrumentId>) -> Self {
194        Self {
195            base: DataActorConfig::default(),
196            instrument_ids,
197            client_id: Some(client_id),
198            bar_types: None,
199            subscribe_book_deltas: false,
200            subscribe_book_depth: false,
201            subscribe_book_at_interval: false,
202            subscribe_quotes: false,
203            subscribe_trades: false,
204            subscribe_mark_prices: false,
205            subscribe_index_prices: false,
206            subscribe_funding_rates: false,
207            subscribe_bars: false,
208
209            subscribe_instrument: false,
210            subscribe_instrument_status: false,
211            subscribe_instrument_close: false,
212            subscribe_option_greeks: false,
213            subscribe_params: None,
214            request_params: None,
215            can_unsubscribe: true,
216            request_instruments: false,
217            request_quotes: false,
218            request_trades: false,
219            request_bars: false,
220            request_book_snapshot: false,
221            request_book_deltas: false,
222            request_funding_rates: false,
223            book_type: BookType::L2_MBP,
224            book_depth: None,
225            book_interval_ms: 1000,
226            book_levels_to_print: 10,
227            manage_book: true,
228            log_data: true,
229            stats_interval_secs: 5,
230        }
231    }
232}
233
234impl Default for DataTesterConfig {
235    fn default() -> Self {
236        Self::builder()
237            .build()
238            .expect("default DataTesterConfig should be valid")
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use rstest::rstest;
245
246    use super::*;
247
248    #[rstest]
249    fn test_default_config_is_valid() {
250        assert!(DataTesterConfig::builder().build().is_ok());
251    }
252
253    #[rstest]
254    fn test_zero_book_interval_ms_rejected() {
255        let result = DataTesterConfig::builder().book_interval_ms(0).build();
256        assert!(
257            matches!(result, Err(ConfigError::Range { field, .. }) if field == "book_interval_ms")
258        );
259    }
260
261    #[rstest]
262    fn test_zero_book_depth_rejected() {
263        let result = DataTesterConfig::builder().book_depth(0).build();
264        assert!(matches!(result, Err(ConfigError::Range { field, .. }) if field == "book_depth"));
265    }
266}