nautilus_execution/engine/
config.rs1use nautilus_common::config::{ConfigError, ConfigErrorCollector, ConfigResult};
17use nautilus_core::{DurationNanos, serialization::default_true};
18use nautilus_model::identifiers::ClientId;
19use serde::{Deserialize, Serialize};
20
21#[cfg_attr(
23 feature = "python",
24 pyo3::pyclass(module = "nautilus_trader.execution", from_py_object)
25)]
26#[cfg_attr(
27 feature = "python",
28 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
29)]
30#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
31#[builder(finish_fn(name = build_inner, vis = ""))]
32#[serde(deny_unknown_fields)]
33pub struct ExecutionEngineConfig {
34 #[serde(default = "default_true")]
36 #[builder(default = true)]
37 pub load_cache: bool,
38 #[serde(default)]
40 #[builder(default)]
41 pub manage_own_order_books: bool,
42 #[serde(default)]
45 #[builder(default)]
46 pub snapshot_orders: bool,
47 #[serde(default)]
50 #[builder(default)]
51 pub snapshot_positions: bool,
52 #[serde(default)]
55 pub snapshot_positions_interval_secs: Option<f64>,
56 #[serde(default)]
59 #[builder(default)]
60 pub carry_replay_events_on_reopen: bool,
61 #[serde(default)]
64 #[builder(default)]
65 pub allow_overfills: bool,
66 #[serde(default)]
68 #[builder(default)]
69 pub filter_unclaimed_external_orders: bool,
70 #[serde(default)]
76 pub external_clients: Option<Vec<ClientId>>,
77 #[serde(default)]
79 pub purge_closed_orders_interval_mins: Option<u32>,
80 #[serde(default)]
82 pub purge_closed_orders_buffer_mins: Option<u32>,
83 #[serde(default)]
85 pub purge_closed_positions_interval_mins: Option<u32>,
86 #[serde(default)]
88 pub purge_closed_positions_buffer_mins: Option<u32>,
89 #[serde(default)]
91 pub purge_account_events_interval_mins: Option<u32>,
92 #[serde(default)]
94 pub purge_account_events_lookback_mins: Option<u32>,
95 #[serde(default)]
97 #[builder(default)]
98 pub purge_from_database: bool,
99 #[serde(default)]
101 #[builder(default)]
102 pub debug: bool,
103}
104
105impl<S: execution_engine_config_builder::IsComplete> ExecutionEngineConfigBuilder<S> {
106 pub fn build(self) -> ConfigResult<ExecutionEngineConfig> {
112 let config = self.build_inner();
113 config.validate()?;
114 Ok(config)
115 }
116}
117
118impl ExecutionEngineConfig {
119 pub fn validate(&self) -> ConfigResult<()> {
126 let mut errors = ConfigErrorCollector::new();
127
128 if let Some(secs) = self.snapshot_positions_interval_secs {
129 errors.check(
130 secs.is_finite() && secs > 0.0,
131 ConfigError::range(
132 "snapshot_positions_interval_secs",
133 format!("must be a positive finite value, was {secs}"),
134 ),
135 );
136 }
137
138 for (field, value) in [
139 (
140 "purge_closed_orders_interval_mins",
141 self.purge_closed_orders_interval_mins,
142 ),
143 (
144 "purge_closed_positions_interval_mins",
145 self.purge_closed_positions_interval_mins,
146 ),
147 (
148 "purge_account_events_interval_mins",
149 self.purge_account_events_interval_mins,
150 ),
151 ] {
152 if let Some(mins) = value {
153 let reason = if mins == 0 {
154 format!("must be a positive number of minutes, was {mins}")
155 } else {
156 format!("must be positive and fit in `u64` nanoseconds, was {mins} minutes")
157 };
158 errors.check(
159 mins > 0 && DurationNanos::try_from_mins(u64::from(mins)).is_ok(),
160 ConfigError::range(field, reason),
161 );
162 }
163 }
164
165 for (field, value) in [
166 (
167 "purge_closed_orders_buffer_mins",
168 self.purge_closed_orders_buffer_mins,
169 ),
170 (
171 "purge_closed_positions_buffer_mins",
172 self.purge_closed_positions_buffer_mins,
173 ),
174 (
175 "purge_account_events_lookback_mins",
176 self.purge_account_events_lookback_mins,
177 ),
178 ] {
179 if let Some(mins) = value {
180 errors.check(
181 DurationNanos::try_from_mins(u64::from(mins)).is_ok(),
182 ConfigError::range(
183 field,
184 format!("must fit in `u64` nanoseconds, was {mins} minutes"),
185 ),
186 );
187 }
188 }
189
190 errors.into_result()
191 }
192}
193
194impl Default for ExecutionEngineConfig {
195 fn default() -> Self {
196 Self::builder()
197 .build()
198 .expect("default `ExecutionEngineConfig` should be valid")
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use rstest::rstest;
205
206 use super::*;
207
208 #[rstest]
209 fn test_default_config_is_valid() {
210 assert!(ExecutionEngineConfig::builder().build().is_ok());
211 }
212
213 #[rstest]
214 fn test_carry_replay_events_on_reopen_defaults_false() {
215 assert!(!ExecutionEngineConfig::default().carry_replay_events_on_reopen);
216
217 let config: ExecutionEngineConfig =
218 serde_json::from_str("{}").expect("empty config should deserialize");
219 assert!(!config.carry_replay_events_on_reopen);
220
221 let config = ExecutionEngineConfig::builder()
222 .carry_replay_events_on_reopen(true)
223 .build()
224 .unwrap();
225 assert!(config.carry_replay_events_on_reopen);
226 }
227
228 #[rstest]
229 #[case(0.0)]
230 #[case(-1.0)]
231 #[case(f64::INFINITY)]
232 #[case(f64::NAN)]
233 fn test_invalid_snapshot_positions_interval_secs_rejected(#[case] secs: f64) {
234 let result = ExecutionEngineConfig::builder()
235 .snapshot_positions_interval_secs(secs)
236 .build();
237 assert!(
238 matches!(result, Err(ConfigError::Range { field, .. }) if field == "snapshot_positions_interval_secs")
239 );
240 }
241
242 #[rstest]
243 fn test_positive_snapshot_positions_interval_secs_accepted() {
244 let result = ExecutionEngineConfig::builder()
245 .snapshot_positions_interval_secs(5.0)
246 .build();
247 assert!(result.is_ok());
248 }
249
250 #[rstest]
251 fn test_zero_purge_closed_orders_interval_rejected() {
252 let result = ExecutionEngineConfig::builder()
253 .purge_closed_orders_interval_mins(0)
254 .build();
255 assert!(
256 matches!(result, Err(ConfigError::Range { field, .. }) if field == "purge_closed_orders_interval_mins")
257 );
258 }
259
260 #[rstest]
261 fn test_zero_purge_closed_positions_interval_rejected() {
262 let result = ExecutionEngineConfig::builder()
263 .purge_closed_positions_interval_mins(0)
264 .build();
265 assert!(
266 matches!(result, Err(ConfigError::Range { field, .. }) if field == "purge_closed_positions_interval_mins")
267 );
268 }
269
270 #[rstest]
271 fn test_zero_purge_account_events_interval_rejected() {
272 let result = ExecutionEngineConfig::builder()
273 .purge_account_events_interval_mins(0)
274 .build();
275 assert!(
276 matches!(result, Err(ConfigError::Range { field, .. }) if field == "purge_account_events_interval_mins")
277 );
278 }
279
280 #[rstest]
281 fn test_positive_purge_intervals_accepted() {
282 let result = ExecutionEngineConfig::builder()
284 .purge_closed_orders_interval_mins(10)
285 .purge_closed_positions_interval_mins(10)
286 .purge_account_events_interval_mins(10)
287 .purge_closed_orders_buffer_mins(0)
288 .build();
289 assert!(result.is_ok());
290 }
291
292 #[rstest]
293 fn test_overflowing_purge_intervals_rejected() {
294 let result = ExecutionEngineConfig::builder()
295 .purge_closed_orders_interval_mins(u32::MAX)
296 .purge_closed_positions_interval_mins(u32::MAX)
297 .purge_account_events_interval_mins(u32::MAX)
298 .build();
299 assert_eq!(
300 result.unwrap_err(),
301 ConfigError::Multiple {
302 errors: vec![
303 ConfigError::range(
304 "purge_closed_orders_interval_mins",
305 "must be positive and fit in `u64` nanoseconds, was 4294967295 minutes",
306 ),
307 ConfigError::range(
308 "purge_closed_positions_interval_mins",
309 "must be positive and fit in `u64` nanoseconds, was 4294967295 minutes",
310 ),
311 ConfigError::range(
312 "purge_account_events_interval_mins",
313 "must be positive and fit in `u64` nanoseconds, was 4294967295 minutes",
314 ),
315 ],
316 }
317 );
318 }
319
320 #[rstest]
321 #[case("purge_closed_orders_buffer_mins", 0)]
322 #[case("purge_closed_orders_buffer_mins", 307_445_734)]
323 #[case("purge_closed_positions_buffer_mins", 0)]
324 #[case("purge_closed_positions_buffer_mins", 307_445_734)]
325 #[case("purge_account_events_lookback_mins", 0)]
326 #[case("purge_account_events_lookback_mins", 307_445_734)]
327 fn test_purge_retention_minute_boundaries_accepted(#[case] field: &str, #[case] mins: u32) {
328 let mut config = ExecutionEngineConfig::default();
329
330 match field {
331 "purge_closed_orders_buffer_mins" => {
332 config.purge_closed_orders_buffer_mins = Some(mins);
333 }
334 "purge_closed_positions_buffer_mins" => {
335 config.purge_closed_positions_buffer_mins = Some(mins);
336 }
337 "purge_account_events_lookback_mins" => {
338 config.purge_account_events_lookback_mins = Some(mins);
339 }
340 _ => unreachable!(),
341 }
342
343 assert!(config.validate().is_ok());
344 }
345
346 #[rstest]
347 #[case("purge_closed_orders_buffer_mins")]
348 #[case("purge_closed_positions_buffer_mins")]
349 #[case("purge_account_events_lookback_mins")]
350 fn test_overflowing_purge_retention_minutes_rejected(#[case] field: &str) {
351 let mut config = ExecutionEngineConfig::default();
352
353 match field {
354 "purge_closed_orders_buffer_mins" => {
355 config.purge_closed_orders_buffer_mins = Some(307_445_735);
356 }
357 "purge_closed_positions_buffer_mins" => {
358 config.purge_closed_positions_buffer_mins = Some(307_445_735);
359 }
360 "purge_account_events_lookback_mins" => {
361 config.purge_account_events_lookback_mins = Some(307_445_735);
362 }
363 _ => unreachable!(),
364 }
365
366 assert!(matches!(
367 config.validate(),
368 Err(ConfigError::Range { field: error_field, .. }) if error_field == field
369 ));
370 }
371
372 #[rstest]
373 fn test_multiple_violations_collected() {
374 let result = ExecutionEngineConfig::builder()
375 .snapshot_positions_interval_secs(0.0)
376 .purge_closed_orders_interval_mins(0)
377 .build();
378 let ConfigError::Multiple { errors } = result.unwrap_err() else {
379 panic!("expected ConfigError::Multiple");
380 };
381 assert_eq!(errors.len(), 2);
382 assert!(errors.iter().any(
383 |e| matches!(e, ConfigError::Range { field, .. } if field == "snapshot_positions_interval_secs")
384 ));
385 assert!(errors.iter().any(
386 |e| matches!(e, ConfigError::Range { field, .. } if field == "purge_closed_orders_interval_mins")
387 ));
388 }
389}