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(
25 module = "nautilus_trader.core.nautilus_pyo3.execution",
26 from_py_object
27 )
28)]
29#[cfg_attr(
30 feature = "python",
31 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.execution")
32)]
33#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
34#[builder(finish_fn(name = build_inner, vis = ""))]
35#[serde(deny_unknown_fields)]
36pub struct ExecutionEngineConfig {
37 #[serde(default = "default_true")]
39 #[builder(default = true)]
40 pub load_cache: bool,
41 #[serde(default)]
43 #[builder(default)]
44 pub manage_own_order_books: bool,
45 #[serde(default)]
48 #[builder(default)]
49 pub snapshot_orders: bool,
50 #[serde(default)]
53 #[builder(default)]
54 pub snapshot_positions: bool,
55 #[serde(default)]
58 pub snapshot_positions_interval_secs: Option<f64>,
59 #[serde(default)]
62 #[builder(default)]
63 pub carry_replay_events_on_reopen: bool,
64 #[serde(default)]
67 #[builder(default)]
68 pub allow_overfills: bool,
69 #[serde(default)]
71 #[builder(default)]
72 pub filter_unclaimed_external_orders: bool,
73 #[serde(default)]
79 pub external_clients: Option<Vec<ClientId>>,
80 #[serde(default)]
82 pub purge_closed_orders_interval_mins: Option<u32>,
83 #[serde(default)]
85 pub purge_closed_orders_buffer_mins: Option<u32>,
86 #[serde(default)]
88 pub purge_closed_positions_interval_mins: Option<u32>,
89 #[serde(default)]
91 pub purge_closed_positions_buffer_mins: Option<u32>,
92 #[serde(default)]
94 pub purge_account_events_interval_mins: Option<u32>,
95 #[serde(default)]
97 pub purge_account_events_lookback_mins: Option<u32>,
98 #[serde(default)]
100 #[builder(default)]
101 pub purge_from_database: bool,
102 #[serde(default)]
104 #[builder(default)]
105 pub debug: bool,
106}
107
108impl<S: execution_engine_config_builder::IsComplete> ExecutionEngineConfigBuilder<S> {
109 pub fn build(self) -> ConfigResult<ExecutionEngineConfig> {
116 let config = self.build_inner();
117 config.validate()?;
118 Ok(config)
119 }
120}
121
122impl ExecutionEngineConfig {
123 pub fn validate(&self) -> ConfigResult<()> {
130 let mut errors = ConfigErrorCollector::new();
131
132 if let Some(secs) = self.snapshot_positions_interval_secs {
133 errors.check(
134 secs.is_finite() && secs > 0.0,
135 ConfigError::range(
136 "snapshot_positions_interval_secs",
137 format!("must be a positive finite value, was {secs}"),
138 ),
139 );
140 }
141
142 for (field, value) in [
143 (
144 "purge_closed_orders_interval_mins",
145 self.purge_closed_orders_interval_mins,
146 ),
147 (
148 "purge_closed_positions_interval_mins",
149 self.purge_closed_positions_interval_mins,
150 ),
151 (
152 "purge_account_events_interval_mins",
153 self.purge_account_events_interval_mins,
154 ),
155 ] {
156 if let Some(mins) = value {
157 let reason = if mins == 0 {
158 format!("must be a positive number of minutes, was {mins}")
159 } else {
160 format!("must be positive and fit in `u64` nanoseconds, was {mins} minutes")
161 };
162 errors.check(
163 mins > 0 && checked_mins_to_nanos(u64::from(mins)).is_some(),
164 ConfigError::range(field, reason),
165 );
166 }
167 }
168
169 errors.into_result()
170 }
171}
172
173impl Default for ExecutionEngineConfig {
174 fn default() -> Self {
175 Self::builder()
176 .build()
177 .expect("default `ExecutionEngineConfig` should be valid")
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use rstest::rstest;
184
185 use super::*;
186
187 #[rstest]
188 fn test_default_config_is_valid() {
189 assert!(ExecutionEngineConfig::builder().build().is_ok());
190 }
191
192 #[rstest]
193 fn test_carry_replay_events_on_reopen_defaults_false() {
194 assert!(!ExecutionEngineConfig::default().carry_replay_events_on_reopen);
195
196 let config: ExecutionEngineConfig =
197 serde_json::from_str("{}").expect("empty config should deserialize");
198 assert!(!config.carry_replay_events_on_reopen);
199
200 let config = ExecutionEngineConfig::builder()
201 .carry_replay_events_on_reopen(true)
202 .build()
203 .unwrap();
204 assert!(config.carry_replay_events_on_reopen);
205 }
206
207 #[rstest]
208 #[case(0.0)]
209 #[case(-1.0)]
210 #[case(f64::INFINITY)]
211 #[case(f64::NAN)]
212 fn test_invalid_snapshot_positions_interval_secs_rejected(#[case] secs: f64) {
213 let result = ExecutionEngineConfig::builder()
214 .snapshot_positions_interval_secs(secs)
215 .build();
216 assert!(
217 matches!(result, Err(ConfigError::Range { field, .. }) if field == "snapshot_positions_interval_secs")
218 );
219 }
220
221 #[rstest]
222 fn test_positive_snapshot_positions_interval_secs_accepted() {
223 let result = ExecutionEngineConfig::builder()
224 .snapshot_positions_interval_secs(5.0)
225 .build();
226 assert!(result.is_ok());
227 }
228
229 #[rstest]
230 fn test_zero_purge_closed_orders_interval_rejected() {
231 let result = ExecutionEngineConfig::builder()
232 .purge_closed_orders_interval_mins(0)
233 .build();
234 assert!(
235 matches!(result, Err(ConfigError::Range { field, .. }) if field == "purge_closed_orders_interval_mins")
236 );
237 }
238
239 #[rstest]
240 fn test_zero_purge_closed_positions_interval_rejected() {
241 let result = ExecutionEngineConfig::builder()
242 .purge_closed_positions_interval_mins(0)
243 .build();
244 assert!(
245 matches!(result, Err(ConfigError::Range { field, .. }) if field == "purge_closed_positions_interval_mins")
246 );
247 }
248
249 #[rstest]
250 fn test_zero_purge_account_events_interval_rejected() {
251 let result = ExecutionEngineConfig::builder()
252 .purge_account_events_interval_mins(0)
253 .build();
254 assert!(
255 matches!(result, Err(ConfigError::Range { field, .. }) if field == "purge_account_events_interval_mins")
256 );
257 }
258
259 #[rstest]
260 fn test_positive_purge_intervals_accepted() {
261 let result = ExecutionEngineConfig::builder()
263 .purge_closed_orders_interval_mins(10)
264 .purge_closed_positions_interval_mins(10)
265 .purge_account_events_interval_mins(10)
266 .purge_closed_orders_buffer_mins(0)
267 .build();
268 assert!(result.is_ok());
269 }
270
271 #[rstest]
272 fn test_overflowing_purge_intervals_rejected() {
273 let result = ExecutionEngineConfig::builder()
274 .purge_closed_orders_interval_mins(u32::MAX)
275 .purge_closed_positions_interval_mins(u32::MAX)
276 .purge_account_events_interval_mins(u32::MAX)
277 .build();
278 assert_eq!(
279 result.unwrap_err(),
280 ConfigError::Multiple {
281 errors: vec![
282 ConfigError::range(
283 "purge_closed_orders_interval_mins",
284 "must be positive and fit in `u64` nanoseconds, was 4294967295 minutes",
285 ),
286 ConfigError::range(
287 "purge_closed_positions_interval_mins",
288 "must be positive and fit in `u64` nanoseconds, was 4294967295 minutes",
289 ),
290 ConfigError::range(
291 "purge_account_events_interval_mins",
292 "must be positive and fit in `u64` nanoseconds, was 4294967295 minutes",
293 ),
294 ],
295 }
296 );
297 }
298
299 #[rstest]
300 fn test_multiple_violations_collected() {
301 let result = ExecutionEngineConfig::builder()
302 .snapshot_positions_interval_secs(0.0)
303 .purge_closed_orders_interval_mins(0)
304 .build();
305 let ConfigError::Multiple { errors } = result.unwrap_err() else {
306 panic!("expected ConfigError::Multiple");
307 };
308 assert_eq!(errors.len(), 2);
309 assert!(errors.iter().any(
310 |e| matches!(e, ConfigError::Range { field, .. } if field == "snapshot_positions_interval_secs")
311 ));
312 assert!(errors.iter().any(
313 |e| matches!(e, ConfigError::Range { field, .. } if field == "purge_closed_orders_interval_mins")
314 ));
315 }
316}