1use ahash::AHashMap;
19use nautilus_execution::{
20 matching_engine::config::OrderMatchingEngineConfig,
21 models::{fee::FeeModelAny, fill::FillModelAny},
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 #[builder(default)]
90 pub frozen_account: bool,
91 #[builder(default = true)]
93 pub bar_execution: bool,
94 #[builder(default = true)]
96 pub trade_execution: bool,
97 #[builder(default = true)]
99 pub reject_stop_orders: bool,
100 #[builder(default = true)]
102 pub support_gtd_orders: bool,
103 #[builder(default = true)]
105 pub support_contingent_orders: bool,
106 #[builder(default = true)]
108 pub use_position_ids: bool,
109 #[builder(default)]
112 pub use_random_ids: bool,
113 #[builder(default = true)]
115 pub use_reduce_only: bool,
116 #[builder(default)]
118 pub queue_position: bool,
119 #[builder(default)]
121 pub liquidity_consumption: bool,
122 #[builder(default)]
124 pub bar_adaptive_high_low_ordering: bool,
125 #[builder(default)]
127 pub use_market_order_acks: bool,
128 #[builder(default)]
130 pub oto_full_trigger: bool,
131 #[builder(default)]
135 pub price_protection_points: u32,
136}
137
138impl SandboxExecutionClientConfig {
139 #[must_use]
141 pub fn to_matching_engine_config(&self) -> OrderMatchingEngineConfig {
142 let price_protection = if self.price_protection_points == 0 {
143 None
144 } else {
145 Some(self.price_protection_points)
146 };
147
148 OrderMatchingEngineConfig::builder()
149 .bar_execution(self.bar_execution)
150 .bar_adaptive_high_low_ordering(self.bar_adaptive_high_low_ordering)
151 .trade_execution(self.trade_execution)
152 .liquidity_consumption(self.liquidity_consumption)
153 .reject_stop_orders(self.reject_stop_orders)
154 .support_gtd_orders(self.support_gtd_orders)
155 .support_contingent_orders(self.support_contingent_orders)
156 .use_position_ids(self.use_position_ids)
157 .use_random_ids(self.use_random_ids)
158 .use_reduce_only(self.use_reduce_only)
159 .use_market_order_acks(self.use_market_order_acks)
160 .queue_position(self.queue_position)
161 .oto_full_trigger(self.oto_full_trigger)
162 .maybe_price_protection_points(price_protection)
163 .build()
164 }
165}
166
167impl Default for SandboxExecutionClientConfig {
168 fn default() -> Self {
169 Self::builder().build()
170 }
171}
172
173fn serialize_fee_model<S>(fee_model: &Option<FeeModelAny>, serializer: S) -> Result<S::Ok, S::Error>
174where
175 S: Serializer,
176{
177 match fee_model {
178 None => serializer.serialize_none(),
179 Some(_) => Err(serde::ser::Error::custom(
180 "SandboxExecutionClientConfig.fee_model is runtime-only and cannot be serialized",
181 )),
182 }
183}
184
185fn deserialize_fee_model<'de, D>(deserializer: D) -> Result<Option<FeeModelAny>, D::Error>
186where
187 D: Deserializer<'de>,
188{
189 let value = Option::<IgnoredAny>::deserialize(deserializer)?;
190
191 match value {
192 None => Ok(None),
193 Some(_) => Err(de::Error::custom(
194 "SandboxExecutionClientConfig.fee_model must be configured at runtime, not deserialized",
195 )),
196 }
197}
198
199fn serialize_fill_model<S>(
200 fill_model: &Option<FillModelAny>,
201 serializer: S,
202) -> Result<S::Ok, S::Error>
203where
204 S: Serializer,
205{
206 match fill_model {
207 None => serializer.serialize_none(),
208 Some(_) => Err(serde::ser::Error::custom(
209 "SandboxExecutionClientConfig.fill_model is runtime-only and cannot be serialized",
210 )),
211 }
212}
213
214fn deserialize_fill_model<'de, D>(deserializer: D) -> Result<Option<FillModelAny>, D::Error>
215where
216 D: Deserializer<'de>,
217{
218 let value = Option::<IgnoredAny>::deserialize(deserializer)?;
219
220 match value {
221 None => Ok(None),
222 Some(_) => Err(de::Error::custom(
223 "SandboxExecutionClientConfig.fill_model must be configured at runtime, not deserialized",
224 )),
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use nautilus_execution::models::{
231 fee::{FeeModelAny, ProbabilityPriceFeeModel},
232 fill::FillModelAny,
233 };
234 use rstest::rstest;
235
236 use super::*;
237
238 #[rstest]
239 fn test_exec_config_toml_empty_uses_defaults() {
240 let config: SandboxExecutionClientConfig = toml::from_str("").unwrap();
241 let expected = SandboxExecutionClientConfig::default();
242 assert_eq!(config.account_id, expected.account_id);
243 assert_eq!(config.venue, expected.venue);
244 assert_eq!(config.oms_type, expected.oms_type);
245 assert_eq!(config.account_type, expected.account_type);
246 assert_eq!(config.default_leverage, expected.default_leverage);
247 assert_eq!(config.book_type, expected.book_type);
248 assert!(config.fee_model.is_none());
249 assert!(config.fill_model.is_none());
250 assert_eq!(config.bar_execution, expected.bar_execution);
251 assert_eq!(config.trade_execution, expected.trade_execution);
252 assert_eq!(config.use_position_ids, expected.use_position_ids);
253 assert!(!config.queue_position);
254 assert!(!config.liquidity_consumption);
255 assert!(!config.bar_adaptive_high_low_ordering);
256 assert!(!config.use_market_order_acks);
257 assert!(!config.oto_full_trigger);
258 assert_eq!(config.price_protection_points, 0);
259 }
260
261 #[rstest]
262 fn test_to_matching_engine_config_forwards_matching_knobs() {
263 let config = SandboxExecutionClientConfig {
264 queue_position: true,
265 liquidity_consumption: true,
266 bar_adaptive_high_low_ordering: true,
267 use_market_order_acks: true,
268 oto_full_trigger: true,
269 price_protection_points: 100,
270 ..SandboxExecutionClientConfig::default()
271 };
272 let engine_config = config.to_matching_engine_config();
273
274 assert!(engine_config.queue_position);
275 assert!(engine_config.liquidity_consumption);
276 assert!(engine_config.bar_adaptive_high_low_ordering);
277 assert!(engine_config.use_market_order_acks);
278 assert!(engine_config.oto_full_trigger);
279 assert_eq!(engine_config.price_protection_points, Some(100));
280 }
281
282 #[rstest]
283 fn test_exec_config_toml_rejects_fill_model_field() {
284 let result =
285 toml::from_str::<SandboxExecutionClientConfig>("fill_model = \"runtime-only\"");
286
287 assert!(result.is_err());
288 }
289
290 #[rstest]
291 fn test_exec_config_toml_rejects_fee_model_field() {
292 let result = toml::from_str::<SandboxExecutionClientConfig>("fee_model = \"runtime-only\"");
293
294 assert!(result.is_err());
295 }
296
297 #[rstest]
298 fn test_exec_config_toml_rejects_serializing_runtime_fee_model() {
299 let config = SandboxExecutionClientConfig {
300 fee_model: Some(FeeModelAny::ProbabilityPrice(ProbabilityPriceFeeModel)),
301 ..SandboxExecutionClientConfig::default()
302 };
303
304 let result = toml::Value::try_from(&config);
305
306 assert!(result.is_err());
307 }
308
309 #[rstest]
310 fn test_exec_config_toml_rejects_serializing_runtime_fill_model() {
311 let config = SandboxExecutionClientConfig {
312 fill_model: Some(FillModelAny::Default(Default::default())),
313 ..SandboxExecutionClientConfig::default()
314 };
315
316 let result = toml::Value::try_from(&config);
317
318 assert!(result.is_err());
319 }
320}