Skip to main content

nautilus_testkit/testers/exec/
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
16use std::num::NonZeroUsize;
17
18use nautilus_core::Params;
19use nautilus_model::{
20    enums::{BookType, OrderType, TimeInForce, TrailingOffsetType, TriggerType},
21    identifiers::{ClientId, InstrumentId, StrategyId},
22    types::Quantity,
23};
24use nautilus_trading::strategy::StrategyConfig;
25use rust_decimal::Decimal;
26use serde::{Deserialize, Serialize};
27
28/// Configuration for the execution tester strategy.
29#[derive(Debug, Clone, Deserialize, Serialize, bon::Builder)]
30#[serde(default, deny_unknown_fields)]
31pub struct ExecTesterConfig {
32    /// Base strategy configuration.
33    #[builder(default)]
34    pub base: StrategyConfig,
35    /// Instrument ID to test.
36    #[builder(default = InstrumentId::from("BTCUSDT-PERP.BINANCE"))]
37    pub instrument_id: InstrumentId,
38    /// Order quantity.
39    #[builder(default = Quantity::from("0.001"))]
40    pub order_qty: Quantity,
41    /// Display quantity for iceberg orders (None for full display, Some(0) for hidden).
42    pub order_display_qty: Option<Quantity>,
43    /// Minutes until GTD orders expire (None for GTC).
44    pub order_expire_time_delta_mins: Option<u64>,
45    /// Adapter-specific order parameters.
46    pub order_params: Option<Params>,
47    /// Client ID to use for orders and subscriptions.
48    pub client_id: Option<ClientId>,
49    /// Whether to subscribe to order book.
50    #[builder(default = false)]
51    pub subscribe_book: bool,
52    /// Whether to subscribe to quotes.
53    #[builder(default = true)]
54    pub subscribe_quotes: bool,
55    /// Whether to subscribe to trades.
56    #[builder(default = true)]
57    pub subscribe_trades: bool,
58    /// Book type for order book subscriptions.
59    #[builder(default = BookType::L2_MBP)]
60    pub book_type: BookType,
61    /// Order book depth for subscriptions.
62    pub book_depth: Option<NonZeroUsize>,
63    /// Order book interval in milliseconds.
64    #[builder(default = NonZeroUsize::new(1000).unwrap())]
65    pub book_interval_ms: NonZeroUsize,
66    /// Number of order book levels to print when logging.
67    #[builder(default = 10)]
68    pub book_levels_to_print: usize,
69    /// Quantity to open position on start (positive for buy, negative for sell).
70    pub open_position_on_start_qty: Option<Decimal>,
71    /// Time in force for opening position order.
72    #[builder(default = TimeInForce::Gtc)]
73    pub open_position_time_in_force: TimeInForce,
74    /// Enable limit buy orders.
75    #[builder(default = true)]
76    pub enable_limit_buys: bool,
77    /// Enable limit sell orders.
78    #[builder(default = true)]
79    pub enable_limit_sells: bool,
80    /// Enable stop buy orders.
81    #[builder(default = false)]
82    pub enable_stop_buys: bool,
83    /// Enable stop sell orders.
84    #[builder(default = false)]
85    pub enable_stop_sells: bool,
86    /// Offset from TOB in price ticks for limit orders.
87    #[builder(default = 500)]
88    pub tob_offset_ticks: u64,
89    /// Override time in force for limit orders (None uses GTC/GTD logic).
90    pub limit_time_in_force: Option<TimeInForce>,
91    /// Type of stop order (STOP_MARKET, STOP_LIMIT, MARKET_IF_TOUCHED, LIMIT_IF_TOUCHED).
92    #[builder(default = OrderType::StopMarket)]
93    pub stop_order_type: OrderType,
94    /// Offset from market in price ticks for stop trigger.
95    #[builder(default = 100)]
96    pub stop_offset_ticks: u64,
97    /// Offset from trigger price in ticks for stop limit price.
98    pub stop_limit_offset_ticks: Option<u64>,
99    /// Trigger type for stop orders.
100    #[builder(default = TriggerType::Default)]
101    pub stop_trigger_type: TriggerType,
102    /// Override time in force for stop orders (None uses GTC/GTD logic).
103    pub stop_time_in_force: Option<TimeInForce>,
104    /// Trailing offset for TRAILING_STOP_MARKET orders.
105    pub trailing_offset: Option<Decimal>,
106    /// Trailing offset type (BasisPoints or Price).
107    #[builder(default = TrailingOffsetType::BasisPoints)]
108    pub trailing_offset_type: TrailingOffsetType,
109    /// Enable bracket orders (entry with TP/SL).
110    #[builder(default = false)]
111    pub enable_brackets: bool,
112    /// Submit limit buy and sell as an order list instead of individual orders.
113    #[builder(default = false)]
114    pub batch_submit_limit_pair: bool,
115    /// Entry order type for bracket orders.
116    #[builder(default = OrderType::Limit)]
117    pub bracket_entry_order_type: OrderType,
118    /// Offset in ticks for bracket TP/SL from entry price.
119    #[builder(default = 500)]
120    pub bracket_offset_ticks: u64,
121    /// Modify limit orders to maintain TOB offset.
122    #[builder(default = false)]
123    pub modify_orders_to_maintain_tob_offset: bool,
124    /// Modify stop orders to maintain offset.
125    #[builder(default = false)]
126    pub modify_stop_orders_to_maintain_offset: bool,
127    /// Cancel and replace limit orders to maintain TOB offset.
128    #[builder(default = false)]
129    pub cancel_replace_orders_to_maintain_tob_offset: bool,
130    /// Cancel and replace stop orders to maintain offset.
131    #[builder(default = false)]
132    pub cancel_replace_stop_orders_to_maintain_offset: bool,
133    /// Use post-only for limit orders.
134    #[builder(default = false)]
135    pub use_post_only: bool,
136    /// Place limit orders at marketable prices (cross the spread). Combined
137    /// with `limit_time_in_force = Ioc`/`Fok`, exercises aggressive-fill
138    /// (TC-E13, TC-E15) and passive-no-fill (TC-E14, TC-E16) scenarios when
139    /// inverted with the standard passive offset.
140    #[builder(default = false)]
141    pub limit_aggressive: bool,
142    /// Use quote quantity for orders.
143    #[builder(default = false)]
144    pub use_quote_quantity: bool,
145    /// Emulation trigger type for orders.
146    pub emulation_trigger: Option<TriggerType>,
147    /// Cancel all orders on stop.
148    #[builder(default = true)]
149    pub cancel_orders_on_stop: bool,
150    /// Close all positions on stop.
151    #[builder(default = true)]
152    pub close_positions_on_stop: bool,
153    /// Time in force for closing positions (None defaults to GTC).
154    pub close_positions_time_in_force: Option<TimeInForce>,
155    /// Use reduce_only when closing positions.
156    #[builder(default = true)]
157    pub reduce_only_on_stop: bool,
158    /// Use individual cancel commands instead of cancel_all.
159    #[builder(default = false)]
160    pub use_individual_cancels_on_stop: bool,
161    /// Use batch cancel command when stopping.
162    #[builder(default = false)]
163    pub use_batch_cancel_on_stop: bool,
164    /// Dry run mode (no order submission).
165    #[builder(default = false)]
166    pub dry_run: bool,
167    /// Log received data.
168    #[builder(default = true)]
169    pub log_data: bool,
170    /// Test post-only rejection by placing orders on wrong side of spread.
171    #[builder(default = false)]
172    pub test_reject_post_only: bool,
173    /// Test reduce-only rejection by setting reduce_only on open position order.
174    #[builder(default = false)]
175    pub test_reject_reduce_only: bool,
176    /// Programmatically attempt one strategy-wide modify against the next
177    /// accepted limit order (whichever side acks first) to exercise the
178    /// adapter's modify-rejection path (TC-E36). Independent of
179    /// `modify_orders_to_maintain_tob_offset`, which only fires on price drift.
180    /// Not honored when `batch_submit_limit_pair` is true; combine with
181    /// individual buy/sell maintenance instead.
182    #[builder(default = false)]
183    pub test_modify_rejected: bool,
184    /// Whether unsubscribe is supported on stop.
185    #[builder(default = true)]
186    pub can_unsubscribe: bool,
187}
188
189impl ExecTesterConfig {
190    /// Creates a new [`ExecTesterConfig`] with minimal settings.
191    ///
192    /// # Panics
193    ///
194    /// Panics if `NonZeroUsize::new(1000)` fails (which should never happen).
195    #[must_use]
196    pub fn new(
197        strategy_id: StrategyId,
198        instrument_id: InstrumentId,
199        client_id: ClientId,
200        order_qty: Quantity,
201    ) -> Self {
202        Self {
203            base: StrategyConfig {
204                strategy_id: Some(strategy_id),
205                order_id_tag: None,
206                ..Default::default()
207            },
208            instrument_id,
209            order_qty,
210            order_display_qty: None,
211            order_expire_time_delta_mins: None,
212            order_params: None,
213            client_id: Some(client_id),
214            subscribe_quotes: true,
215            subscribe_trades: true,
216            subscribe_book: false,
217            book_type: BookType::L2_MBP,
218            book_depth: None,
219            book_interval_ms: NonZeroUsize::new(1000).unwrap(),
220            book_levels_to_print: 10,
221            open_position_on_start_qty: None,
222            open_position_time_in_force: TimeInForce::Gtc,
223            enable_limit_buys: true,
224            enable_limit_sells: true,
225            enable_stop_buys: false,
226            enable_stop_sells: false,
227            tob_offset_ticks: 500,
228            limit_time_in_force: None,
229            stop_order_type: OrderType::StopMarket,
230            stop_offset_ticks: 100,
231            stop_limit_offset_ticks: None,
232            stop_trigger_type: TriggerType::Default,
233            stop_time_in_force: None,
234            trailing_offset: None,
235            trailing_offset_type: TrailingOffsetType::BasisPoints,
236            enable_brackets: false,
237            batch_submit_limit_pair: false,
238            bracket_entry_order_type: OrderType::Limit,
239            bracket_offset_ticks: 500,
240            modify_orders_to_maintain_tob_offset: false,
241            modify_stop_orders_to_maintain_offset: false,
242            cancel_replace_orders_to_maintain_tob_offset: false,
243            cancel_replace_stop_orders_to_maintain_offset: false,
244            use_post_only: false,
245            limit_aggressive: false,
246            use_quote_quantity: false,
247            emulation_trigger: None,
248            cancel_orders_on_stop: true,
249            close_positions_on_stop: true,
250            close_positions_time_in_force: None,
251            reduce_only_on_stop: true,
252            use_individual_cancels_on_stop: false,
253            use_batch_cancel_on_stop: false,
254            dry_run: false,
255            log_data: true,
256            test_reject_post_only: false,
257            test_reject_reduce_only: false,
258            test_modify_rejected: false,
259            can_unsubscribe: true,
260        }
261    }
262}
263
264impl Default for ExecTesterConfig {
265    fn default() -> Self {
266        Self::builder().build()
267    }
268}