nautilus_execution/engine/
config.rs1use nautilus_common::config::{ConfigError, ConfigErrorCollector, ConfigResult};
17use nautilus_core::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 allow_overfills: bool,
64 #[serde(default)]
66 #[builder(default)]
67 pub filter_unclaimed_external_orders: bool,
68 #[serde(default)]
74 pub external_clients: Option<Vec<ClientId>>,
75 #[serde(default)]
77 pub purge_closed_orders_interval_mins: Option<u32>,
78 #[serde(default)]
80 pub purge_closed_orders_buffer_mins: Option<u32>,
81 #[serde(default)]
83 pub purge_closed_positions_interval_mins: Option<u32>,
84 #[serde(default)]
86 pub purge_closed_positions_buffer_mins: Option<u32>,
87 #[serde(default)]
89 pub purge_account_events_interval_mins: Option<u32>,
90 #[serde(default)]
92 pub purge_account_events_lookback_mins: Option<u32>,
93 #[serde(default)]
95 #[builder(default)]
96 pub purge_from_database: bool,
97 #[serde(default)]
99 #[builder(default)]
100 pub debug: bool,
101}
102
103impl<S: execution_engine_config_builder::IsComplete> ExecutionEngineConfigBuilder<S> {
104 pub fn build(self) -> ConfigResult<ExecutionEngineConfig> {
111 let config = self.build_inner();
112 config.validate()?;
113 Ok(config)
114 }
115}
116
117impl ExecutionEngineConfig {
118 pub fn validate(&self) -> ConfigResult<()> {
125 let mut errors = ConfigErrorCollector::new();
126
127 if let Some(secs) = self.snapshot_positions_interval_secs {
128 errors.check(
129 secs.is_finite() && secs > 0.0,
130 ConfigError::range(
131 "snapshot_positions_interval_secs",
132 format!("must be a positive finite value, was {secs}"),
133 ),
134 );
135 }
136
137 for (field, value) in [
138 (
139 "purge_closed_orders_interval_mins",
140 self.purge_closed_orders_interval_mins,
141 ),
142 (
143 "purge_closed_positions_interval_mins",
144 self.purge_closed_positions_interval_mins,
145 ),
146 (
147 "purge_account_events_interval_mins",
148 self.purge_account_events_interval_mins,
149 ),
150 ] {
151 if let Some(mins) = value {
152 errors.check(
153 mins > 0,
154 ConfigError::range(
155 field,
156 format!("must be a positive number of minutes, was {mins}"),
157 ),
158 );
159 }
160 }
161
162 errors.into_result()
163 }
164}
165
166impl Default for ExecutionEngineConfig {
167 fn default() -> Self {
168 Self::builder()
169 .build()
170 .expect("default `ExecutionEngineConfig` should be valid")
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use rstest::rstest;
177
178 use super::*;
179
180 #[rstest]
181 fn test_default_config_is_valid() {
182 assert!(ExecutionEngineConfig::builder().build().is_ok());
183 }
184
185 #[rstest]
186 #[case(0.0)]
187 #[case(-1.0)]
188 #[case(f64::INFINITY)]
189 #[case(f64::NAN)]
190 fn test_invalid_snapshot_positions_interval_secs_rejected(#[case] secs: f64) {
191 let result = ExecutionEngineConfig::builder()
192 .snapshot_positions_interval_secs(secs)
193 .build();
194 assert!(
195 matches!(result, Err(ConfigError::Range { field, .. }) if field == "snapshot_positions_interval_secs")
196 );
197 }
198
199 #[rstest]
200 fn test_positive_snapshot_positions_interval_secs_accepted() {
201 let result = ExecutionEngineConfig::builder()
202 .snapshot_positions_interval_secs(5.0)
203 .build();
204 assert!(result.is_ok());
205 }
206
207 #[rstest]
208 fn test_zero_purge_closed_orders_interval_rejected() {
209 let result = ExecutionEngineConfig::builder()
210 .purge_closed_orders_interval_mins(0)
211 .build();
212 assert!(
213 matches!(result, Err(ConfigError::Range { field, .. }) if field == "purge_closed_orders_interval_mins")
214 );
215 }
216
217 #[rstest]
218 fn test_zero_purge_closed_positions_interval_rejected() {
219 let result = ExecutionEngineConfig::builder()
220 .purge_closed_positions_interval_mins(0)
221 .build();
222 assert!(
223 matches!(result, Err(ConfigError::Range { field, .. }) if field == "purge_closed_positions_interval_mins")
224 );
225 }
226
227 #[rstest]
228 fn test_zero_purge_account_events_interval_rejected() {
229 let result = ExecutionEngineConfig::builder()
230 .purge_account_events_interval_mins(0)
231 .build();
232 assert!(
233 matches!(result, Err(ConfigError::Range { field, .. }) if field == "purge_account_events_interval_mins")
234 );
235 }
236
237 #[rstest]
238 fn test_positive_purge_intervals_accepted() {
239 let result = ExecutionEngineConfig::builder()
241 .purge_closed_orders_interval_mins(10)
242 .purge_closed_positions_interval_mins(10)
243 .purge_account_events_interval_mins(10)
244 .purge_closed_orders_buffer_mins(0)
245 .build();
246 assert!(result.is_ok());
247 }
248
249 #[rstest]
250 fn test_multiple_violations_collected() {
251 let result = ExecutionEngineConfig::builder()
252 .snapshot_positions_interval_secs(0.0)
253 .purge_closed_orders_interval_mins(0)
254 .build();
255 let ConfigError::Multiple { errors } = result.unwrap_err() else {
256 panic!("expected ConfigError::Multiple");
257 };
258 assert_eq!(errors.len(), 2);
259 assert!(errors.iter().any(
260 |e| matches!(e, ConfigError::Range { field, .. } if field == "snapshot_positions_interval_secs")
261 ));
262 assert!(errors.iter().any(
263 |e| matches!(e, ConfigError::Range { field, .. } if field == "purge_closed_orders_interval_mins")
264 ));
265 }
266}