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},
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    /// If True, account balances won't change (frozen).
89    #[builder(default)]
90    pub frozen_account: bool,
91    /// If bars should be processed by the matching engine (and move the market).
92    #[builder(default = true)]
93    pub bar_execution: bool,
94    /// If trades should be processed by the matching engine (and move the market).
95    #[builder(default = true)]
96    pub trade_execution: bool,
97    /// If stop orders are rejected on submission if trigger price is in the market.
98    #[builder(default = true)]
99    pub reject_stop_orders: bool,
100    /// If orders with GTD time in force will be supported by the venue.
101    #[builder(default = true)]
102    pub support_gtd_orders: bool,
103    /// If contingent orders will be supported/respected by the venue.
104    #[builder(default = true)]
105    pub support_contingent_orders: bool,
106    /// If venue position IDs will be generated on order fills.
107    #[builder(default = true)]
108    pub use_position_ids: bool,
109    /// If venue order IDs and position IDs will be random UUID4's.
110    /// Trade IDs are always deterministic and not affected by this flag.
111    #[builder(default)]
112    pub use_random_ids: bool,
113    /// If the `reduce_only` execution instruction on orders will be honored.
114    #[builder(default = true)]
115    pub use_reduce_only: bool,
116    /// If limit order queue position tracking is enabled during trade execution.
117    #[builder(default)]
118    pub queue_position: bool,
119    /// If order book liquidity consumption should be tracked per level.
120    #[builder(default)]
121    pub liquidity_consumption: bool,
122    /// If bar high/low processing order adapts to the bar's shape.
123    #[builder(default)]
124    pub bar_adaptive_high_low_ordering: bool,
125    /// If `OrderAccepted` events should be generated for market orders.
126    #[builder(default)]
127    pub use_market_order_acks: bool,
128    /// If OTO child orders wait for a full parent fill before release.
129    #[builder(default)]
130    pub oto_full_trigger: bool,
131    /// Exchange-calculated price boundary for aggressive market fills.
132    ///
133    /// A value of `0` disables protection.
134    #[builder(default)]
135    pub price_protection_points: u32,
136}
137
138impl SandboxExecutionClientConfig {
139    /// Creates an [`OrderMatchingEngineConfig`] from this sandbox config.
140    #[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}