nautilus_testkit/testers/data/
config.rs1use 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#[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 #[builder(default)]
51 pub base: DataActorConfig,
52 #[builder(default)]
54 pub instrument_ids: Vec<InstrumentId>,
55 pub client_id: Option<ClientId>,
57 pub bar_types: Option<Vec<BarType>>,
59 #[builder(default = false)]
61 pub subscribe_book_deltas: bool,
62 #[builder(default = false)]
64 pub subscribe_book_depth: bool,
65 #[builder(default = false)]
67 pub subscribe_book_at_interval: bool,
68 #[builder(default = false)]
70 pub subscribe_quotes: bool,
71 #[builder(default = false)]
73 pub subscribe_trades: bool,
74 #[builder(default = false)]
76 pub subscribe_mark_prices: bool,
77 #[builder(default = false)]
79 pub subscribe_index_prices: bool,
80 #[builder(default = false)]
82 pub subscribe_funding_rates: bool,
83 #[builder(default = false)]
85 pub subscribe_bars: bool,
86 #[builder(default = false)]
88 pub subscribe_instrument: bool,
89 #[builder(default = false)]
91 pub subscribe_instrument_status: bool,
92 #[builder(default = false)]
94 pub subscribe_instrument_close: bool,
95 #[builder(default = false)]
97 pub subscribe_option_greeks: bool,
98 pub subscribe_params: Option<Params>,
100 pub request_params: Option<Params>,
102 #[builder(default = true)]
104 pub can_unsubscribe: bool,
105 #[builder(default = false)]
107 pub request_instruments: bool,
108 #[builder(default = false)]
110 pub request_quotes: bool,
111 #[builder(default = false)]
114 pub request_trades: bool,
115 #[builder(default = false)]
117 pub request_bars: bool,
118 #[builder(default = false)]
120 pub request_book_snapshot: bool,
121 #[builder(default = false)]
124 pub request_book_deltas: bool,
125 #[builder(default = false)]
127 pub request_funding_rates: bool,
128 #[builder(default = BookType::L2_MBP)]
131 pub book_type: BookType,
132 pub book_depth: Option<usize>,
134 #[builder(default = 1000)]
137 pub book_interval_ms: usize,
138 #[builder(default = 10)]
140 pub book_levels_to_print: usize,
141 #[builder(default = true)]
143 pub manage_book: bool,
144 #[builder(default = true)]
146 pub log_data: bool,
147 #[builder(default = 5)]
149 pub stats_interval_secs: u64,
150}
151
152impl<S: data_tester_config_builder::IsComplete> DataTesterConfigBuilder<S> {
153 pub fn build(self) -> ConfigResult<DataTesterConfig> {
160 let config = self.build_inner();
161 config.validate()?;
162 Ok(config)
163 }
164}
165
166impl DataTesterConfig {
167 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 #[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}