Skip to main content

nautilus_sandbox/
config.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Configuration for sandbox execution client.
17
18use 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/// Configuration for `SandboxExecutionClient` instances.
35#[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    /// The account ID for this client.
47    #[builder(default = AccountId::from("SANDBOX-001"))]
48    pub account_id: AccountId,
49    /// The venue for this sandbox execution client.
50    #[builder(default = Venue::new("SANDBOX"))]
51    pub venue: Venue,
52    /// The starting balances for this sandbox venue.
53    #[builder(default)]
54    pub starting_balances: Vec<Money>,
55    /// The base currency for this venue (None for multi-currency).
56    pub base_currency: Option<Currency>,
57    /// The order management system type used by the exchange.
58    #[builder(default = OmsType::Netting)]
59    pub oms_type: OmsType,
60    /// The account type for the client.
61    #[builder(default = AccountType::Margin)]
62    pub account_type: AccountType,
63    /// The account default leverage (for margin accounts).
64    #[builder(default = Decimal::ONE)]
65    pub default_leverage: Decimal,
66    /// Per-instrument leverage overrides.
67    #[builder(default)]
68    pub leverages: AHashMap<InstrumentId, Decimal>,
69    /// The order book type for the matching engine.
70    #[builder(default = BookType::L1_MBP)]
71    pub book_type: BookType,
72    /// The fee model for sandbox matching engines.
73    #[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    /// The fill model for sandbox matching engines.
81    #[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    /// The latency model for sandbox matching engines.
89    #[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    /// If True, account balances won't change (frozen).
97    #[builder(default)]
98    pub frozen_account: bool,
99    /// If bars should be processed by the matching engine (and move the market).
100    #[builder(default = true)]
101    pub bar_execution: bool,
102    /// If trades should be processed by the matching engine (and move the market).
103    #[builder(default = true)]
104    pub trade_execution: bool,
105    /// If stop orders are rejected on submission if trigger price is in the market.
106    #[builder(default = true)]
107    pub reject_stop_orders: bool,
108    /// If orders with GTD time in force will be supported by the venue.
109    #[builder(default = true)]
110    pub support_gtd_orders: bool,
111    /// If contingent orders will be supported/respected by the venue.
112    #[builder(default = true)]
113    pub support_contingent_orders: bool,
114    /// If venue position IDs will be generated on order fills.
115    #[builder(default = true)]
116    pub use_position_ids: bool,
117    /// If venue order IDs and position IDs will be random UUID4's.
118    /// Trade IDs are always deterministic and not affected by this flag.
119    #[builder(default)]
120    pub use_random_ids: bool,
121    /// If the `reduce_only` execution instruction on orders will be enforced.
122    /// If false, reduce-only orders are rejected.
123    #[builder(default = true)]
124    pub use_reduce_only: bool,
125    /// If limit order queue position tracking is enabled during trade execution.
126    #[builder(default)]
127    pub queue_position: bool,
128    /// If order book liquidity consumption should be tracked per level.
129    #[builder(default)]
130    pub liquidity_consumption: bool,
131    /// If bar high/low processing order adapts to the bar's shape.
132    #[builder(default)]
133    pub bar_adaptive_high_low_ordering: bool,
134    /// If `OrderAccepted` events should be generated for market orders.
135    #[builder(default)]
136    pub use_market_order_acks: bool,
137    /// If OTO child orders wait for a full parent fill before release.
138    #[builder(default)]
139    pub oto_full_trigger: bool,
140    /// Exchange-calculated price boundary for aggressive market fills.
141    ///
142    /// A value of `0` disables protection.
143    #[builder(default)]
144    pub price_protection_points: u32,
145}
146
147impl SandboxExecutionClientConfig {
148    /// Creates an [`OrderMatchingEngineConfig`] from this sandbox config.
149    #[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
276    use super::*;
277
278    #[rstest]
279    fn test_exec_config_toml_empty_uses_defaults() {
280        let config: SandboxExecutionClientConfig = toml::from_str("").unwrap();
281        let expected = SandboxExecutionClientConfig::default();
282        assert_eq!(config.account_id, expected.account_id);
283        assert_eq!(config.venue, expected.venue);
284        assert_eq!(config.oms_type, expected.oms_type);
285        assert_eq!(config.account_type, expected.account_type);
286        assert_eq!(config.default_leverage, expected.default_leverage);
287        assert_eq!(config.book_type, expected.book_type);
288        assert!(config.fee_model.is_none());
289        assert!(config.fill_model.is_none());
290        assert!(config.latency_model.is_none());
291        assert_eq!(config.bar_execution, expected.bar_execution);
292        assert_eq!(config.trade_execution, expected.trade_execution);
293        assert_eq!(config.use_position_ids, expected.use_position_ids);
294        assert!(!config.queue_position);
295        assert!(!config.liquidity_consumption);
296        assert!(!config.bar_adaptive_high_low_ordering);
297        assert!(!config.use_market_order_acks);
298        assert!(!config.oto_full_trigger);
299        assert_eq!(config.price_protection_points, 0);
300    }
301
302    #[rstest]
303    fn test_to_matching_engine_config_forwards_matching_knobs() {
304        let config = SandboxExecutionClientConfig {
305            queue_position: true,
306            liquidity_consumption: true,
307            bar_adaptive_high_low_ordering: true,
308            use_market_order_acks: true,
309            oto_full_trigger: true,
310            price_protection_points: 100,
311            ..SandboxExecutionClientConfig::default()
312        };
313        let engine_config = config.to_matching_engine_config();
314
315        assert!(engine_config.queue_position);
316        assert!(engine_config.liquidity_consumption);
317        assert!(engine_config.bar_adaptive_high_low_ordering);
318        assert!(engine_config.use_market_order_acks);
319        assert!(engine_config.oto_full_trigger);
320        assert_eq!(engine_config.price_protection_points, Some(100));
321    }
322
323    #[rstest]
324    fn test_exec_config_toml_rejects_fill_model_field() {
325        let result =
326            toml::from_str::<SandboxExecutionClientConfig>("fill_model = \"runtime-only\"");
327
328        assert!(result.is_err());
329    }
330
331    #[rstest]
332    fn test_exec_config_toml_rejects_fee_model_field() {
333        let result = toml::from_str::<SandboxExecutionClientConfig>("fee_model = \"runtime-only\"");
334
335        assert!(result.is_err());
336    }
337
338    #[rstest]
339    fn test_exec_config_toml_rejects_latency_model_field() {
340        let result =
341            toml::from_str::<SandboxExecutionClientConfig>("latency_model = \"runtime-only\"");
342
343        assert!(result.is_err());
344    }
345
346    #[rstest]
347    fn test_exec_config_toml_rejects_serializing_runtime_fee_model() {
348        let config = SandboxExecutionClientConfig {
349            fee_model: Some(FeeModelAny::ProbabilityPrice(ProbabilityPriceFeeModel)),
350            ..SandboxExecutionClientConfig::default()
351        };
352
353        let result = toml::Value::try_from(&config);
354
355        assert!(result.is_err());
356    }
357
358    #[rstest]
359    fn test_exec_config_toml_rejects_serializing_runtime_fill_model() {
360        let config = SandboxExecutionClientConfig {
361            fill_model: Some(FillModelAny::Default(Default::default())),
362            ..SandboxExecutionClientConfig::default()
363        };
364
365        let result = toml::Value::try_from(&config);
366
367        assert!(result.is_err());
368    }
369
370    #[rstest]
371    fn test_exec_config_toml_rejects_serializing_runtime_latency_model() {
372        let config = SandboxExecutionClientConfig {
373            latency_model: Some(LatencyModelAny::Static(StaticLatencyModel::new(
374                DurationNanos::ZERO,
375                DurationNanos::ZERO,
376                DurationNanos::ZERO,
377                DurationNanos::ZERO,
378            ))),
379            ..SandboxExecutionClientConfig::default()
380        };
381
382        let result = toml::Value::try_from(&config);
383
384        assert!(result.is_err());
385    }
386}