1use ahash::AHashMap;
19use nautilus_execution::{
20 matching_engine::config::OrderMatchingEngineConfig,
21 models::{fee::FeeModelAny, fill::FillModelAny, latency::LatencyModelAny},
22};
23use nautilus_model::{
24 enums::{AccountType, BookType, OmsType},
25 identifiers::{AccountId, InstrumentId, Venue},
26 types::{Currency, Money},
27};
28use rust_decimal::Decimal;
29use serde::{
30 Deserialize, Deserializer, Serialize, Serializer,
31 de::{self, IgnoredAny},
32};
33
34#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
36#[serde(default, deny_unknown_fields)]
37#[cfg_attr(
38 feature = "python",
39 pyo3::pyclass(module = "nautilus_trader.adapters.sandbox", from_py_object)
40)]
41#[cfg_attr(
42 feature = "python",
43 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.sandbox")
44)]
45pub struct SandboxExecutionClientConfig {
46 #[builder(default = AccountId::from("SANDBOX-001"))]
48 pub account_id: AccountId,
49 #[builder(default = Venue::new("SANDBOX"))]
51 pub venue: Venue,
52 #[builder(default)]
54 pub starting_balances: Vec<Money>,
55 pub base_currency: Option<Currency>,
57 #[builder(default = OmsType::Netting)]
59 pub oms_type: OmsType,
60 #[builder(default = AccountType::Margin)]
62 pub account_type: AccountType,
63 #[builder(default = Decimal::ONE)]
65 pub default_leverage: Decimal,
66 #[builder(default)]
68 pub leverages: AHashMap<InstrumentId, Decimal>,
69 #[builder(default = BookType::L1_MBP)]
71 pub book_type: BookType,
72 #[serde(
74 default,
75 skip_serializing_if = "Option::is_none",
76 serialize_with = "serialize_fee_model",
77 deserialize_with = "deserialize_fee_model"
78 )]
79 pub fee_model: Option<FeeModelAny>,
80 #[serde(
82 default,
83 skip_serializing_if = "Option::is_none",
84 serialize_with = "serialize_fill_model",
85 deserialize_with = "deserialize_fill_model"
86 )]
87 pub fill_model: Option<FillModelAny>,
88 #[serde(
90 default,
91 skip_serializing_if = "Option::is_none",
92 serialize_with = "serialize_latency_model",
93 deserialize_with = "deserialize_latency_model"
94 )]
95 pub latency_model: Option<LatencyModelAny>,
96 #[builder(default)]
98 pub frozen_account: bool,
99 #[builder(default = true)]
101 pub bar_execution: bool,
102 #[builder(default = true)]
104 pub trade_execution: bool,
105 #[builder(default = true)]
107 pub reject_stop_orders: bool,
108 #[builder(default = true)]
110 pub support_gtd_orders: bool,
111 #[builder(default = true)]
113 pub support_contingent_orders: bool,
114 #[builder(default = true)]
116 pub use_position_ids: bool,
117 #[builder(default)]
120 pub use_random_ids: bool,
121 #[builder(default = true)]
124 pub use_reduce_only: bool,
125 #[builder(default)]
127 pub queue_position: bool,
128 #[builder(default)]
130 pub liquidity_consumption: bool,
131 #[builder(default)]
133 pub bar_adaptive_high_low_ordering: bool,
134 #[builder(default)]
136 pub use_market_order_acks: bool,
137 #[builder(default)]
139 pub oto_full_trigger: bool,
140 #[builder(default)]
144 pub price_protection_points: u32,
145}
146
147impl SandboxExecutionClientConfig {
148 #[must_use]
150 pub fn to_matching_engine_config(&self) -> OrderMatchingEngineConfig {
151 let price_protection = if self.price_protection_points == 0 {
152 None
153 } else {
154 Some(self.price_protection_points)
155 };
156
157 OrderMatchingEngineConfig::builder()
158 .bar_execution(self.bar_execution)
159 .bar_adaptive_high_low_ordering(self.bar_adaptive_high_low_ordering)
160 .trade_execution(self.trade_execution)
161 .liquidity_consumption(self.liquidity_consumption)
162 .reject_stop_orders(self.reject_stop_orders)
163 .support_gtd_orders(self.support_gtd_orders)
164 .support_contingent_orders(self.support_contingent_orders)
165 .use_position_ids(self.use_position_ids)
166 .use_random_ids(self.use_random_ids)
167 .use_reduce_only(self.use_reduce_only)
168 .use_market_order_acks(self.use_market_order_acks)
169 .queue_position(self.queue_position)
170 .oto_full_trigger(self.oto_full_trigger)
171 .maybe_price_protection_points(price_protection)
172 .build()
173 }
174}
175
176impl Default for SandboxExecutionClientConfig {
177 fn default() -> Self {
178 Self::builder().build()
179 }
180}
181
182fn serialize_fee_model<S>(fee_model: &Option<FeeModelAny>, serializer: S) -> Result<S::Ok, S::Error>
183where
184 S: Serializer,
185{
186 match fee_model {
187 None => serializer.serialize_none(),
188 Some(_) => Err(serde::ser::Error::custom(
189 "SandboxExecutionClientConfig.fee_model is runtime-only and cannot be serialized",
190 )),
191 }
192}
193
194fn deserialize_fee_model<'de, D>(deserializer: D) -> Result<Option<FeeModelAny>, D::Error>
195where
196 D: Deserializer<'de>,
197{
198 let value = Option::<IgnoredAny>::deserialize(deserializer)?;
199
200 match value {
201 None => Ok(None),
202 Some(_) => Err(de::Error::custom(
203 "SandboxExecutionClientConfig.fee_model must be configured at runtime, not deserialized",
204 )),
205 }
206}
207
208fn serialize_fill_model<S>(
209 fill_model: &Option<FillModelAny>,
210 serializer: S,
211) -> Result<S::Ok, S::Error>
212where
213 S: Serializer,
214{
215 match fill_model {
216 None => serializer.serialize_none(),
217 Some(_) => Err(serde::ser::Error::custom(
218 "SandboxExecutionClientConfig.fill_model is runtime-only and cannot be serialized",
219 )),
220 }
221}
222
223fn deserialize_fill_model<'de, D>(deserializer: D) -> Result<Option<FillModelAny>, D::Error>
224where
225 D: Deserializer<'de>,
226{
227 let value = Option::<IgnoredAny>::deserialize(deserializer)?;
228
229 match value {
230 None => Ok(None),
231 Some(_) => Err(de::Error::custom(
232 "SandboxExecutionClientConfig.fill_model must be configured at runtime, not deserialized",
233 )),
234 }
235}
236
237fn serialize_latency_model<S>(
238 latency_model: &Option<LatencyModelAny>,
239 serializer: S,
240) -> Result<S::Ok, S::Error>
241where
242 S: Serializer,
243{
244 match latency_model {
245 None => serializer.serialize_none(),
246 Some(_) => Err(serde::ser::Error::custom(
247 "SandboxExecutionClientConfig.latency_model is runtime-only and cannot be serialized",
248 )),
249 }
250}
251
252fn deserialize_latency_model<'de, D>(deserializer: D) -> Result<Option<LatencyModelAny>, D::Error>
253where
254 D: Deserializer<'de>,
255{
256 let value = Option::<IgnoredAny>::deserialize(deserializer)?;
257
258 match value {
259 None => Ok(None),
260 Some(_) => Err(de::Error::custom(
261 "SandboxExecutionClientConfig.latency_model must be configured at runtime, not deserialized",
262 )),
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use nautilus_core::DurationNanos;
269 use nautilus_execution::models::{
270 fee::{FeeModelAny, ProbabilityPriceFeeModel},
271 fill::FillModelAny,
272 latency::{LatencyModelAny, StaticLatencyModel},
273 };
274 use rstest::rstest;
275 use rust_decimal_macros::dec;
276
277 use super::*;
278
279 #[rstest]
280 fn test_exec_config_toml_empty_uses_defaults() {
281 let config: SandboxExecutionClientConfig = toml::from_str("").unwrap();
282 let expected = SandboxExecutionClientConfig::default();
283 assert_eq!(config.account_id, expected.account_id);
284 assert_eq!(config.venue, expected.venue);
285 assert_eq!(config.oms_type, expected.oms_type);
286 assert_eq!(config.account_type, expected.account_type);
287 assert_eq!(config.default_leverage, expected.default_leverage);
288 assert_eq!(config.book_type, expected.book_type);
289 assert!(config.fee_model.is_none());
290 assert!(config.fill_model.is_none());
291 assert!(config.latency_model.is_none());
292 assert_eq!(config.bar_execution, expected.bar_execution);
293 assert_eq!(config.trade_execution, expected.trade_execution);
294 assert_eq!(config.use_position_ids, expected.use_position_ids);
295 assert!(!config.queue_position);
296 assert!(!config.liquidity_consumption);
297 assert!(!config.bar_adaptive_high_low_ordering);
298 assert!(!config.use_market_order_acks);
299 assert!(!config.oto_full_trigger);
300 assert_eq!(config.price_protection_points, 0);
301 }
302
303 #[rstest]
304 fn test_to_matching_engine_config_forwards_matching_knobs() {
305 let config = SandboxExecutionClientConfig {
306 queue_position: true,
307 liquidity_consumption: true,
308 bar_adaptive_high_low_ordering: true,
309 use_market_order_acks: true,
310 oto_full_trigger: true,
311 price_protection_points: 100,
312 ..SandboxExecutionClientConfig::default()
313 };
314 let engine_config = config.to_matching_engine_config();
315
316 assert!(engine_config.queue_position);
317 assert!(engine_config.liquidity_consumption);
318 assert!(engine_config.bar_adaptive_high_low_ordering);
319 assert!(engine_config.use_market_order_acks);
320 assert!(engine_config.oto_full_trigger);
321 assert_eq!(engine_config.price_protection_points, Some(100));
322 }
323
324 #[rstest]
325 fn test_exec_config_toml_rejects_fill_model_field() {
326 let result =
327 toml::from_str::<SandboxExecutionClientConfig>("fill_model = \"runtime-only\"");
328
329 assert!(result.is_err());
330 }
331
332 #[rstest]
333 fn test_exec_config_toml_rejects_fee_model_field() {
334 let result = toml::from_str::<SandboxExecutionClientConfig>("fee_model = \"runtime-only\"");
335
336 assert!(result.is_err());
337 }
338
339 #[rstest]
340 fn test_exec_config_toml_rejects_latency_model_field() {
341 let result =
342 toml::from_str::<SandboxExecutionClientConfig>("latency_model = \"runtime-only\"");
343
344 assert!(result.is_err());
345 }
346
347 #[rstest]
348 fn test_exec_config_toml_rejects_serializing_runtime_fee_model() {
349 let config = SandboxExecutionClientConfig {
350 fee_model: Some(FeeModelAny::ProbabilityPrice(
351 ProbabilityPriceFeeModel::new(dec!(0.01), dec!(0.02)),
352 )),
353 ..SandboxExecutionClientConfig::default()
354 };
355
356 let result = toml::Value::try_from(&config);
357
358 assert!(result.is_err());
359 }
360
361 #[rstest]
362 fn test_exec_config_toml_rejects_serializing_runtime_fill_model() {
363 let config = SandboxExecutionClientConfig {
364 fill_model: Some(FillModelAny::Default(Default::default())),
365 ..SandboxExecutionClientConfig::default()
366 };
367
368 let result = toml::Value::try_from(&config);
369
370 assert!(result.is_err());
371 }
372
373 #[rstest]
374 fn test_exec_config_toml_rejects_serializing_runtime_latency_model() {
375 let config = SandboxExecutionClientConfig {
376 latency_model: Some(LatencyModelAny::Static(StaticLatencyModel::new(
377 DurationNanos::ZERO,
378 DurationNanos::ZERO,
379 DurationNanos::ZERO,
380 DurationNanos::ZERO,
381 ))),
382 ..SandboxExecutionClientConfig::default()
383 };
384
385 let result = toml::Value::try_from(&config);
386
387 assert!(result.is_err());
388 }
389}