use nautilus_common::{
actor::DataActorConfig,
config::{ConfigError, ConfigErrorCollector, ConfigResult},
};
use nautilus_core::Params;
use nautilus_model::{
data::bar::BarType,
enums::BookType,
identifiers::{ClientId, InstrumentId},
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, Serialize, bon::Builder)]
#[builder(finish_fn(name = build_inner, vis = ""))]
#[serde(default, deny_unknown_fields)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.testkit", from_py_object)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.testkit")
)]
#[expect(
clippy::struct_excessive_bools,
reason = "tester configuration exposes independent scenario toggles"
)]
#[allow(
clippy::unsafe_derive_deserialize,
reason = "config type deserializes plain field values; unsafe PyO3 methods are unrelated"
)]
pub struct DataTesterConfig {
#[builder(default)]
pub base: DataActorConfig,
#[builder(default)]
pub instrument_ids: Vec<InstrumentId>,
pub client_id: Option<ClientId>,
pub bar_types: Option<Vec<BarType>>,
#[builder(default = false)]
pub subscribe_book_deltas: bool,
#[builder(default = false)]
pub subscribe_book_depth: bool,
#[builder(default = false)]
pub subscribe_book_at_interval: bool,
#[builder(default = false)]
pub subscribe_quotes: bool,
#[builder(default = false)]
pub subscribe_trades: bool,
#[builder(default = false)]
pub subscribe_mark_prices: bool,
#[builder(default = false)]
pub subscribe_index_prices: bool,
#[builder(default = false)]
pub subscribe_funding_rates: bool,
#[builder(default = false)]
pub subscribe_bars: bool,
#[builder(default = false)]
pub subscribe_instrument: bool,
#[builder(default = false)]
pub subscribe_instrument_status: bool,
#[builder(default = false)]
pub subscribe_instrument_close: bool,
#[builder(default = false)]
pub subscribe_option_greeks: bool,
pub subscribe_params: Option<Params>,
pub request_params: Option<Params>,
#[builder(default = true)]
pub can_unsubscribe: bool,
#[builder(default = false)]
pub request_instruments: bool,
#[builder(default = false)]
pub request_quotes: bool,
#[builder(default = false)]
pub request_trades: bool,
#[builder(default = false)]
pub request_bars: bool,
#[builder(default = false)]
pub request_book_snapshot: bool,
#[builder(default = false)]
pub request_book_deltas: bool,
#[builder(default = false)]
pub request_funding_rates: bool,
#[builder(default = BookType::L2_MBP)]
pub book_type: BookType,
pub book_depth: Option<usize>,
#[builder(default = 1000)]
pub book_interval_ms: usize,
#[builder(default = 10)]
pub book_levels_to_print: usize,
#[builder(default = true)]
pub manage_book: bool,
#[builder(default = true)]
pub log_data: bool,
#[builder(default = 5)]
pub stats_interval_secs: u64,
}
impl<S: data_tester_config_builder::IsComplete> DataTesterConfigBuilder<S> {
pub fn build(self) -> ConfigResult<DataTesterConfig> {
let config = self.build_inner();
config.validate()?;
Ok(config)
}
}
impl DataTesterConfig {
pub fn validate(&self) -> ConfigResult<()> {
let mut errors = ConfigErrorCollector::new();
errors.check(
self.book_interval_ms > 0,
ConfigError::range("book_interval_ms", "must be positive, was 0"),
);
if let Some(book_depth) = self.book_depth {
errors.check(
book_depth > 0,
ConfigError::range("book_depth", "must be positive, was 0"),
);
}
errors.into_result()
}
#[must_use]
pub fn new(client_id: ClientId, instrument_ids: Vec<InstrumentId>) -> Self {
Self {
base: DataActorConfig::default(),
instrument_ids,
client_id: Some(client_id),
bar_types: None,
subscribe_book_deltas: false,
subscribe_book_depth: false,
subscribe_book_at_interval: false,
subscribe_quotes: false,
subscribe_trades: false,
subscribe_mark_prices: false,
subscribe_index_prices: false,
subscribe_funding_rates: false,
subscribe_bars: false,
subscribe_instrument: false,
subscribe_instrument_status: false,
subscribe_instrument_close: false,
subscribe_option_greeks: false,
subscribe_params: None,
request_params: None,
can_unsubscribe: true,
request_instruments: false,
request_quotes: false,
request_trades: false,
request_bars: false,
request_book_snapshot: false,
request_book_deltas: false,
request_funding_rates: false,
book_type: BookType::L2_MBP,
book_depth: None,
book_interval_ms: 1000,
book_levels_to_print: 10,
manage_book: true,
log_data: true,
stats_interval_secs: 5,
}
}
}
impl Default for DataTesterConfig {
fn default() -> Self {
Self::builder()
.build()
.expect("default DataTesterConfig should be valid")
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
fn test_default_config_is_valid() {
assert!(DataTesterConfig::builder().build().is_ok());
}
#[rstest]
fn test_zero_book_interval_ms_rejected() {
let result = DataTesterConfig::builder().book_interval_ms(0).build();
assert!(
matches!(result, Err(ConfigError::Range { field, .. }) if field == "book_interval_ms")
);
}
#[rstest]
fn test_zero_book_depth_rejected() {
let result = DataTesterConfig::builder().book_depth(0).build();
assert!(matches!(result, Err(ConfigError::Range { field, .. }) if field == "book_depth"));
}
}