nautilus_execution/engine/
config.rs1use nautilus_common::config::{ConfigError, ConfigErrorCollector, ConfigResult};
17use nautilus_core::{datetime::checked_mins_to_nanos, 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> {
113 let config = self.build_inner();
114 config.validate()?;
115 Ok(config)
116 }
117}
118
119impl ExecutionEngineConfig {
120 pub fn validate(&self) -> ConfigResult<()> {
127 let mut errors = ConfigErrorCollector::new();
128
129 if let Some(secs) = self.snapshot_positions_interval_secs {
130 errors.check(
131 secs.is_finite() && secs > 0.0,
132 ConfigError::range(
133 "snapshot_positions_interval_secs",
134 format!("must be a positive finite value, was {secs}"),
135 ),
136 );
137 }
138
139 for (field, value) in [
140 (
141 "purge_closed_orders_interval_mins",
142 self.purge_closed_orders_interval_mins,
143 ),
144 (
145 "purge_closed_positions_interval_mins",
146 self.purge_closed_positions_interval_mins,
147 ),
148 (
149 "purge_account_events_interval_mins",
150 self.purge_account_events_interval_mins,
151 ),
152 ] {
153 if let Some(mins) = value {
154 let reason = if mins == 0 {
155 format!("must be a positive number of minutes, was {mins}")
156 } else {
157 format!("must be positive and fit in `u64` nanoseconds, was {mins} minutes")
158 };
159 errors.check(
160 mins > 0 && checked_mins_to_nanos(u64::from(mins)).is_some(),
161 ConfigError::range(field, reason),
162 );
163 }
164 }
165
166 for (field, value) in [
167 (
168 "purge_closed_orders_buffer_mins",
169 self.purge_closed_orders_buffer_mins,
170 ),
171 (
172 "purge_closed_positions_buffer_mins",
173 self.purge_closed_positions_buffer_mins,
174 ),
175 (
176 "purge_account_events_lookback_mins",
177 self.purge_account_events_lookback_mins,
178 ),
179 ] {
180 if let Some(mins) = value {
181 errors.check(
182 checked_mins_to_nanos(u64::from(mins)).is_some(),
183 ConfigError::range(
184 field,
185 format!("must fit in `u64` nanoseconds, was {mins} minutes"),
186 ),
187 );
188 }
189 }
190
191 errors.into_result()
192 }
193}
194
195impl Default for ExecutionEngineConfig {
196 fn default() -> Self {
197 Self::builder()
198 .build()
199 .expect("default `ExecutionEngineConfig` should be valid")
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use rstest::rstest;
206
207 use super::*;
208
209 #[rstest]
210 fn test_default_config_is_valid() {
211 assert!(ExecutionEngineConfig::builder().build().is_ok());
212 }
213
214 #[rstest]
215 fn test_carry_replay_events_on_reopen_defaults_false() {
216 assert!(!ExecutionEngineConfig::default().carry_replay_events_on_reopen);
217
218 let config: ExecutionEngineConfig =
219 serde_json::from_str("{}").expect("empty config should deserialize");
220 assert!(!config.carry_replay_events_on_reopen);
221
222 let config = ExecutionEngineConfig::builder()
223 .carry_replay_events_on_reopen(true)
224 .build()
225 .unwrap();
226 assert!(config.carry_replay_events_on_reopen);
227 }
228
229 #[rstest]
230 #[case(0.0)]
231 #[case(-1.0)]
232 #[case(f64::INFINITY)]
233 #[case(f64::NAN)]
234 fn test_invalid_snapshot_positions_interval_secs_rejected(#[case] secs: f64) {
235 let result = ExecutionEngineConfig::builder()
236 .snapshot_positions_interval_secs(secs)
237 .build();
238 assert!(
239 matches!(result, Err(ConfigError::Range { field, .. }) if field == "snapshot_positions_interval_secs")
240 );
241 }
242
243 #[rstest]
244 fn test_positive_snapshot_positions_interval_secs_accepted() {
245 let result = ExecutionEngineConfig::builder()
246 .snapshot_positions_interval_secs(5.0)
247 .build();
248 assert!(result.is_ok());
249 }
250
251 #[rstest]
252 fn test_zero_purge_closed_orders_interval_rejected() {
253 let result = ExecutionEngineConfig::builder()
254 .purge_closed_orders_interval_mins(0)
255 .build();
256 assert!(
257 matches!(result, Err(ConfigError::Range { field, .. }) if field == "purge_closed_orders_interval_mins")
258 );
259 }
260
261 #[rstest]
262 fn test_zero_purge_closed_positions_interval_rejected() {
263 let result = ExecutionEngineConfig::builder()
264 .purge_closed_positions_interval_mins(0)
265 .build();
266 assert!(
267 matches!(result, Err(ConfigError::Range { field, .. }) if field == "purge_closed_positions_interval_mins")
268 );
269 }
270
271 #[rstest]
272 fn test_zero_purge_account_events_interval_rejected() {
273 let result = ExecutionEngineConfig::builder()
274 .purge_account_events_interval_mins(0)
275 .build();
276 assert!(
277 matches!(result, Err(ConfigError::Range { field, .. }) if field == "purge_account_events_interval_mins")
278 );
279 }
280
281 #[rstest]
282 fn test_positive_purge_intervals_accepted() {
283 let result = ExecutionEngineConfig::builder()
285 .purge_closed_orders_interval_mins(10)
286 .purge_closed_positions_interval_mins(10)
287 .purge_account_events_interval_mins(10)
288 .purge_closed_orders_buffer_mins(0)
289 .build();
290 assert!(result.is_ok());
291 }
292
293 #[rstest]
294 fn test_overflowing_purge_intervals_rejected() {
295 let result = ExecutionEngineConfig::builder()
296 .purge_closed_orders_interval_mins(u32::MAX)
297 .purge_closed_positions_interval_mins(u32::MAX)
298 .purge_account_events_interval_mins(u32::MAX)
299 .build();
300 assert_eq!(
301 result.unwrap_err(),
302 ConfigError::Multiple {
303 errors: vec![
304 ConfigError::range(
305 "purge_closed_orders_interval_mins",
306 "must be positive and fit in `u64` nanoseconds, was 4294967295 minutes",
307 ),
308 ConfigError::range(
309 "purge_closed_positions_interval_mins",
310 "must be positive and fit in `u64` nanoseconds, was 4294967295 minutes",
311 ),
312 ConfigError::range(
313 "purge_account_events_interval_mins",
314 "must be positive and fit in `u64` nanoseconds, was 4294967295 minutes",
315 ),
316 ],
317 }
318 );
319 }
320
321 #[rstest]
322 #[case("purge_closed_orders_buffer_mins", 0)]
323 #[case("purge_closed_orders_buffer_mins", 307_445_734)]
324 #[case("purge_closed_positions_buffer_mins", 0)]
325 #[case("purge_closed_positions_buffer_mins", 307_445_734)]
326 #[case("purge_account_events_lookback_mins", 0)]
327 #[case("purge_account_events_lookback_mins", 307_445_734)]
328 fn test_purge_retention_minute_boundaries_accepted(#[case] field: &str, #[case] mins: u32) {
329 let mut config = ExecutionEngineConfig::default();
330
331 match field {
332 "purge_closed_orders_buffer_mins" => {
333 config.purge_closed_orders_buffer_mins = Some(mins);
334 }
335 "purge_closed_positions_buffer_mins" => {
336 config.purge_closed_positions_buffer_mins = Some(mins);
337 }
338 "purge_account_events_lookback_mins" => {
339 config.purge_account_events_lookback_mins = Some(mins);
340 }
341 _ => unreachable!(),
342 }
343
344 assert!(config.validate().is_ok());
345 }
346
347 #[rstest]
348 #[case("purge_closed_orders_buffer_mins")]
349 #[case("purge_closed_positions_buffer_mins")]
350 #[case("purge_account_events_lookback_mins")]
351 fn test_overflowing_purge_retention_minutes_rejected(#[case] field: &str) {
352 let mut config = ExecutionEngineConfig::default();
353
354 match field {
355 "purge_closed_orders_buffer_mins" => {
356 config.purge_closed_orders_buffer_mins = Some(307_445_735);
357 }
358 "purge_closed_positions_buffer_mins" => {
359 config.purge_closed_positions_buffer_mins = Some(307_445_735);
360 }
361 "purge_account_events_lookback_mins" => {
362 config.purge_account_events_lookback_mins = Some(307_445_735);
363 }
364 _ => unreachable!(),
365 }
366
367 assert!(matches!(
368 config.validate(),
369 Err(ConfigError::Range { field: error_field, .. }) if error_field == field
370 ));
371 }
372
373 #[rstest]
374 fn test_multiple_violations_collected() {
375 let result = ExecutionEngineConfig::builder()
376 .snapshot_positions_interval_secs(0.0)
377 .purge_closed_orders_interval_mins(0)
378 .build();
379 let ConfigError::Multiple { errors } = result.unwrap_err() else {
380 panic!("expected ConfigError::Multiple");
381 };
382 assert_eq!(errors.len(), 2);
383 assert!(errors.iter().any(
384 |e| matches!(e, ConfigError::Range { field, .. } if field == "snapshot_positions_interval_secs")
385 ));
386 assert!(errors.iter().any(
387 |e| matches!(e, ConfigError::Range { field, .. } if field == "purge_closed_orders_interval_mins")
388 ));
389 }
390}