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