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, models::fee::FeeModelAny,
21};
22use nautilus_model::{
23    enums::{AccountType, BookType, OmsType},
24    identifiers::{AccountId, InstrumentId, TraderId, Venue},
25    types::{Currency, Money},
26};
27use rust_decimal::Decimal;
28use serde::{
29    Deserialize, Deserializer, Serialize, Serializer,
30    de::{self, IgnoredAny},
31};
32
33/// Configuration for `SandboxExecutionClient` instances.
34#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
35#[serde(default, deny_unknown_fields)]
36#[cfg_attr(
37    feature = "python",
38    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.sandbox", from_py_object)
39)]
40#[cfg_attr(
41    feature = "python",
42    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.sandbox")
43)]
44pub struct SandboxExecutionClientConfig {
45    /// The trader ID for this client.
46    #[builder(default = TraderId::from("SANDBOX-001"))]
47    pub trader_id: TraderId,
48    /// The account ID for this client.
49    #[builder(default = AccountId::from("SANDBOX-001"))]
50    pub account_id: AccountId,
51    /// The venue for this sandbox execution client.
52    #[builder(default = Venue::new("SANDBOX"))]
53    pub venue: Venue,
54    /// The starting balances for this sandbox venue.
55    #[builder(default)]
56    pub starting_balances: Vec<Money>,
57    /// The base currency for this venue (None for multi-currency).
58    pub base_currency: Option<Currency>,
59    /// The order management system type used by the exchange.
60    #[builder(default = OmsType::Netting)]
61    pub oms_type: OmsType,
62    /// The account type for the client.
63    #[builder(default = AccountType::Margin)]
64    pub account_type: AccountType,
65    /// The account default leverage (for margin accounts).
66    #[builder(default = Decimal::ONE)]
67    pub default_leverage: Decimal,
68    /// Per-instrument leverage overrides.
69    #[builder(default)]
70    pub leverages: AHashMap<InstrumentId, Decimal>,
71    /// The order book type for the matching engine.
72    #[builder(default = BookType::L1_MBP)]
73    pub book_type: BookType,
74    /// The fee model for sandbox matching engines.
75    #[serde(
76        default,
77        skip_serializing_if = "Option::is_none",
78        serialize_with = "serialize_fee_model",
79        deserialize_with = "deserialize_fee_model"
80    )]
81    pub fee_model: Option<FeeModelAny>,
82    /// If True, account balances won't change (frozen).
83    #[builder(default)]
84    pub frozen_account: bool,
85    /// If bars should be processed by the matching engine (and move the market).
86    #[builder(default = true)]
87    pub bar_execution: bool,
88    /// If trades should be processed by the matching engine (and move the market).
89    #[builder(default = true)]
90    pub trade_execution: bool,
91    /// If stop orders are rejected on submission if trigger price is in the market.
92    #[builder(default = true)]
93    pub reject_stop_orders: bool,
94    /// If orders with GTD time in force will be supported by the venue.
95    #[builder(default = true)]
96    pub support_gtd_orders: bool,
97    /// If contingent orders will be supported/respected by the venue.
98    #[builder(default = true)]
99    pub support_contingent_orders: bool,
100    /// If venue position IDs will be generated on order fills.
101    #[builder(default = true)]
102    pub use_position_ids: bool,
103    /// If venue order IDs and position IDs will be random UUID4's.
104    /// Trade IDs are always deterministic and not affected by this flag.
105    #[builder(default)]
106    pub use_random_ids: bool,
107    /// If the `reduce_only` execution instruction on orders will be honored.
108    #[builder(default = true)]
109    pub use_reduce_only: bool,
110}
111
112impl SandboxExecutionClientConfig {
113    /// Creates an [`OrderMatchingEngineConfig`] from this sandbox config.
114    #[must_use]
115    pub fn to_matching_engine_config(&self) -> OrderMatchingEngineConfig {
116        OrderMatchingEngineConfig::builder()
117            .bar_execution(self.bar_execution)
118            .trade_execution(self.trade_execution)
119            .reject_stop_orders(self.reject_stop_orders)
120            .support_gtd_orders(self.support_gtd_orders)
121            .support_contingent_orders(self.support_contingent_orders)
122            .use_position_ids(self.use_position_ids)
123            .use_random_ids(self.use_random_ids)
124            .use_reduce_only(self.use_reduce_only)
125            .build()
126    }
127}
128
129impl Default for SandboxExecutionClientConfig {
130    fn default() -> Self {
131        Self::builder().build()
132    }
133}
134
135fn serialize_fee_model<S>(fee_model: &Option<FeeModelAny>, serializer: S) -> Result<S::Ok, S::Error>
136where
137    S: Serializer,
138{
139    match fee_model {
140        None => serializer.serialize_none(),
141        Some(_) => Err(serde::ser::Error::custom(
142            "SandboxExecutionClientConfig.fee_model is runtime-only and cannot be serialized",
143        )),
144    }
145}
146
147fn deserialize_fee_model<'de, D>(deserializer: D) -> Result<Option<FeeModelAny>, D::Error>
148where
149    D: Deserializer<'de>,
150{
151    let value = Option::<IgnoredAny>::deserialize(deserializer)?;
152
153    match value {
154        None => Ok(None),
155        Some(_) => Err(de::Error::custom(
156            "SandboxExecutionClientConfig.fee_model must be configured at runtime, not deserialized",
157        )),
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use nautilus_execution::models::fee::{FeeModelAny, ProbabilityPriceFeeModel};
164    use rstest::rstest;
165
166    use super::*;
167
168    #[rstest]
169    fn test_exec_config_toml_empty_uses_defaults() {
170        let config: SandboxExecutionClientConfig = toml::from_str("").unwrap();
171        let expected = SandboxExecutionClientConfig::default();
172
173        assert_eq!(config.trader_id, expected.trader_id);
174        assert_eq!(config.account_id, expected.account_id);
175        assert_eq!(config.venue, expected.venue);
176        assert_eq!(config.oms_type, expected.oms_type);
177        assert_eq!(config.account_type, expected.account_type);
178        assert_eq!(config.default_leverage, expected.default_leverage);
179        assert_eq!(config.book_type, expected.book_type);
180        assert!(config.fee_model.is_none());
181        assert_eq!(config.bar_execution, expected.bar_execution);
182        assert_eq!(config.trade_execution, expected.trade_execution);
183        assert_eq!(config.use_position_ids, expected.use_position_ids);
184    }
185
186    #[rstest]
187    fn test_exec_config_toml_rejects_fee_model_field() {
188        let result = toml::from_str::<SandboxExecutionClientConfig>("fee_model = \"runtime-only\"");
189
190        assert!(result.is_err());
191    }
192
193    #[rstest]
194    fn test_exec_config_toml_rejects_serializing_runtime_fee_model() {
195        let config = SandboxExecutionClientConfig {
196            fee_model: Some(FeeModelAny::ProbabilityPrice(ProbabilityPriceFeeModel)),
197            ..SandboxExecutionClientConfig::default()
198        };
199
200        let result = toml::Value::try_from(&config);
201
202        assert!(result.is_err());
203    }
204}