Skip to main content

nautilus_testkit/testers/
exec.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//! Execution tester strategy for live testing order execution.
17
18use std::{
19    num::NonZeroUsize,
20    ops::{Deref, DerefMut},
21};
22
23use nautilus_common::{
24    actor::{DataActor, DataActorCore},
25    enums::LogColor,
26    log_info, log_warn,
27    timer::TimeEvent,
28};
29use nautilus_core::{Params, UnixNanos, datetime::secs_to_nanos_unchecked};
30use nautilus_model::{
31    data::{Bar, IndexPriceUpdate, MarkPriceUpdate, OrderBookDeltas, QuoteTick, TradeTick},
32    enums::{BookType, OrderSide, OrderType, TimeInForce, TriggerType},
33    identifiers::{ClientId, InstrumentId, StrategyId},
34    instruments::{Instrument, InstrumentAny},
35    orderbook::OrderBook,
36    orders::{Order, OrderAny},
37    types::{Price, Quantity},
38};
39use nautilus_trading::strategy::{Strategy, StrategyConfig, StrategyCore};
40use rust_decimal::{Decimal, prelude::ToPrimitive};
41
42/// Configuration for the execution tester strategy.
43#[derive(Debug, Clone)]
44pub struct ExecTesterConfig {
45    /// Base strategy configuration.
46    pub base: StrategyConfig,
47    /// Instrument ID to test.
48    pub instrument_id: InstrumentId,
49    /// Order quantity.
50    pub order_qty: Quantity,
51    /// Display quantity for iceberg orders (None for full display, Some(0) for hidden).
52    pub order_display_qty: Option<Quantity>,
53    /// Minutes until GTD orders expire (None for GTC).
54    pub order_expire_time_delta_mins: Option<u64>,
55    /// Adapter-specific order parameters.
56    pub order_params: Option<Params>,
57    /// Client ID to use for orders and subscriptions.
58    pub client_id: Option<ClientId>,
59    /// Whether to subscribe to order book.
60    pub subscribe_book: bool,
61    /// Whether to subscribe to quotes.
62    pub subscribe_quotes: bool,
63    /// Whether to subscribe to trades.
64    pub subscribe_trades: bool,
65    /// Book type for order book subscriptions.
66    pub book_type: BookType,
67    /// Order book depth for subscriptions.
68    pub book_depth: Option<NonZeroUsize>,
69    /// Order book interval in milliseconds.
70    pub book_interval_ms: NonZeroUsize,
71    /// Number of order book levels to print when logging.
72    pub book_levels_to_print: usize,
73    /// Quantity to open position on start (positive for buy, negative for sell).
74    pub open_position_on_start_qty: Option<Decimal>,
75    /// Time in force for opening position order.
76    pub open_position_time_in_force: TimeInForce,
77    /// Enable limit buy orders.
78    pub enable_limit_buys: bool,
79    /// Enable limit sell orders.
80    pub enable_limit_sells: bool,
81    /// Enable stop buy orders.
82    pub enable_stop_buys: bool,
83    /// Enable stop sell orders.
84    pub enable_stop_sells: bool,
85    /// Offset from TOB in price ticks for limit orders.
86    pub tob_offset_ticks: u64,
87    /// Override time in force for limit orders (None uses GTC/GTD logic).
88    pub limit_time_in_force: Option<TimeInForce>,
89    /// Type of stop order (STOP_MARKET, STOP_LIMIT, MARKET_IF_TOUCHED, LIMIT_IF_TOUCHED).
90    pub stop_order_type: OrderType,
91    /// Offset from market in price ticks for stop trigger.
92    pub stop_offset_ticks: u64,
93    /// Offset from trigger price in ticks for stop limit price.
94    pub stop_limit_offset_ticks: Option<u64>,
95    /// Trigger type for stop orders.
96    pub stop_trigger_type: TriggerType,
97    /// Override time in force for stop orders (None uses GTC/GTD logic).
98    pub stop_time_in_force: Option<TimeInForce>,
99    /// Enable bracket orders (entry with TP/SL).
100    pub enable_brackets: bool,
101    /// Entry order type for bracket orders.
102    pub bracket_entry_order_type: OrderType,
103    /// Offset in ticks for bracket TP/SL from entry price.
104    pub bracket_offset_ticks: u64,
105    /// Modify limit orders to maintain TOB offset.
106    pub modify_orders_to_maintain_tob_offset: bool,
107    /// Modify stop orders to maintain offset.
108    pub modify_stop_orders_to_maintain_offset: bool,
109    /// Cancel and replace limit orders to maintain TOB offset.
110    pub cancel_replace_orders_to_maintain_tob_offset: bool,
111    /// Cancel and replace stop orders to maintain offset.
112    pub cancel_replace_stop_orders_to_maintain_offset: bool,
113    /// Use post-only for limit orders.
114    pub use_post_only: bool,
115    /// Use quote quantity for orders.
116    pub use_quote_quantity: bool,
117    /// Emulation trigger type for orders.
118    pub emulation_trigger: Option<TriggerType>,
119    /// Cancel all orders on stop.
120    pub cancel_orders_on_stop: bool,
121    /// Close all positions on stop.
122    pub close_positions_on_stop: bool,
123    /// Time in force for closing positions (None defaults to GTC).
124    pub close_positions_time_in_force: Option<TimeInForce>,
125    /// Use reduce_only when closing positions.
126    pub reduce_only_on_stop: bool,
127    /// Use individual cancel commands instead of cancel_all.
128    pub use_individual_cancels_on_stop: bool,
129    /// Use batch cancel command when stopping.
130    pub use_batch_cancel_on_stop: bool,
131    /// Dry run mode (no order submission).
132    pub dry_run: bool,
133    /// Log received data.
134    pub log_data: bool,
135    /// Test post-only rejection by placing orders on wrong side of spread.
136    pub test_reject_post_only: bool,
137    /// Test reduce-only rejection by setting reduce_only on open position order.
138    pub test_reject_reduce_only: bool,
139    /// Whether unsubscribe is supported on stop.
140    pub can_unsubscribe: bool,
141}
142
143impl ExecTesterConfig {
144    /// Creates a new [`ExecTesterConfig`] with minimal settings.
145    ///
146    /// # Panics
147    ///
148    /// Panics if `NonZeroUsize::new(1000)` fails (which should never happen).
149    #[must_use]
150    pub fn new(
151        strategy_id: StrategyId,
152        instrument_id: InstrumentId,
153        client_id: ClientId,
154        order_qty: Quantity,
155    ) -> Self {
156        Self {
157            base: StrategyConfig {
158                strategy_id: Some(strategy_id),
159                order_id_tag: None,
160                ..Default::default()
161            },
162            instrument_id,
163            order_qty,
164            order_display_qty: None,
165            order_expire_time_delta_mins: None,
166            order_params: None,
167            client_id: Some(client_id),
168            subscribe_quotes: true,
169            subscribe_trades: true,
170            subscribe_book: false,
171            book_type: BookType::L2_MBP,
172            book_depth: None,
173            book_interval_ms: NonZeroUsize::new(1000).unwrap(),
174            book_levels_to_print: 10,
175            open_position_on_start_qty: None,
176            open_position_time_in_force: TimeInForce::Gtc,
177            enable_limit_buys: true,
178            enable_limit_sells: true,
179            enable_stop_buys: false,
180            enable_stop_sells: false,
181            tob_offset_ticks: 500,
182            limit_time_in_force: None,
183            stop_order_type: OrderType::StopMarket,
184            stop_offset_ticks: 100,
185            stop_limit_offset_ticks: None,
186            stop_trigger_type: TriggerType::Default,
187            stop_time_in_force: None,
188            enable_brackets: false,
189            bracket_entry_order_type: OrderType::Limit,
190            bracket_offset_ticks: 500,
191            modify_orders_to_maintain_tob_offset: false,
192            modify_stop_orders_to_maintain_offset: false,
193            cancel_replace_orders_to_maintain_tob_offset: false,
194            cancel_replace_stop_orders_to_maintain_offset: false,
195            use_post_only: false,
196            use_quote_quantity: false,
197            emulation_trigger: None,
198            cancel_orders_on_stop: true,
199            close_positions_on_stop: true,
200            close_positions_time_in_force: None,
201            reduce_only_on_stop: true,
202            use_individual_cancels_on_stop: false,
203            use_batch_cancel_on_stop: false,
204            dry_run: false,
205            log_data: true,
206            test_reject_post_only: false,
207            test_reject_reduce_only: false,
208            can_unsubscribe: true,
209        }
210    }
211
212    #[must_use]
213    pub fn with_log_data(mut self, log_data: bool) -> Self {
214        self.log_data = log_data;
215        self
216    }
217
218    #[must_use]
219    pub fn with_dry_run(mut self, dry_run: bool) -> Self {
220        self.dry_run = dry_run;
221        self
222    }
223
224    #[must_use]
225    pub fn with_subscribe_quotes(mut self, subscribe: bool) -> Self {
226        self.subscribe_quotes = subscribe;
227        self
228    }
229
230    #[must_use]
231    pub fn with_subscribe_trades(mut self, subscribe: bool) -> Self {
232        self.subscribe_trades = subscribe;
233        self
234    }
235
236    #[must_use]
237    pub fn with_subscribe_book(mut self, subscribe: bool) -> Self {
238        self.subscribe_book = subscribe;
239        self
240    }
241
242    #[must_use]
243    pub fn with_book_type(mut self, book_type: BookType) -> Self {
244        self.book_type = book_type;
245        self
246    }
247
248    #[must_use]
249    pub fn with_book_depth(mut self, depth: Option<NonZeroUsize>) -> Self {
250        self.book_depth = depth;
251        self
252    }
253
254    #[must_use]
255    pub fn with_enable_limit_buys(mut self, enable: bool) -> Self {
256        self.enable_limit_buys = enable;
257        self
258    }
259
260    #[must_use]
261    pub fn with_enable_limit_sells(mut self, enable: bool) -> Self {
262        self.enable_limit_sells = enable;
263        self
264    }
265
266    #[must_use]
267    pub fn with_enable_stop_buys(mut self, enable: bool) -> Self {
268        self.enable_stop_buys = enable;
269        self
270    }
271
272    #[must_use]
273    pub fn with_enable_stop_sells(mut self, enable: bool) -> Self {
274        self.enable_stop_sells = enable;
275        self
276    }
277
278    #[must_use]
279    pub fn with_tob_offset_ticks(mut self, ticks: u64) -> Self {
280        self.tob_offset_ticks = ticks;
281        self
282    }
283
284    #[must_use]
285    pub fn with_stop_order_type(mut self, order_type: OrderType) -> Self {
286        self.stop_order_type = order_type;
287        self
288    }
289
290    #[must_use]
291    pub fn with_stop_offset_ticks(mut self, ticks: u64) -> Self {
292        self.stop_offset_ticks = ticks;
293        self
294    }
295
296    #[must_use]
297    pub fn with_use_post_only(mut self, use_post_only: bool) -> Self {
298        self.use_post_only = use_post_only;
299        self
300    }
301
302    #[must_use]
303    pub fn with_open_position_on_start(mut self, qty: Decimal) -> Self {
304        self.open_position_on_start_qty = Some(qty);
305        self
306    }
307
308    #[must_use]
309    pub fn with_cancel_orders_on_stop(mut self, cancel: bool) -> Self {
310        self.cancel_orders_on_stop = cancel;
311        self
312    }
313
314    #[must_use]
315    pub fn with_close_positions_on_stop(mut self, close: bool) -> Self {
316        self.close_positions_on_stop = close;
317        self
318    }
319
320    #[must_use]
321    pub fn with_close_positions_time_in_force(
322        mut self,
323        time_in_force: Option<TimeInForce>,
324    ) -> Self {
325        self.close_positions_time_in_force = time_in_force;
326        self
327    }
328
329    #[must_use]
330    pub fn with_use_batch_cancel_on_stop(mut self, use_batch: bool) -> Self {
331        self.use_batch_cancel_on_stop = use_batch;
332        self
333    }
334
335    #[must_use]
336    pub fn with_can_unsubscribe(mut self, can_unsubscribe: bool) -> Self {
337        self.can_unsubscribe = can_unsubscribe;
338        self
339    }
340
341    #[must_use]
342    pub fn with_enable_brackets(mut self, enable: bool) -> Self {
343        self.enable_brackets = enable;
344        self
345    }
346
347    #[must_use]
348    pub fn with_bracket_entry_order_type(mut self, order_type: OrderType) -> Self {
349        self.bracket_entry_order_type = order_type;
350        self
351    }
352
353    #[must_use]
354    pub fn with_bracket_offset_ticks(mut self, ticks: u64) -> Self {
355        self.bracket_offset_ticks = ticks;
356        self
357    }
358
359    #[must_use]
360    pub fn with_test_reject_post_only(mut self, test: bool) -> Self {
361        self.test_reject_post_only = test;
362        self
363    }
364
365    #[must_use]
366    pub fn with_test_reject_reduce_only(mut self, test: bool) -> Self {
367        self.test_reject_reduce_only = test;
368        self
369    }
370
371    #[must_use]
372    pub fn with_emulation_trigger(mut self, trigger: Option<TriggerType>) -> Self {
373        self.emulation_trigger = trigger;
374        self
375    }
376
377    #[must_use]
378    pub fn with_use_quote_quantity(mut self, use_quote: bool) -> Self {
379        self.use_quote_quantity = use_quote;
380        self
381    }
382
383    #[must_use]
384    pub fn with_order_params(mut self, params: Option<Params>) -> Self {
385        self.order_params = params;
386        self
387    }
388
389    #[must_use]
390    pub fn with_limit_time_in_force(mut self, tif: Option<TimeInForce>) -> Self {
391        self.limit_time_in_force = tif;
392        self
393    }
394
395    #[must_use]
396    pub fn with_stop_time_in_force(mut self, tif: Option<TimeInForce>) -> Self {
397        self.stop_time_in_force = tif;
398        self
399    }
400}
401
402impl Default for ExecTesterConfig {
403    fn default() -> Self {
404        Self {
405            base: StrategyConfig::default(),
406            instrument_id: InstrumentId::from("BTCUSDT-PERP.BINANCE"),
407            order_qty: Quantity::from("0.001"),
408            order_display_qty: None,
409            order_expire_time_delta_mins: None,
410            order_params: None,
411            client_id: None,
412            subscribe_quotes: true,
413            subscribe_trades: true,
414            subscribe_book: false,
415            book_type: BookType::L2_MBP,
416            book_depth: None,
417            book_interval_ms: NonZeroUsize::new(1000).unwrap(),
418            book_levels_to_print: 10,
419            open_position_on_start_qty: None,
420            open_position_time_in_force: TimeInForce::Gtc,
421            enable_limit_buys: true,
422            enable_limit_sells: true,
423            enable_stop_buys: false,
424            enable_stop_sells: false,
425            tob_offset_ticks: 500,
426            limit_time_in_force: None,
427            stop_order_type: OrderType::StopMarket,
428            stop_offset_ticks: 100,
429            stop_limit_offset_ticks: None,
430            stop_trigger_type: TriggerType::Default,
431            stop_time_in_force: None,
432            enable_brackets: false,
433            bracket_entry_order_type: OrderType::Limit,
434            bracket_offset_ticks: 500,
435            modify_orders_to_maintain_tob_offset: false,
436            modify_stop_orders_to_maintain_offset: false,
437            cancel_replace_orders_to_maintain_tob_offset: false,
438            cancel_replace_stop_orders_to_maintain_offset: false,
439            use_post_only: false,
440            use_quote_quantity: false,
441            emulation_trigger: None,
442            cancel_orders_on_stop: true,
443            close_positions_on_stop: true,
444            close_positions_time_in_force: None,
445            reduce_only_on_stop: true,
446            use_individual_cancels_on_stop: false,
447            use_batch_cancel_on_stop: false,
448            dry_run: false,
449            log_data: true,
450            test_reject_post_only: false,
451            test_reject_reduce_only: false,
452            can_unsubscribe: true,
453        }
454    }
455}
456
457/// An execution tester strategy for live testing order execution functionality.
458///
459/// This strategy is designed for testing execution adapters by submitting
460/// limit orders, stop orders, and managing positions. It can maintain orders
461/// at a configurable offset from the top of book.
462///
463/// **WARNING**: This strategy has no alpha advantage whatsoever.
464/// It is not intended to be used for live trading with real money.
465#[derive(Debug)]
466pub struct ExecTester {
467    core: StrategyCore,
468    config: ExecTesterConfig,
469    instrument: Option<InstrumentAny>,
470    price_offset: Option<f64>,
471    preinitialized_market_data: bool,
472
473    // Order tracking
474    buy_order: Option<OrderAny>,
475    sell_order: Option<OrderAny>,
476    buy_stop_order: Option<OrderAny>,
477    sell_stop_order: Option<OrderAny>,
478}
479
480impl Deref for ExecTester {
481    type Target = DataActorCore;
482
483    fn deref(&self) -> &Self::Target {
484        &self.core
485    }
486}
487
488impl DerefMut for ExecTester {
489    fn deref_mut(&mut self) -> &mut Self::Target {
490        &mut self.core
491    }
492}
493
494impl DataActor for ExecTester {
495    fn on_start(&mut self) -> anyhow::Result<()> {
496        Strategy::on_start(self)?;
497
498        let instrument_id = self.config.instrument_id;
499        let client_id = self.config.client_id;
500
501        let instrument = {
502            let cache = self.cache();
503            cache.instrument(&instrument_id).cloned()
504        };
505
506        if let Some(inst) = instrument {
507            self.initialize_with_instrument(inst, true)?;
508        } else {
509            log::info!("Instrument {instrument_id} not in cache, subscribing...");
510            self.subscribe_instrument(instrument_id, client_id, None);
511
512            // Also subscribe to market data to trigger instrument definitions from data providers
513            // (e.g., Databento sends instrument definitions as part of market data subscriptions)
514            if self.config.subscribe_quotes {
515                self.subscribe_quotes(instrument_id, client_id, None);
516            }
517            if self.config.subscribe_trades {
518                self.subscribe_trades(instrument_id, client_id, None);
519            }
520            self.preinitialized_market_data =
521                self.config.subscribe_quotes || self.config.subscribe_trades;
522        }
523
524        Ok(())
525    }
526
527    fn on_instrument(&mut self, instrument: &InstrumentAny) -> anyhow::Result<()> {
528        if instrument.id() == self.config.instrument_id && self.instrument.is_none() {
529            let id = instrument.id();
530            log::info!("Received instrument {id}, initializing...");
531            self.initialize_with_instrument(instrument.clone(), !self.preinitialized_market_data)?;
532        }
533        Ok(())
534    }
535
536    fn on_stop(&mut self) -> anyhow::Result<()> {
537        if self.config.dry_run {
538            log_warn!("Dry run mode, skipping cancel all orders and close all positions");
539            return Ok(());
540        }
541
542        let instrument_id = self.config.instrument_id;
543        let client_id = self.config.client_id;
544
545        if self.config.cancel_orders_on_stop {
546            let strategy_id = StrategyId::from(self.core.actor_id.inner().as_str());
547            if self.config.use_individual_cancels_on_stop {
548                let cache = self.cache();
549                let open_orders: Vec<OrderAny> = cache
550                    .orders_open(None, Some(&instrument_id), Some(&strategy_id), None, None)
551                    .iter()
552                    .map(|o| (*o).clone())
553                    .collect();
554                drop(cache);
555
556                for order in open_orders {
557                    if let Err(e) = self.cancel_order(order, client_id) {
558                        log::error!("Failed to cancel order: {e}");
559                    }
560                }
561            } else if self.config.use_batch_cancel_on_stop {
562                let cache = self.cache();
563                let open_orders: Vec<OrderAny> = cache
564                    .orders_open(None, Some(&instrument_id), Some(&strategy_id), None, None)
565                    .iter()
566                    .map(|o| (*o).clone())
567                    .collect();
568                drop(cache);
569
570                if let Err(e) = self.cancel_orders(open_orders, client_id, None) {
571                    log::error!("Failed to batch cancel orders: {e}");
572                }
573            } else if let Err(e) = self.cancel_all_orders(instrument_id, None, client_id) {
574                log::error!("Failed to cancel all orders: {e}");
575            }
576        }
577
578        if self.config.close_positions_on_stop {
579            let time_in_force = self
580                .config
581                .close_positions_time_in_force
582                .or(Some(TimeInForce::Gtc));
583            if let Err(e) = self.close_all_positions(
584                instrument_id,
585                None,
586                client_id,
587                None,
588                time_in_force,
589                Some(self.config.reduce_only_on_stop),
590                None,
591            ) {
592                log::error!("Failed to close all positions: {e}");
593            }
594        }
595
596        if self.config.can_unsubscribe && self.instrument.is_some() {
597            if self.config.subscribe_quotes {
598                self.unsubscribe_quotes(instrument_id, client_id, None);
599            }
600
601            if self.config.subscribe_trades {
602                self.unsubscribe_trades(instrument_id, client_id, None);
603            }
604
605            if self.config.subscribe_book {
606                self.unsubscribe_book_at_interval(
607                    instrument_id,
608                    self.config.book_interval_ms,
609                    client_id,
610                    None,
611                );
612            }
613        }
614
615        Ok(())
616    }
617
618    fn on_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
619        if self.config.log_data {
620            log_info!("{quote:?}", color = LogColor::Cyan);
621        }
622
623        self.maintain_orders(quote.bid_price, quote.ask_price);
624        Ok(())
625    }
626
627    fn on_trade(&mut self, trade: &TradeTick) -> anyhow::Result<()> {
628        if self.config.log_data {
629            log_info!("{trade:?}", color = LogColor::Cyan);
630        }
631        Ok(())
632    }
633
634    fn on_book(&mut self, book: &OrderBook) -> anyhow::Result<()> {
635        if self.config.log_data {
636            let num_levels = self.config.book_levels_to_print;
637            let instrument_id = book.instrument_id;
638            let book_str = book.pprint(num_levels, None);
639            log_info!("\n{instrument_id}\n{book_str}", color = LogColor::Cyan);
640
641            // Log own order book if available
642            if self.is_registered() {
643                let cache = self.cache();
644                if let Some(own_book) = cache.own_order_book(&instrument_id) {
645                    let own_book_str = own_book.pprint(num_levels, None);
646                    log_info!(
647                        "\n{instrument_id} (own)\n{own_book_str}",
648                        color = LogColor::Magenta
649                    );
650                }
651            }
652        }
653
654        let Some(best_bid) = book.best_bid_price() else {
655            return Ok(()); // Wait for market
656        };
657        let Some(best_ask) = book.best_ask_price() else {
658            return Ok(()); // Wait for market
659        };
660
661        self.maintain_orders(best_bid, best_ask);
662        Ok(())
663    }
664
665    fn on_book_deltas(&mut self, deltas: &OrderBookDeltas) -> anyhow::Result<()> {
666        if self.config.log_data {
667            log_info!("{deltas:?}", color = LogColor::Cyan);
668        }
669        Ok(())
670    }
671
672    fn on_bar(&mut self, bar: &Bar) -> anyhow::Result<()> {
673        if self.config.log_data {
674            log_info!("{bar:?}", color = LogColor::Cyan);
675        }
676        Ok(())
677    }
678
679    fn on_mark_price(&mut self, mark_price: &MarkPriceUpdate) -> anyhow::Result<()> {
680        if self.config.log_data {
681            log_info!("{mark_price:?}", color = LogColor::Cyan);
682        }
683        Ok(())
684    }
685
686    fn on_index_price(&mut self, index_price: &IndexPriceUpdate) -> anyhow::Result<()> {
687        if self.config.log_data {
688            log_info!("{index_price:?}", color = LogColor::Cyan);
689        }
690        Ok(())
691    }
692
693    fn on_time_event(&mut self, event: &TimeEvent) -> anyhow::Result<()> {
694        Strategy::on_time_event(self, event)
695    }
696}
697
698impl Strategy for ExecTester {
699    fn core(&self) -> &StrategyCore {
700        &self.core
701    }
702
703    fn core_mut(&mut self) -> &mut StrategyCore {
704        &mut self.core
705    }
706
707    fn external_order_claims(&self) -> Option<Vec<InstrumentId>> {
708        self.config.base.external_order_claims.clone()
709    }
710}
711
712impl ExecTester {
713    /// Creates a new [`ExecTester`] instance.
714    #[must_use]
715    pub fn new(config: ExecTesterConfig) -> Self {
716        Self {
717            core: StrategyCore::new(config.base.clone()),
718            config,
719            instrument: None,
720            price_offset: None,
721            preinitialized_market_data: false,
722            buy_order: None,
723            sell_order: None,
724            buy_stop_order: None,
725            sell_stop_order: None,
726        }
727    }
728
729    fn initialize_with_instrument(
730        &mut self,
731        instrument: InstrumentAny,
732        subscribe_market_data: bool,
733    ) -> anyhow::Result<()> {
734        let instrument_id = self.config.instrument_id;
735        let client_id = self.config.client_id;
736
737        self.price_offset = Some(self.get_price_offset(&instrument));
738        self.instrument = Some(instrument);
739
740        if subscribe_market_data && self.config.subscribe_quotes {
741            self.subscribe_quotes(instrument_id, client_id, None);
742        }
743
744        if subscribe_market_data && self.config.subscribe_trades {
745            self.subscribe_trades(instrument_id, client_id, None);
746        }
747
748        if self.config.subscribe_book {
749            self.subscribe_book_at_interval(
750                instrument_id,
751                self.config.book_type,
752                self.config.book_depth,
753                self.config.book_interval_ms,
754                client_id,
755                None,
756            );
757        }
758
759        if let Some(qty) = self.config.open_position_on_start_qty {
760            self.open_position(qty)?;
761        }
762
763        Ok(())
764    }
765
766    fn get_price_offset(&self, instrument: &InstrumentAny) -> f64 {
767        instrument.price_increment().as_f64() * self.config.tob_offset_ticks as f64
768    }
769
770    fn expire_time_from_delta(&self, mins: u64) -> UnixNanos {
771        let current_ns = self.timestamp_ns();
772        let delta_ns = secs_to_nanos_unchecked((mins * 60) as f64);
773        UnixNanos::from(current_ns.as_u64() + delta_ns)
774    }
775
776    fn resolve_time_in_force(
777        &self,
778        tif_override: Option<TimeInForce>,
779    ) -> (TimeInForce, Option<UnixNanos>) {
780        match (tif_override, self.config.order_expire_time_delta_mins) {
781            (Some(TimeInForce::Gtd), Some(mins)) => {
782                (TimeInForce::Gtd, Some(self.expire_time_from_delta(mins)))
783            }
784            (Some(TimeInForce::Gtd), None) => {
785                log_warn!(
786                    "GTD time in force requires order_expire_time_delta_mins, falling back to GTC"
787                );
788                (TimeInForce::Gtc, None)
789            }
790            (Some(tif), _) => (tif, None),
791            (None, Some(mins)) => (TimeInForce::Gtd, Some(self.expire_time_from_delta(mins))),
792            (None, None) => (TimeInForce::Gtc, None),
793        }
794    }
795
796    fn is_order_active(&self, order: &OrderAny) -> bool {
797        order.is_active_local() || order.is_inflight() || order.is_open()
798    }
799
800    fn get_order_trigger_price(&self, order: &OrderAny) -> Option<Price> {
801        order.trigger_price()
802    }
803
804    fn modify_stop_order(
805        &mut self,
806        order: OrderAny,
807        trigger_price: Price,
808        limit_price: Option<Price>,
809    ) -> anyhow::Result<()> {
810        let client_id = self.config.client_id;
811
812        match &order {
813            OrderAny::StopMarket(_) | OrderAny::MarketIfTouched(_) => {
814                self.modify_order(order, None, None, Some(trigger_price), client_id)
815            }
816            OrderAny::StopLimit(_) | OrderAny::LimitIfTouched(_) => {
817                self.modify_order(order, None, limit_price, Some(trigger_price), client_id)
818            }
819            _ => {
820                log_warn!("Cannot modify order of type {:?}", order.order_type());
821                Ok(())
822            }
823        }
824    }
825
826    /// Submit an order, applying order_params if configured.
827    fn submit_order_apply_params(&mut self, order: OrderAny) -> anyhow::Result<()> {
828        let client_id = self.config.client_id;
829        if let Some(params) = &self.config.order_params {
830            self.submit_order_with_params(order, None, client_id, params.clone())
831        } else {
832            self.submit_order(order, None, client_id)
833        }
834    }
835
836    /// Maintain orders based on current market prices.
837    fn maintain_orders(&mut self, best_bid: Price, best_ask: Price) {
838        if self.instrument.is_none() || self.config.dry_run {
839            return;
840        }
841
842        if self.config.enable_limit_buys {
843            self.maintain_buy_orders(best_bid, best_ask);
844        }
845
846        if self.config.enable_limit_sells {
847            self.maintain_sell_orders(best_bid, best_ask);
848        }
849
850        if self.config.enable_stop_buys {
851            self.maintain_stop_buy_orders(best_bid, best_ask);
852        }
853
854        if self.config.enable_stop_sells {
855            self.maintain_stop_sell_orders(best_bid, best_ask);
856        }
857    }
858
859    /// Maintain buy limit orders.
860    fn maintain_buy_orders(&mut self, best_bid: Price, best_ask: Price) {
861        let Some(instrument) = &self.instrument else {
862            return;
863        };
864        let Some(price_offset) = self.price_offset else {
865            return;
866        };
867
868        // test_reject_post_only places order on wrong side of spread to trigger rejection
869        let price = if self.config.use_post_only && self.config.test_reject_post_only {
870            instrument.make_price(best_ask.as_f64() + price_offset)
871        } else {
872            instrument.make_price(best_bid.as_f64() - price_offset)
873        };
874
875        let needs_new_order = match &self.buy_order {
876            None => true,
877            Some(order) => !self.is_order_active(order),
878        };
879
880        if needs_new_order {
881            let result = if self.config.enable_brackets {
882                self.submit_bracket_order(OrderSide::Buy, price)
883            } else {
884                self.submit_limit_order(OrderSide::Buy, price)
885            };
886            if let Err(e) = result {
887                log::error!("Failed to submit buy order: {e}");
888            }
889        } else if let Some(order) = &self.buy_order
890            && order.venue_order_id().is_some()
891            && !order.is_pending_update()
892            && !order.is_pending_cancel()
893            && let Some(order_price) = order.price()
894            && order_price < price
895        {
896            let client_id = self.config.client_id;
897            if self.config.modify_orders_to_maintain_tob_offset {
898                let order_clone = order.clone();
899                if let Err(e) = self.modify_order(order_clone, None, Some(price), None, client_id) {
900                    log::error!("Failed to modify buy order: {e}");
901                }
902            } else if self.config.cancel_replace_orders_to_maintain_tob_offset {
903                let order_clone = order.clone();
904                let _ = self.cancel_order(order_clone, client_id);
905                if let Err(e) = self.submit_limit_order(OrderSide::Buy, price) {
906                    log::error!("Failed to submit replacement buy order: {e}");
907                }
908            }
909        }
910    }
911
912    /// Maintain sell limit orders.
913    fn maintain_sell_orders(&mut self, best_bid: Price, best_ask: Price) {
914        let Some(instrument) = &self.instrument else {
915            return;
916        };
917        let Some(price_offset) = self.price_offset else {
918            return;
919        };
920
921        // test_reject_post_only places order on wrong side of spread to trigger rejection
922        let price = if self.config.use_post_only && self.config.test_reject_post_only {
923            instrument.make_price(best_bid.as_f64() - price_offset)
924        } else {
925            instrument.make_price(best_ask.as_f64() + price_offset)
926        };
927
928        let needs_new_order = match &self.sell_order {
929            None => true,
930            Some(order) => !self.is_order_active(order),
931        };
932
933        if needs_new_order {
934            let result = if self.config.enable_brackets {
935                self.submit_bracket_order(OrderSide::Sell, price)
936            } else {
937                self.submit_limit_order(OrderSide::Sell, price)
938            };
939            if let Err(e) = result {
940                log::error!("Failed to submit sell order: {e}");
941            }
942        } else if let Some(order) = &self.sell_order
943            && order.venue_order_id().is_some()
944            && !order.is_pending_update()
945            && !order.is_pending_cancel()
946            && let Some(order_price) = order.price()
947            && order_price > price
948        {
949            let client_id = self.config.client_id;
950            if self.config.modify_orders_to_maintain_tob_offset {
951                let order_clone = order.clone();
952                if let Err(e) = self.modify_order(order_clone, None, Some(price), None, client_id) {
953                    log::error!("Failed to modify sell order: {e}");
954                }
955            } else if self.config.cancel_replace_orders_to_maintain_tob_offset {
956                let order_clone = order.clone();
957                let _ = self.cancel_order(order_clone, client_id);
958                if let Err(e) = self.submit_limit_order(OrderSide::Sell, price) {
959                    log::error!("Failed to submit replacement sell order: {e}");
960                }
961            }
962        }
963    }
964
965    /// Maintain stop buy orders.
966    fn maintain_stop_buy_orders(&mut self, best_bid: Price, best_ask: Price) {
967        let Some(instrument) = &self.instrument else {
968            return;
969        };
970
971        let price_increment = instrument.price_increment().as_f64();
972        let stop_offset = price_increment * self.config.stop_offset_ticks as f64;
973
974        // Determine trigger price based on order type
975        let trigger_price = if matches!(
976            self.config.stop_order_type,
977            OrderType::LimitIfTouched | OrderType::MarketIfTouched
978        ) {
979            // IF_TOUCHED buy: place BELOW market (buy on dip)
980            instrument.make_price(best_bid.as_f64() - stop_offset)
981        } else {
982            // STOP buy orders are placed ABOVE the market (stop loss on short)
983            instrument.make_price(best_ask.as_f64() + stop_offset)
984        };
985
986        // Calculate limit price if needed
987        let limit_price = if matches!(
988            self.config.stop_order_type,
989            OrderType::StopLimit | OrderType::LimitIfTouched
990        ) {
991            if let Some(limit_offset_ticks) = self.config.stop_limit_offset_ticks {
992                let limit_offset = price_increment * limit_offset_ticks as f64;
993                if self.config.stop_order_type == OrderType::LimitIfTouched {
994                    Some(instrument.make_price(trigger_price.as_f64() - limit_offset))
995                } else {
996                    Some(instrument.make_price(trigger_price.as_f64() + limit_offset))
997                }
998            } else {
999                Some(trigger_price)
1000            }
1001        } else {
1002            None
1003        };
1004
1005        let needs_new_order = match &self.buy_stop_order {
1006            None => true,
1007            Some(order) => !self.is_order_active(order),
1008        };
1009
1010        if needs_new_order {
1011            if let Err(e) = self.submit_stop_order(OrderSide::Buy, trigger_price, limit_price) {
1012                log::error!("Failed to submit buy stop order: {e}");
1013            }
1014        } else if let Some(order) = &self.buy_stop_order
1015            && order.venue_order_id().is_some()
1016            && !order.is_pending_update()
1017            && !order.is_pending_cancel()
1018        {
1019            let current_trigger = self.get_order_trigger_price(order);
1020            if current_trigger.is_some() && current_trigger != Some(trigger_price) {
1021                if self.config.modify_stop_orders_to_maintain_offset {
1022                    let order_clone = order.clone();
1023                    if let Err(e) = self.modify_stop_order(order_clone, trigger_price, limit_price)
1024                    {
1025                        log::error!("Failed to modify buy stop order: {e}");
1026                    }
1027                } else if self.config.cancel_replace_stop_orders_to_maintain_offset {
1028                    let order_clone = order.clone();
1029                    let _ = self.cancel_order(order_clone, self.config.client_id);
1030                    if let Err(e) =
1031                        self.submit_stop_order(OrderSide::Buy, trigger_price, limit_price)
1032                    {
1033                        log::error!("Failed to submit replacement buy stop order: {e}");
1034                    }
1035                }
1036            }
1037        }
1038    }
1039
1040    /// Maintain stop sell orders.
1041    fn maintain_stop_sell_orders(&mut self, best_bid: Price, best_ask: Price) {
1042        let Some(instrument) = &self.instrument else {
1043            return;
1044        };
1045
1046        let price_increment = instrument.price_increment().as_f64();
1047        let stop_offset = price_increment * self.config.stop_offset_ticks as f64;
1048
1049        // Determine trigger price based on order type
1050        let trigger_price = if matches!(
1051            self.config.stop_order_type,
1052            OrderType::LimitIfTouched | OrderType::MarketIfTouched
1053        ) {
1054            // IF_TOUCHED sell: place ABOVE market (sell on rally)
1055            instrument.make_price(best_ask.as_f64() + stop_offset)
1056        } else {
1057            // STOP sell orders are placed BELOW the market (stop loss on long)
1058            instrument.make_price(best_bid.as_f64() - stop_offset)
1059        };
1060
1061        // Calculate limit price if needed
1062        let limit_price = if matches!(
1063            self.config.stop_order_type,
1064            OrderType::StopLimit | OrderType::LimitIfTouched
1065        ) {
1066            if let Some(limit_offset_ticks) = self.config.stop_limit_offset_ticks {
1067                let limit_offset = price_increment * limit_offset_ticks as f64;
1068                if self.config.stop_order_type == OrderType::LimitIfTouched {
1069                    Some(instrument.make_price(trigger_price.as_f64() + limit_offset))
1070                } else {
1071                    Some(instrument.make_price(trigger_price.as_f64() - limit_offset))
1072                }
1073            } else {
1074                Some(trigger_price)
1075            }
1076        } else {
1077            None
1078        };
1079
1080        let needs_new_order = match &self.sell_stop_order {
1081            None => true,
1082            Some(order) => !self.is_order_active(order),
1083        };
1084
1085        if needs_new_order {
1086            if let Err(e) = self.submit_stop_order(OrderSide::Sell, trigger_price, limit_price) {
1087                log::error!("Failed to submit sell stop order: {e}");
1088            }
1089        } else if let Some(order) = &self.sell_stop_order
1090            && order.venue_order_id().is_some()
1091            && !order.is_pending_update()
1092            && !order.is_pending_cancel()
1093        {
1094            let current_trigger = self.get_order_trigger_price(order);
1095            if current_trigger.is_some() && current_trigger != Some(trigger_price) {
1096                if self.config.modify_stop_orders_to_maintain_offset {
1097                    let order_clone = order.clone();
1098                    if let Err(e) = self.modify_stop_order(order_clone, trigger_price, limit_price)
1099                    {
1100                        log::error!("Failed to modify sell stop order: {e}");
1101                    }
1102                } else if self.config.cancel_replace_stop_orders_to_maintain_offset {
1103                    let order_clone = order.clone();
1104                    let _ = self.cancel_order(order_clone, self.config.client_id);
1105                    if let Err(e) =
1106                        self.submit_stop_order(OrderSide::Sell, trigger_price, limit_price)
1107                    {
1108                        log::error!("Failed to submit replacement sell stop order: {e}");
1109                    }
1110                }
1111            }
1112        }
1113    }
1114
1115    /// Submit a limit order.
1116    ///
1117    /// # Errors
1118    ///
1119    /// Returns an error if order creation or submission fails.
1120    fn submit_limit_order(&mut self, order_side: OrderSide, price: Price) -> anyhow::Result<()> {
1121        let Some(instrument) = &self.instrument else {
1122            anyhow::bail!("No instrument loaded");
1123        };
1124
1125        if self.config.dry_run {
1126            log_warn!("Dry run, skipping create {order_side:?} order");
1127            return Ok(());
1128        }
1129
1130        if order_side == OrderSide::Buy && !self.config.enable_limit_buys {
1131            log_warn!("BUY orders not enabled, skipping");
1132            return Ok(());
1133        } else if order_side == OrderSide::Sell && !self.config.enable_limit_sells {
1134            log_warn!("SELL orders not enabled, skipping");
1135            return Ok(());
1136        }
1137
1138        let (time_in_force, expire_time) =
1139            self.resolve_time_in_force(self.config.limit_time_in_force);
1140
1141        let quantity = instrument.make_qty(self.config.order_qty.as_f64(), None);
1142
1143        let order = self.core.order_factory().limit(
1144            self.config.instrument_id,
1145            order_side,
1146            quantity,
1147            price,
1148            Some(time_in_force),
1149            expire_time,
1150            Some(self.config.use_post_only),
1151            None, // reduce_only
1152            Some(self.config.use_quote_quantity),
1153            self.config.order_display_qty,
1154            self.config.emulation_trigger,
1155            None, // trigger_instrument_id
1156            None, // exec_algorithm_id
1157            None, // exec_algorithm_params
1158            None, // tags
1159            None, // client_order_id
1160        );
1161
1162        if order_side == OrderSide::Buy {
1163            self.buy_order = Some(order.clone());
1164        } else {
1165            self.sell_order = Some(order.clone());
1166        }
1167
1168        self.submit_order_apply_params(order)
1169    }
1170
1171    /// Submit a stop order.
1172    ///
1173    /// # Errors
1174    ///
1175    /// Returns an error if order creation or submission fails.
1176    fn submit_stop_order(
1177        &mut self,
1178        order_side: OrderSide,
1179        trigger_price: Price,
1180        limit_price: Option<Price>,
1181    ) -> anyhow::Result<()> {
1182        let Some(instrument) = &self.instrument else {
1183            anyhow::bail!("No instrument loaded");
1184        };
1185
1186        if self.config.dry_run {
1187            log_warn!("Dry run, skipping create {order_side:?} stop order");
1188            return Ok(());
1189        }
1190
1191        if order_side == OrderSide::Buy && !self.config.enable_stop_buys {
1192            log_warn!("BUY stop orders not enabled, skipping");
1193            return Ok(());
1194        } else if order_side == OrderSide::Sell && !self.config.enable_stop_sells {
1195            log_warn!("SELL stop orders not enabled, skipping");
1196            return Ok(());
1197        }
1198
1199        let (time_in_force, expire_time) =
1200            self.resolve_time_in_force(self.config.stop_time_in_force);
1201
1202        // Use instrument's make_qty to ensure correct precision
1203        let quantity = instrument.make_qty(self.config.order_qty.as_f64(), None);
1204
1205        let factory = self.core.order_factory();
1206
1207        let order: OrderAny = match self.config.stop_order_type {
1208            OrderType::StopMarket => factory.stop_market(
1209                self.config.instrument_id,
1210                order_side,
1211                quantity,
1212                trigger_price,
1213                Some(self.config.stop_trigger_type),
1214                Some(time_in_force),
1215                expire_time,
1216                None, // reduce_only
1217                Some(self.config.use_quote_quantity),
1218                None, // display_qty
1219                self.config.emulation_trigger,
1220                None, // trigger_instrument_id
1221                None, // exec_algorithm_id
1222                None, // exec_algorithm_params
1223                None, // tags
1224                None, // client_order_id
1225            ),
1226            OrderType::StopLimit => {
1227                let Some(limit_price) = limit_price else {
1228                    anyhow::bail!("STOP_LIMIT order requires limit_price");
1229                };
1230                factory.stop_limit(
1231                    self.config.instrument_id,
1232                    order_side,
1233                    quantity,
1234                    limit_price,
1235                    trigger_price,
1236                    Some(self.config.stop_trigger_type),
1237                    Some(time_in_force),
1238                    expire_time,
1239                    None, // post_only
1240                    None, // reduce_only
1241                    Some(self.config.use_quote_quantity),
1242                    self.config.order_display_qty,
1243                    self.config.emulation_trigger,
1244                    None, // trigger_instrument_id
1245                    None, // exec_algorithm_id
1246                    None, // exec_algorithm_params
1247                    None, // tags
1248                    None, // client_order_id
1249                )
1250            }
1251            OrderType::MarketIfTouched => factory.market_if_touched(
1252                self.config.instrument_id,
1253                order_side,
1254                quantity,
1255                trigger_price,
1256                Some(self.config.stop_trigger_type),
1257                Some(time_in_force),
1258                expire_time,
1259                None, // reduce_only
1260                Some(self.config.use_quote_quantity),
1261                self.config.emulation_trigger,
1262                None, // trigger_instrument_id
1263                None, // exec_algorithm_id
1264                None, // exec_algorithm_params
1265                None, // tags
1266                None, // client_order_id
1267            ),
1268            OrderType::LimitIfTouched => {
1269                let Some(limit_price) = limit_price else {
1270                    anyhow::bail!("LIMIT_IF_TOUCHED order requires limit_price");
1271                };
1272                factory.limit_if_touched(
1273                    self.config.instrument_id,
1274                    order_side,
1275                    quantity,
1276                    limit_price,
1277                    trigger_price,
1278                    Some(self.config.stop_trigger_type),
1279                    Some(time_in_force),
1280                    expire_time,
1281                    None, // post_only
1282                    None, // reduce_only
1283                    Some(self.config.use_quote_quantity),
1284                    self.config.order_display_qty,
1285                    self.config.emulation_trigger,
1286                    None, // trigger_instrument_id
1287                    None, // exec_algorithm_id
1288                    None, // exec_algorithm_params
1289                    None, // tags
1290                    None, // client_order_id
1291                )
1292            }
1293            _ => {
1294                anyhow::bail!("Unknown stop order type: {:?}", self.config.stop_order_type);
1295            }
1296        };
1297
1298        if order_side == OrderSide::Buy {
1299            self.buy_stop_order = Some(order.clone());
1300        } else {
1301            self.sell_stop_order = Some(order.clone());
1302        }
1303
1304        self.submit_order_apply_params(order)
1305    }
1306
1307    /// Submit a bracket order (entry with stop-loss and take-profit).
1308    ///
1309    /// # Errors
1310    ///
1311    /// Returns an error if order creation or submission fails.
1312    fn submit_bracket_order(
1313        &mut self,
1314        order_side: OrderSide,
1315        entry_price: Price,
1316    ) -> anyhow::Result<()> {
1317        let Some(instrument) = &self.instrument else {
1318            anyhow::bail!("No instrument loaded");
1319        };
1320
1321        if self.config.dry_run {
1322            log_warn!("Dry run, skipping create {order_side:?} bracket order");
1323            return Ok(());
1324        }
1325
1326        if self.config.bracket_entry_order_type != OrderType::Limit {
1327            anyhow::bail!(
1328                "Only Limit entry orders are supported for brackets, was {:?}",
1329                self.config.bracket_entry_order_type
1330            );
1331        }
1332
1333        if order_side == OrderSide::Buy && !self.config.enable_limit_buys {
1334            log_warn!("BUY orders not enabled, skipping bracket");
1335            return Ok(());
1336        } else if order_side == OrderSide::Sell && !self.config.enable_limit_sells {
1337            log_warn!("SELL orders not enabled, skipping bracket");
1338            return Ok(());
1339        }
1340
1341        let (time_in_force, expire_time) =
1342            self.resolve_time_in_force(self.config.limit_time_in_force);
1343        let sl_time_in_force = self.config.stop_time_in_force.unwrap_or(TimeInForce::Gtc);
1344        if sl_time_in_force == TimeInForce::Gtd {
1345            anyhow::bail!("GTD time in force not supported for bracket stop-loss legs");
1346        }
1347
1348        let quantity = instrument.make_qty(self.config.order_qty.as_f64(), None);
1349        let price_increment = instrument.price_increment().as_f64();
1350        let bracket_offset = price_increment * self.config.bracket_offset_ticks as f64;
1351
1352        let (tp_price, sl_trigger_price) = match order_side {
1353            OrderSide::Buy => {
1354                let tp = instrument.make_price(entry_price.as_f64() + bracket_offset);
1355                let sl = instrument.make_price(entry_price.as_f64() - bracket_offset);
1356                (tp, sl)
1357            }
1358            OrderSide::Sell => {
1359                let tp = instrument.make_price(entry_price.as_f64() - bracket_offset);
1360                let sl = instrument.make_price(entry_price.as_f64() + bracket_offset);
1361                (tp, sl)
1362            }
1363            _ => anyhow::bail!("Invalid order side for bracket: {order_side:?}"),
1364        };
1365
1366        let orders = self.core.order_factory().bracket(
1367            self.config.instrument_id,
1368            order_side,
1369            quantity,
1370            Some(entry_price),                   // entry_price
1371            sl_trigger_price,                    // sl_trigger_price
1372            Some(self.config.stop_trigger_type), // sl_trigger_type
1373            tp_price,                            // tp_price
1374            None,                                // entry_trigger_price (limit entry, no trigger)
1375            Some(time_in_force),
1376            expire_time,
1377            Some(sl_time_in_force),
1378            Some(self.config.use_post_only),
1379            None, // reduce_only
1380            Some(self.config.use_quote_quantity),
1381            self.config.emulation_trigger,
1382            None, // trigger_instrument_id
1383            None, // exec_algorithm_id
1384            None, // exec_algorithm_params
1385            None, // tags
1386        );
1387
1388        if let Some(entry_order) = orders.first() {
1389            if order_side == OrderSide::Buy {
1390                self.buy_order = Some(entry_order.clone());
1391            } else {
1392                self.sell_order = Some(entry_order.clone());
1393            }
1394        }
1395
1396        let client_id = self.config.client_id;
1397        if let Some(params) = &self.config.order_params {
1398            self.submit_order_list_with_params(orders, None, client_id, params.clone())
1399        } else {
1400            self.submit_order_list(orders, None, client_id)
1401        }
1402    }
1403
1404    /// Open a position with a market order.
1405    ///
1406    /// # Errors
1407    ///
1408    /// Returns an error if order creation or submission fails.
1409    fn open_position(&mut self, net_qty: Decimal) -> anyhow::Result<()> {
1410        let Some(instrument) = &self.instrument else {
1411            anyhow::bail!("No instrument loaded");
1412        };
1413
1414        if net_qty == Decimal::ZERO {
1415            log_warn!("Open position with zero quantity, skipping");
1416            return Ok(());
1417        }
1418
1419        let order_side = if net_qty > Decimal::ZERO {
1420            OrderSide::Buy
1421        } else {
1422            OrderSide::Sell
1423        };
1424
1425        let quantity = instrument.make_qty(net_qty.abs().to_f64().unwrap_or(0.0), None);
1426
1427        // Test reduce_only rejection by setting reduce_only on open position order
1428        let reduce_only = if self.config.test_reject_reduce_only {
1429            Some(true)
1430        } else {
1431            None
1432        };
1433
1434        let order = self.core.order_factory().market(
1435            self.config.instrument_id,
1436            order_side,
1437            quantity,
1438            Some(self.config.open_position_time_in_force),
1439            reduce_only,
1440            Some(self.config.use_quote_quantity),
1441            None, // exec_algorithm_id
1442            None, // exec_algorithm_params
1443            None, // tags
1444            None, // client_order_id
1445        );
1446
1447        self.submit_order_apply_params(order)
1448    }
1449}
1450
1451#[cfg(test)]
1452mod tests {
1453    use std::{cell::RefCell, rc::Rc};
1454
1455    use nautilus_common::{
1456        cache::Cache,
1457        clock::{Clock, TestClock},
1458    };
1459    use nautilus_core::UnixNanos;
1460    use nautilus_model::{
1461        data::stubs::{OrderBookDeltaTestBuilder, stub_bar},
1462        enums::{AggressorSide, ContingencyType, OrderStatus},
1463        identifiers::{StrategyId, TradeId, TraderId},
1464        instruments::stubs::crypto_perpetual_ethusdt,
1465        orders::LimitOrder,
1466        stubs::TestDefault,
1467    };
1468    use nautilus_portfolio::portfolio::Portfolio;
1469    use rstest::*;
1470
1471    use super::*;
1472
1473    /// Register an ExecTester with all required components.
1474    /// This gives the tester access to OrderFactory for actual order creation.
1475    fn register_exec_tester(tester: &mut ExecTester, cache: Rc<RefCell<Cache>>) {
1476        let trader_id = TraderId::from("TRADER-001");
1477        let clock: Rc<RefCell<dyn Clock>> = Rc::new(RefCell::new(TestClock::new()));
1478        let portfolio = Rc::new(RefCell::new(Portfolio::new(
1479            cache.clone(),
1480            clock.clone(),
1481            None,
1482        )));
1483
1484        tester
1485            .core
1486            .register(trader_id, clock, cache, portfolio)
1487            .unwrap();
1488    }
1489
1490    /// Create a cache with the test instrument pre-loaded.
1491    fn create_cache_with_instrument(instrument: &InstrumentAny) -> Rc<RefCell<Cache>> {
1492        let cache = Rc::new(RefCell::new(Cache::default()));
1493        let _ = cache.borrow_mut().add_instrument(instrument.clone());
1494        cache
1495    }
1496
1497    #[fixture]
1498    fn config() -> ExecTesterConfig {
1499        ExecTesterConfig::new(
1500            StrategyId::from("EXEC_TESTER-001"),
1501            InstrumentId::from("ETHUSDT-PERP.BINANCE"),
1502            ClientId::new("BINANCE"),
1503            Quantity::from("0.001"),
1504        )
1505    }
1506
1507    #[fixture]
1508    fn instrument() -> InstrumentAny {
1509        InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt())
1510    }
1511
1512    fn create_initialized_limit_order() -> OrderAny {
1513        OrderAny::Limit(LimitOrder::test_default())
1514    }
1515
1516    #[rstest]
1517    fn test_config_creation(config: ExecTesterConfig) {
1518        assert_eq!(
1519            config.base.strategy_id,
1520            Some(StrategyId::from("EXEC_TESTER-001"))
1521        );
1522        assert_eq!(
1523            config.instrument_id,
1524            InstrumentId::from("ETHUSDT-PERP.BINANCE")
1525        );
1526        assert_eq!(config.client_id, Some(ClientId::new("BINANCE")));
1527        assert_eq!(config.order_qty, Quantity::from("0.001"));
1528        assert!(config.subscribe_quotes);
1529        assert!(config.subscribe_trades);
1530        assert!(!config.subscribe_book);
1531        assert!(config.enable_limit_buys);
1532        assert!(config.enable_limit_sells);
1533        assert!(!config.enable_stop_buys);
1534        assert!(!config.enable_stop_sells);
1535        assert_eq!(config.tob_offset_ticks, 500);
1536    }
1537
1538    #[rstest]
1539    fn test_config_default() {
1540        let config = ExecTesterConfig::default();
1541
1542        assert!(config.base.strategy_id.is_none());
1543        assert!(config.subscribe_quotes);
1544        assert!(config.subscribe_trades);
1545        assert!(config.enable_limit_buys);
1546        assert!(config.enable_limit_sells);
1547        assert!(config.cancel_orders_on_stop);
1548        assert!(config.close_positions_on_stop);
1549        assert!(config.close_positions_time_in_force.is_none());
1550        assert!(!config.use_batch_cancel_on_stop);
1551    }
1552
1553    #[rstest]
1554    fn test_config_with_stop_orders(mut config: ExecTesterConfig) {
1555        config.enable_stop_buys = true;
1556        config.enable_stop_sells = true;
1557        config.stop_order_type = OrderType::StopLimit;
1558        config.stop_offset_ticks = 200;
1559        config.stop_limit_offset_ticks = Some(50);
1560
1561        let tester = ExecTester::new(config);
1562
1563        assert!(tester.config.enable_stop_buys);
1564        assert!(tester.config.enable_stop_sells);
1565        assert_eq!(tester.config.stop_order_type, OrderType::StopLimit);
1566        assert_eq!(tester.config.stop_offset_ticks, 200);
1567        assert_eq!(tester.config.stop_limit_offset_ticks, Some(50));
1568    }
1569
1570    #[rstest]
1571    fn test_config_with_batch_cancel() {
1572        let config = ExecTesterConfig::default().with_use_batch_cancel_on_stop(true);
1573        assert!(config.use_batch_cancel_on_stop);
1574    }
1575
1576    #[rstest]
1577    fn test_config_with_order_maintenance(mut config: ExecTesterConfig) {
1578        config.modify_orders_to_maintain_tob_offset = true;
1579        config.cancel_replace_orders_to_maintain_tob_offset = false;
1580
1581        let tester = ExecTester::new(config);
1582
1583        assert!(tester.config.modify_orders_to_maintain_tob_offset);
1584        assert!(!tester.config.cancel_replace_orders_to_maintain_tob_offset);
1585    }
1586
1587    #[rstest]
1588    fn test_config_with_dry_run(mut config: ExecTesterConfig) {
1589        config.dry_run = true;
1590
1591        let tester = ExecTester::new(config);
1592
1593        assert!(tester.config.dry_run);
1594    }
1595
1596    #[rstest]
1597    fn test_config_with_position_opening(mut config: ExecTesterConfig) {
1598        config.open_position_on_start_qty = Some(Decimal::from(1));
1599        config.open_position_time_in_force = TimeInForce::Ioc;
1600
1601        let tester = ExecTester::new(config);
1602
1603        assert_eq!(
1604            tester.config.open_position_on_start_qty,
1605            Some(Decimal::from(1))
1606        );
1607        assert_eq!(tester.config.open_position_time_in_force, TimeInForce::Ioc);
1608    }
1609
1610    #[rstest]
1611    fn test_config_with_close_positions_time_in_force_builder() {
1612        let config =
1613            ExecTesterConfig::default().with_close_positions_time_in_force(Some(TimeInForce::Ioc));
1614
1615        assert_eq!(config.close_positions_time_in_force, Some(TimeInForce::Ioc));
1616    }
1617
1618    #[rstest]
1619    fn test_config_with_all_stop_order_types(mut config: ExecTesterConfig) {
1620        // Test STOP_MARKET
1621        config.stop_order_type = OrderType::StopMarket;
1622        assert_eq!(config.stop_order_type, OrderType::StopMarket);
1623
1624        // Test STOP_LIMIT
1625        config.stop_order_type = OrderType::StopLimit;
1626        assert_eq!(config.stop_order_type, OrderType::StopLimit);
1627
1628        // Test MARKET_IF_TOUCHED
1629        config.stop_order_type = OrderType::MarketIfTouched;
1630        assert_eq!(config.stop_order_type, OrderType::MarketIfTouched);
1631
1632        // Test LIMIT_IF_TOUCHED
1633        config.stop_order_type = OrderType::LimitIfTouched;
1634        assert_eq!(config.stop_order_type, OrderType::LimitIfTouched);
1635    }
1636
1637    #[rstest]
1638    fn test_exec_tester_creation(config: ExecTesterConfig) {
1639        let tester = ExecTester::new(config);
1640
1641        assert!(tester.instrument.is_none());
1642        assert!(tester.price_offset.is_none());
1643        assert!(tester.buy_order.is_none());
1644        assert!(tester.sell_order.is_none());
1645        assert!(tester.buy_stop_order.is_none());
1646        assert!(tester.sell_stop_order.is_none());
1647    }
1648
1649    #[rstest]
1650    fn test_get_price_offset(config: ExecTesterConfig, instrument: InstrumentAny) {
1651        let tester = ExecTester::new(config);
1652
1653        // price_increment = 0.01, tob_offset_ticks = 500
1654        // Expected: 0.01 * 500 = 5.0
1655        let offset = tester.get_price_offset(&instrument);
1656
1657        assert!((offset - 5.0).abs() < 1e-10);
1658    }
1659
1660    #[rstest]
1661    fn test_get_price_offset_different_ticks(instrument: InstrumentAny) {
1662        let config = ExecTesterConfig {
1663            tob_offset_ticks: 100,
1664            ..Default::default()
1665        };
1666
1667        let tester = ExecTester::new(config);
1668
1669        // price_increment = 0.01, tob_offset_ticks = 100
1670        let offset = tester.get_price_offset(&instrument);
1671
1672        assert!((offset - 1.0).abs() < 1e-10);
1673    }
1674
1675    #[rstest]
1676    fn test_get_price_offset_single_tick(instrument: InstrumentAny) {
1677        let config = ExecTesterConfig {
1678            tob_offset_ticks: 1,
1679            ..Default::default()
1680        };
1681
1682        let tester = ExecTester::new(config);
1683
1684        // price_increment = 0.01, tob_offset_ticks = 1
1685        let offset = tester.get_price_offset(&instrument);
1686
1687        assert!((offset - 0.01).abs() < 1e-10);
1688    }
1689
1690    #[rstest]
1691    fn test_is_order_active_initialized(config: ExecTesterConfig) {
1692        let tester = ExecTester::new(config);
1693        let order = create_initialized_limit_order();
1694
1695        assert!(tester.is_order_active(&order));
1696        assert_eq!(order.status(), OrderStatus::Initialized);
1697    }
1698
1699    #[rstest]
1700    fn test_get_order_trigger_price_limit_order_returns_none(config: ExecTesterConfig) {
1701        let tester = ExecTester::new(config);
1702        let order = create_initialized_limit_order();
1703
1704        assert!(tester.get_order_trigger_price(&order).is_none());
1705    }
1706
1707    #[rstest]
1708    fn test_on_quote_with_logging(config: ExecTesterConfig) {
1709        let mut tester = ExecTester::new(config);
1710
1711        let quote = QuoteTick::new(
1712            InstrumentId::from("BTCUSDT-PERP.BINANCE"),
1713            Price::from("50000.0"),
1714            Price::from("50001.0"),
1715            Quantity::from("1.0"),
1716            Quantity::from("1.0"),
1717            UnixNanos::default(),
1718            UnixNanos::default(),
1719        );
1720
1721        let result = tester.on_quote(&quote);
1722        assert!(result.is_ok());
1723    }
1724
1725    #[rstest]
1726    fn test_on_quote_without_logging(mut config: ExecTesterConfig) {
1727        config.log_data = false;
1728        let mut tester = ExecTester::new(config);
1729
1730        let quote = QuoteTick::new(
1731            InstrumentId::from("BTCUSDT-PERP.BINANCE"),
1732            Price::from("50000.0"),
1733            Price::from("50001.0"),
1734            Quantity::from("1.0"),
1735            Quantity::from("1.0"),
1736            UnixNanos::default(),
1737            UnixNanos::default(),
1738        );
1739
1740        let result = tester.on_quote(&quote);
1741        assert!(result.is_ok());
1742    }
1743
1744    #[rstest]
1745    fn test_on_trade_with_logging(config: ExecTesterConfig) {
1746        let mut tester = ExecTester::new(config);
1747
1748        let trade = TradeTick::new(
1749            InstrumentId::from("BTCUSDT-PERP.BINANCE"),
1750            Price::from("50000.0"),
1751            Quantity::from("0.1"),
1752            AggressorSide::Buyer,
1753            TradeId::new("12345"),
1754            UnixNanos::default(),
1755            UnixNanos::default(),
1756        );
1757
1758        let result = tester.on_trade(&trade);
1759        assert!(result.is_ok());
1760    }
1761
1762    #[rstest]
1763    fn test_on_trade_without_logging(mut config: ExecTesterConfig) {
1764        config.log_data = false;
1765        let mut tester = ExecTester::new(config);
1766
1767        let trade = TradeTick::new(
1768            InstrumentId::from("BTCUSDT-PERP.BINANCE"),
1769            Price::from("50000.0"),
1770            Quantity::from("0.1"),
1771            AggressorSide::Buyer,
1772            TradeId::new("12345"),
1773            UnixNanos::default(),
1774            UnixNanos::default(),
1775        );
1776
1777        let result = tester.on_trade(&trade);
1778        assert!(result.is_ok());
1779    }
1780
1781    #[rstest]
1782    fn test_on_book_without_bids_or_asks(config: ExecTesterConfig) {
1783        let mut tester = ExecTester::new(config);
1784
1785        let book = OrderBook::new(InstrumentId::from("BTCUSDT-PERP.BINANCE"), BookType::L2_MBP);
1786
1787        let result = tester.on_book(&book);
1788        assert!(result.is_ok());
1789    }
1790
1791    #[rstest]
1792    fn test_on_book_deltas_with_logging(config: ExecTesterConfig) {
1793        let mut tester = ExecTester::new(config);
1794        let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
1795        let delta = OrderBookDeltaTestBuilder::new(instrument_id).build();
1796        let deltas = OrderBookDeltas::new(instrument_id, vec![delta]);
1797
1798        let result = tester.on_book_deltas(&deltas);
1799
1800        assert!(result.is_ok());
1801    }
1802
1803    #[rstest]
1804    fn test_on_book_deltas_without_logging(mut config: ExecTesterConfig) {
1805        config.log_data = false;
1806        let mut tester = ExecTester::new(config);
1807        let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
1808        let delta = OrderBookDeltaTestBuilder::new(instrument_id).build();
1809        let deltas = OrderBookDeltas::new(instrument_id, vec![delta]);
1810
1811        let result = tester.on_book_deltas(&deltas);
1812
1813        assert!(result.is_ok());
1814    }
1815
1816    #[rstest]
1817    fn test_on_bar_with_logging(config: ExecTesterConfig) {
1818        let mut tester = ExecTester::new(config);
1819        let bar = stub_bar();
1820
1821        let result = tester.on_bar(&bar);
1822
1823        assert!(result.is_ok());
1824    }
1825
1826    #[rstest]
1827    fn test_on_bar_without_logging(mut config: ExecTesterConfig) {
1828        config.log_data = false;
1829        let mut tester = ExecTester::new(config);
1830        let bar = stub_bar();
1831
1832        let result = tester.on_bar(&bar);
1833
1834        assert!(result.is_ok());
1835    }
1836
1837    #[rstest]
1838    fn test_on_mark_price_with_logging(config: ExecTesterConfig) {
1839        let mut tester = ExecTester::new(config);
1840        let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
1841        let mark_price = MarkPriceUpdate::new(
1842            instrument_id,
1843            Price::from("50000.0"),
1844            UnixNanos::default(),
1845            UnixNanos::default(),
1846        );
1847
1848        let result = tester.on_mark_price(&mark_price);
1849
1850        assert!(result.is_ok());
1851    }
1852
1853    #[rstest]
1854    fn test_on_mark_price_without_logging(mut config: ExecTesterConfig) {
1855        config.log_data = false;
1856        let mut tester = ExecTester::new(config);
1857        let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
1858        let mark_price = MarkPriceUpdate::new(
1859            instrument_id,
1860            Price::from("50000.0"),
1861            UnixNanos::default(),
1862            UnixNanos::default(),
1863        );
1864
1865        let result = tester.on_mark_price(&mark_price);
1866
1867        assert!(result.is_ok());
1868    }
1869
1870    #[rstest]
1871    fn test_on_index_price_with_logging(config: ExecTesterConfig) {
1872        let mut tester = ExecTester::new(config);
1873        let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
1874        let index_price = IndexPriceUpdate::new(
1875            instrument_id,
1876            Price::from("49999.0"),
1877            UnixNanos::default(),
1878            UnixNanos::default(),
1879        );
1880
1881        let result = tester.on_index_price(&index_price);
1882
1883        assert!(result.is_ok());
1884    }
1885
1886    #[rstest]
1887    fn test_on_index_price_without_logging(mut config: ExecTesterConfig) {
1888        config.log_data = false;
1889        let mut tester = ExecTester::new(config);
1890        let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
1891        let index_price = IndexPriceUpdate::new(
1892            instrument_id,
1893            Price::from("49999.0"),
1894            UnixNanos::default(),
1895            UnixNanos::default(),
1896        );
1897
1898        let result = tester.on_index_price(&index_price);
1899
1900        assert!(result.is_ok());
1901    }
1902
1903    #[rstest]
1904    fn test_on_stop_dry_run(mut config: ExecTesterConfig) {
1905        config.dry_run = true;
1906        let mut tester = ExecTester::new(config);
1907
1908        let result = tester.on_stop();
1909
1910        assert!(result.is_ok());
1911    }
1912
1913    #[rstest]
1914    fn test_maintain_orders_dry_run_does_nothing(mut config: ExecTesterConfig) {
1915        config.dry_run = true;
1916        config.enable_limit_buys = true;
1917        config.enable_limit_sells = true;
1918        let mut tester = ExecTester::new(config);
1919
1920        let best_bid = Price::from("50000.0");
1921        let best_ask = Price::from("50001.0");
1922
1923        tester.maintain_orders(best_bid, best_ask);
1924
1925        assert!(tester.buy_order.is_none());
1926        assert!(tester.sell_order.is_none());
1927    }
1928
1929    #[rstest]
1930    fn test_maintain_orders_no_instrument_does_nothing(config: ExecTesterConfig) {
1931        let mut tester = ExecTester::new(config);
1932
1933        let best_bid = Price::from("50000.0");
1934        let best_ask = Price::from("50001.0");
1935
1936        tester.maintain_orders(best_bid, best_ask);
1937
1938        assert!(tester.buy_order.is_none());
1939        assert!(tester.sell_order.is_none());
1940    }
1941
1942    #[rstest]
1943    fn test_submit_limit_order_no_instrument_returns_error(config: ExecTesterConfig) {
1944        let mut tester = ExecTester::new(config);
1945
1946        let result = tester.submit_limit_order(OrderSide::Buy, Price::from("50000.0"));
1947
1948        assert!(result.is_err());
1949        assert!(result.unwrap_err().to_string().contains("No instrument"));
1950    }
1951
1952    #[rstest]
1953    fn test_submit_limit_order_dry_run_returns_ok(
1954        mut config: ExecTesterConfig,
1955        instrument: InstrumentAny,
1956    ) {
1957        config.dry_run = true;
1958        let mut tester = ExecTester::new(config);
1959        tester.instrument = Some(instrument);
1960
1961        let result = tester.submit_limit_order(OrderSide::Buy, Price::from("50000.0"));
1962
1963        assert!(result.is_ok());
1964        assert!(tester.buy_order.is_none());
1965    }
1966
1967    #[rstest]
1968    fn test_submit_limit_order_buys_disabled_returns_ok(
1969        mut config: ExecTesterConfig,
1970        instrument: InstrumentAny,
1971    ) {
1972        config.enable_limit_buys = false;
1973        let mut tester = ExecTester::new(config);
1974        tester.instrument = Some(instrument);
1975
1976        let result = tester.submit_limit_order(OrderSide::Buy, Price::from("50000.0"));
1977
1978        assert!(result.is_ok());
1979        assert!(tester.buy_order.is_none());
1980    }
1981
1982    #[rstest]
1983    fn test_submit_limit_order_sells_disabled_returns_ok(
1984        mut config: ExecTesterConfig,
1985        instrument: InstrumentAny,
1986    ) {
1987        config.enable_limit_sells = false;
1988        let mut tester = ExecTester::new(config);
1989        tester.instrument = Some(instrument);
1990
1991        let result = tester.submit_limit_order(OrderSide::Sell, Price::from("50000.0"));
1992
1993        assert!(result.is_ok());
1994        assert!(tester.sell_order.is_none());
1995    }
1996
1997    #[rstest]
1998    fn test_submit_stop_order_no_instrument_returns_error(config: ExecTesterConfig) {
1999        let mut tester = ExecTester::new(config);
2000
2001        let result = tester.submit_stop_order(OrderSide::Buy, Price::from("51000.0"), None);
2002
2003        assert!(result.is_err());
2004        assert!(result.unwrap_err().to_string().contains("No instrument"));
2005    }
2006
2007    #[rstest]
2008    fn test_submit_stop_order_dry_run_returns_ok(
2009        mut config: ExecTesterConfig,
2010        instrument: InstrumentAny,
2011    ) {
2012        config.dry_run = true;
2013        config.enable_stop_buys = true;
2014        let mut tester = ExecTester::new(config);
2015        tester.instrument = Some(instrument);
2016
2017        let result = tester.submit_stop_order(OrderSide::Buy, Price::from("51000.0"), None);
2018
2019        assert!(result.is_ok());
2020        assert!(tester.buy_stop_order.is_none());
2021    }
2022
2023    #[rstest]
2024    fn test_submit_stop_order_buys_disabled_returns_ok(
2025        mut config: ExecTesterConfig,
2026        instrument: InstrumentAny,
2027    ) {
2028        config.enable_stop_buys = false;
2029        let mut tester = ExecTester::new(config);
2030        tester.instrument = Some(instrument);
2031
2032        let result = tester.submit_stop_order(OrderSide::Buy, Price::from("51000.0"), None);
2033
2034        assert!(result.is_ok());
2035        assert!(tester.buy_stop_order.is_none());
2036    }
2037
2038    #[rstest]
2039    fn test_submit_stop_limit_without_limit_price_returns_error(
2040        mut config: ExecTesterConfig,
2041        instrument: InstrumentAny,
2042    ) {
2043        config.enable_stop_buys = true;
2044        config.stop_order_type = OrderType::StopLimit;
2045        let mut tester = ExecTester::new(config);
2046        tester.instrument = Some(instrument);
2047
2048        // Cannot actually submit without a registered OrderFactory
2049    }
2050
2051    #[rstest]
2052    fn test_open_position_no_instrument_returns_error(config: ExecTesterConfig) {
2053        let mut tester = ExecTester::new(config);
2054
2055        let result = tester.open_position(Decimal::from(1));
2056
2057        assert!(result.is_err());
2058        assert!(result.unwrap_err().to_string().contains("No instrument"));
2059    }
2060
2061    #[rstest]
2062    fn test_open_position_zero_quantity_returns_ok(
2063        config: ExecTesterConfig,
2064        instrument: InstrumentAny,
2065    ) {
2066        let mut tester = ExecTester::new(config);
2067        tester.instrument = Some(instrument);
2068
2069        let result = tester.open_position(Decimal::ZERO);
2070
2071        assert!(result.is_ok());
2072    }
2073
2074    #[rstest]
2075    fn test_config_with_enable_brackets() {
2076        let config = ExecTesterConfig::default().with_enable_brackets(true);
2077        assert!(config.enable_brackets);
2078    }
2079
2080    #[rstest]
2081    fn test_config_with_bracket_offset_ticks() {
2082        let config = ExecTesterConfig::default().with_bracket_offset_ticks(1000);
2083        assert_eq!(config.bracket_offset_ticks, 1000);
2084    }
2085
2086    #[rstest]
2087    fn test_config_with_test_reject_post_only() {
2088        let config = ExecTesterConfig::default().with_test_reject_post_only(true);
2089        assert!(config.test_reject_post_only);
2090    }
2091
2092    #[rstest]
2093    fn test_config_with_test_reject_reduce_only() {
2094        let config = ExecTesterConfig::default().with_test_reject_reduce_only(true);
2095        assert!(config.test_reject_reduce_only);
2096    }
2097
2098    #[rstest]
2099    fn test_config_with_emulation_trigger() {
2100        let config =
2101            ExecTesterConfig::default().with_emulation_trigger(Some(TriggerType::LastPrice));
2102        assert_eq!(config.emulation_trigger, Some(TriggerType::LastPrice));
2103    }
2104
2105    #[rstest]
2106    fn test_config_with_use_quote_quantity() {
2107        let config = ExecTesterConfig::default().with_use_quote_quantity(true);
2108        assert!(config.use_quote_quantity);
2109    }
2110
2111    #[rstest]
2112    fn test_config_with_order_params() {
2113        use serde_json::Value;
2114        let mut params = Params::new();
2115        params.insert("key".to_string(), Value::String("value".to_string()));
2116        let config = ExecTesterConfig::default().with_order_params(Some(params.clone()));
2117        assert_eq!(config.order_params, Some(params));
2118    }
2119
2120    #[rstest]
2121    fn test_submit_bracket_order_no_instrument_returns_error(config: ExecTesterConfig) {
2122        let mut tester = ExecTester::new(config);
2123
2124        let result = tester.submit_bracket_order(OrderSide::Buy, Price::from("50000.0"));
2125
2126        assert!(result.is_err());
2127        assert!(result.unwrap_err().to_string().contains("No instrument"));
2128    }
2129
2130    #[rstest]
2131    fn test_submit_bracket_order_dry_run_returns_ok(
2132        mut config: ExecTesterConfig,
2133        instrument: InstrumentAny,
2134    ) {
2135        config.dry_run = true;
2136        config.enable_brackets = true;
2137        let mut tester = ExecTester::new(config);
2138        tester.instrument = Some(instrument);
2139
2140        let result = tester.submit_bracket_order(OrderSide::Buy, Price::from("50000.0"));
2141
2142        assert!(result.is_ok());
2143        assert!(tester.buy_order.is_none());
2144    }
2145
2146    #[rstest]
2147    fn test_submit_bracket_order_unsupported_entry_type_returns_error(
2148        mut config: ExecTesterConfig,
2149        instrument: InstrumentAny,
2150    ) {
2151        config.enable_brackets = true;
2152        config.bracket_entry_order_type = OrderType::Market;
2153        let mut tester = ExecTester::new(config);
2154        tester.instrument = Some(instrument);
2155
2156        let result = tester.submit_bracket_order(OrderSide::Buy, Price::from("50000.0"));
2157
2158        assert!(result.is_err());
2159        assert!(
2160            result
2161                .unwrap_err()
2162                .to_string()
2163                .contains("Only Limit entry orders are supported")
2164        );
2165    }
2166
2167    #[rstest]
2168    fn test_submit_bracket_order_buys_disabled_returns_ok(
2169        mut config: ExecTesterConfig,
2170        instrument: InstrumentAny,
2171    ) {
2172        config.enable_brackets = true;
2173        config.enable_limit_buys = false;
2174        let mut tester = ExecTester::new(config);
2175        tester.instrument = Some(instrument);
2176
2177        let result = tester.submit_bracket_order(OrderSide::Buy, Price::from("50000.0"));
2178
2179        assert!(result.is_ok());
2180        assert!(tester.buy_order.is_none());
2181    }
2182
2183    #[rstest]
2184    fn test_submit_bracket_order_sells_disabled_returns_ok(
2185        mut config: ExecTesterConfig,
2186        instrument: InstrumentAny,
2187    ) {
2188        config.enable_brackets = true;
2189        config.enable_limit_sells = false;
2190        let mut tester = ExecTester::new(config);
2191        tester.instrument = Some(instrument);
2192
2193        let result = tester.submit_bracket_order(OrderSide::Sell, Price::from("50000.0"));
2194
2195        assert!(result.is_ok());
2196        assert!(tester.sell_order.is_none());
2197    }
2198
2199    #[rstest]
2200    fn test_submit_limit_order_creates_buy_order(
2201        config: ExecTesterConfig,
2202        instrument: InstrumentAny,
2203    ) {
2204        let cache = create_cache_with_instrument(&instrument);
2205        let mut tester = ExecTester::new(config);
2206        register_exec_tester(&mut tester, cache);
2207        tester.instrument = Some(instrument);
2208
2209        let result = tester.submit_limit_order(OrderSide::Buy, Price::from("3000.0"));
2210
2211        assert!(result.is_ok());
2212        assert!(tester.buy_order.is_some());
2213        let order = tester.buy_order.unwrap();
2214        assert_eq!(order.order_side(), OrderSide::Buy);
2215        assert_eq!(order.order_type(), OrderType::Limit);
2216    }
2217
2218    #[rstest]
2219    fn test_submit_limit_order_creates_sell_order(
2220        config: ExecTesterConfig,
2221        instrument: InstrumentAny,
2222    ) {
2223        let cache = create_cache_with_instrument(&instrument);
2224        let mut tester = ExecTester::new(config);
2225        register_exec_tester(&mut tester, cache);
2226        tester.instrument = Some(instrument);
2227
2228        let result = tester.submit_limit_order(OrderSide::Sell, Price::from("3000.0"));
2229
2230        assert!(result.is_ok());
2231        assert!(tester.sell_order.is_some());
2232        let order = tester.sell_order.unwrap();
2233        assert_eq!(order.order_side(), OrderSide::Sell);
2234        assert_eq!(order.order_type(), OrderType::Limit);
2235    }
2236
2237    #[rstest]
2238    fn test_submit_limit_order_with_post_only(
2239        mut config: ExecTesterConfig,
2240        instrument: InstrumentAny,
2241    ) {
2242        config.use_post_only = true;
2243        let cache = create_cache_with_instrument(&instrument);
2244        let mut tester = ExecTester::new(config);
2245        register_exec_tester(&mut tester, cache);
2246        tester.instrument = Some(instrument);
2247
2248        let result = tester.submit_limit_order(OrderSide::Buy, Price::from("3000.0"));
2249
2250        assert!(result.is_ok());
2251        let order = tester.buy_order.unwrap();
2252        assert!(order.is_post_only());
2253    }
2254
2255    #[rstest]
2256    fn test_submit_limit_order_with_expire_time(
2257        mut config: ExecTesterConfig,
2258        instrument: InstrumentAny,
2259    ) {
2260        config.order_expire_time_delta_mins = Some(30);
2261        let cache = create_cache_with_instrument(&instrument);
2262        let mut tester = ExecTester::new(config);
2263        register_exec_tester(&mut tester, cache);
2264        tester.instrument = Some(instrument);
2265
2266        let result = tester.submit_limit_order(OrderSide::Buy, Price::from("3000.0"));
2267
2268        assert!(result.is_ok());
2269        let order = tester.buy_order.unwrap();
2270        assert_eq!(order.time_in_force(), TimeInForce::Gtd);
2271        assert!(order.expire_time().is_some());
2272    }
2273
2274    #[rstest]
2275    fn test_submit_limit_order_with_order_params(
2276        mut config: ExecTesterConfig,
2277        instrument: InstrumentAny,
2278    ) {
2279        use serde_json::Value;
2280        let mut params = Params::new();
2281        params.insert("tdMode".to_string(), Value::String("cross".to_string()));
2282        config.order_params = Some(params);
2283        let cache = create_cache_with_instrument(&instrument);
2284        let mut tester = ExecTester::new(config);
2285        register_exec_tester(&mut tester, cache);
2286        tester.instrument = Some(instrument);
2287
2288        let result = tester.submit_limit_order(OrderSide::Buy, Price::from("3000.0"));
2289
2290        assert!(result.is_ok());
2291        assert!(tester.buy_order.is_some());
2292    }
2293
2294    #[rstest]
2295    fn test_submit_stop_market_order_creates_order(
2296        mut config: ExecTesterConfig,
2297        instrument: InstrumentAny,
2298    ) {
2299        config.enable_stop_buys = true;
2300        config.stop_order_type = OrderType::StopMarket;
2301        let cache = create_cache_with_instrument(&instrument);
2302        let mut tester = ExecTester::new(config);
2303        register_exec_tester(&mut tester, cache);
2304        tester.instrument = Some(instrument);
2305
2306        let result = tester.submit_stop_order(OrderSide::Buy, Price::from("3500.0"), None);
2307
2308        assert!(result.is_ok());
2309        assert!(tester.buy_stop_order.is_some());
2310        let order = tester.buy_stop_order.unwrap();
2311        assert_eq!(order.order_type(), OrderType::StopMarket);
2312        assert_eq!(order.trigger_price(), Some(Price::from("3500.0")));
2313    }
2314
2315    #[rstest]
2316    fn test_submit_stop_limit_order_creates_order(
2317        mut config: ExecTesterConfig,
2318        instrument: InstrumentAny,
2319    ) {
2320        config.enable_stop_sells = true;
2321        config.stop_order_type = OrderType::StopLimit;
2322        let cache = create_cache_with_instrument(&instrument);
2323        let mut tester = ExecTester::new(config);
2324        register_exec_tester(&mut tester, cache);
2325        tester.instrument = Some(instrument);
2326
2327        let result = tester.submit_stop_order(
2328            OrderSide::Sell,
2329            Price::from("2500.0"),
2330            Some(Price::from("2490.0")),
2331        );
2332
2333        assert!(result.is_ok());
2334        assert!(tester.sell_stop_order.is_some());
2335        let order = tester.sell_stop_order.unwrap();
2336        assert_eq!(order.order_type(), OrderType::StopLimit);
2337        assert_eq!(order.trigger_price(), Some(Price::from("2500.0")));
2338        assert_eq!(order.price(), Some(Price::from("2490.0")));
2339    }
2340
2341    #[rstest]
2342    fn test_submit_market_if_touched_order_creates_order(
2343        mut config: ExecTesterConfig,
2344        instrument: InstrumentAny,
2345    ) {
2346        config.enable_stop_buys = true;
2347        config.stop_order_type = OrderType::MarketIfTouched;
2348        let cache = create_cache_with_instrument(&instrument);
2349        let mut tester = ExecTester::new(config);
2350        register_exec_tester(&mut tester, cache);
2351        tester.instrument = Some(instrument);
2352
2353        let result = tester.submit_stop_order(OrderSide::Buy, Price::from("2800.0"), None);
2354
2355        assert!(result.is_ok());
2356        assert!(tester.buy_stop_order.is_some());
2357        let order = tester.buy_stop_order.unwrap();
2358        assert_eq!(order.order_type(), OrderType::MarketIfTouched);
2359    }
2360
2361    #[rstest]
2362    fn test_submit_limit_if_touched_order_creates_order(
2363        mut config: ExecTesterConfig,
2364        instrument: InstrumentAny,
2365    ) {
2366        config.enable_stop_sells = true;
2367        config.stop_order_type = OrderType::LimitIfTouched;
2368        let cache = create_cache_with_instrument(&instrument);
2369        let mut tester = ExecTester::new(config);
2370        register_exec_tester(&mut tester, cache);
2371        tester.instrument = Some(instrument);
2372
2373        let result = tester.submit_stop_order(
2374            OrderSide::Sell,
2375            Price::from("3200.0"),
2376            Some(Price::from("3190.0")),
2377        );
2378
2379        assert!(result.is_ok());
2380        assert!(tester.sell_stop_order.is_some());
2381        let order = tester.sell_stop_order.unwrap();
2382        assert_eq!(order.order_type(), OrderType::LimitIfTouched);
2383    }
2384
2385    #[rstest]
2386    fn test_submit_stop_order_with_emulation_trigger(
2387        mut config: ExecTesterConfig,
2388        instrument: InstrumentAny,
2389    ) {
2390        config.enable_stop_buys = true;
2391        config.stop_order_type = OrderType::StopMarket;
2392        config.emulation_trigger = Some(TriggerType::LastPrice);
2393        let cache = create_cache_with_instrument(&instrument);
2394        let mut tester = ExecTester::new(config);
2395        register_exec_tester(&mut tester, cache);
2396        tester.instrument = Some(instrument);
2397
2398        let result = tester.submit_stop_order(OrderSide::Buy, Price::from("3500.0"), None);
2399
2400        assert!(result.is_ok());
2401        let order = tester.buy_stop_order.unwrap();
2402        assert_eq!(order.emulation_trigger(), Some(TriggerType::LastPrice));
2403    }
2404
2405    #[rstest]
2406    fn test_submit_bracket_order_creates_order_list(
2407        mut config: ExecTesterConfig,
2408        instrument: InstrumentAny,
2409    ) {
2410        config.enable_brackets = true;
2411        config.bracket_offset_ticks = 100;
2412        let cache = create_cache_with_instrument(&instrument);
2413        let mut tester = ExecTester::new(config);
2414        register_exec_tester(&mut tester, cache);
2415        tester.instrument = Some(instrument);
2416
2417        let result = tester.submit_bracket_order(OrderSide::Buy, Price::from("3000.0"));
2418
2419        assert!(result.is_ok());
2420        assert!(tester.buy_order.is_some());
2421        let order = tester.buy_order.unwrap();
2422        assert_eq!(order.order_side(), OrderSide::Buy);
2423        assert_eq!(order.order_type(), OrderType::Limit);
2424        assert_eq!(order.contingency_type(), Some(ContingencyType::Oto));
2425    }
2426
2427    #[rstest]
2428    fn test_submit_bracket_order_sell_creates_order_list(
2429        mut config: ExecTesterConfig,
2430        instrument: InstrumentAny,
2431    ) {
2432        config.enable_brackets = true;
2433        config.bracket_offset_ticks = 100;
2434        let cache = create_cache_with_instrument(&instrument);
2435        let mut tester = ExecTester::new(config);
2436        register_exec_tester(&mut tester, cache);
2437        tester.instrument = Some(instrument);
2438
2439        let result = tester.submit_bracket_order(OrderSide::Sell, Price::from("3000.0"));
2440
2441        assert!(result.is_ok());
2442        assert!(tester.sell_order.is_some());
2443        let order = tester.sell_order.unwrap();
2444        assert_eq!(order.order_side(), OrderSide::Sell);
2445        assert_eq!(order.contingency_type(), Some(ContingencyType::Oto));
2446    }
2447
2448    #[rstest]
2449    fn test_open_position_creates_market_order(
2450        config: ExecTesterConfig,
2451        instrument: InstrumentAny,
2452    ) {
2453        let cache = create_cache_with_instrument(&instrument);
2454        let mut tester = ExecTester::new(config);
2455        register_exec_tester(&mut tester, cache);
2456        tester.instrument = Some(instrument);
2457
2458        let result = tester.open_position(Decimal::from(1));
2459
2460        assert!(result.is_ok());
2461    }
2462
2463    #[rstest]
2464    fn test_open_position_with_reduce_only_rejection(
2465        mut config: ExecTesterConfig,
2466        instrument: InstrumentAny,
2467    ) {
2468        config.test_reject_reduce_only = true;
2469        let cache = create_cache_with_instrument(&instrument);
2470        let mut tester = ExecTester::new(config);
2471        register_exec_tester(&mut tester, cache);
2472        tester.instrument = Some(instrument);
2473
2474        // Should succeed in creating order (rejection happens at exchange)
2475        let result = tester.open_position(Decimal::from(1));
2476
2477        assert!(result.is_ok());
2478    }
2479
2480    #[rstest]
2481    fn test_submit_stop_limit_without_limit_price_fails(
2482        mut config: ExecTesterConfig,
2483        instrument: InstrumentAny,
2484    ) {
2485        config.enable_stop_buys = true;
2486        config.stop_order_type = OrderType::StopLimit;
2487        let cache = create_cache_with_instrument(&instrument);
2488        let mut tester = ExecTester::new(config);
2489        register_exec_tester(&mut tester, cache);
2490        tester.instrument = Some(instrument);
2491
2492        let result = tester.submit_stop_order(OrderSide::Buy, Price::from("3500.0"), None);
2493
2494        assert!(result.is_err());
2495        assert!(
2496            result
2497                .unwrap_err()
2498                .to_string()
2499                .contains("requires limit_price")
2500        );
2501    }
2502
2503    #[rstest]
2504    fn test_submit_limit_if_touched_without_limit_price_fails(
2505        mut config: ExecTesterConfig,
2506        instrument: InstrumentAny,
2507    ) {
2508        config.enable_stop_sells = true;
2509        config.stop_order_type = OrderType::LimitIfTouched;
2510        let cache = create_cache_with_instrument(&instrument);
2511        let mut tester = ExecTester::new(config);
2512        register_exec_tester(&mut tester, cache);
2513        tester.instrument = Some(instrument);
2514
2515        let result = tester.submit_stop_order(OrderSide::Sell, Price::from("3200.0"), None);
2516
2517        assert!(result.is_err());
2518        assert!(
2519            result
2520                .unwrap_err()
2521                .to_string()
2522                .contains("requires limit_price")
2523        );
2524    }
2525
2526    #[rstest]
2527    fn test_config_new_fields_default_values(config: ExecTesterConfig) {
2528        assert!(config.limit_time_in_force.is_none());
2529        assert!(config.stop_time_in_force.is_none());
2530    }
2531
2532    #[rstest]
2533    fn test_config_with_limit_time_in_force_builder() {
2534        let config = ExecTesterConfig::default().with_limit_time_in_force(Some(TimeInForce::Ioc));
2535        assert_eq!(config.limit_time_in_force, Some(TimeInForce::Ioc));
2536    }
2537
2538    #[rstest]
2539    fn test_config_with_stop_time_in_force_builder() {
2540        let config = ExecTesterConfig::default().with_stop_time_in_force(Some(TimeInForce::Day));
2541        assert_eq!(config.stop_time_in_force, Some(TimeInForce::Day));
2542    }
2543
2544    #[rstest]
2545    fn test_submit_limit_order_with_limit_time_in_force(
2546        mut config: ExecTesterConfig,
2547        instrument: InstrumentAny,
2548    ) {
2549        config.limit_time_in_force = Some(TimeInForce::Ioc);
2550        let cache = create_cache_with_instrument(&instrument);
2551        let mut tester = ExecTester::new(config);
2552        register_exec_tester(&mut tester, cache);
2553        tester.instrument = Some(instrument);
2554
2555        let result = tester.submit_limit_order(OrderSide::Buy, Price::from("3000.0"));
2556
2557        assert!(result.is_ok());
2558        let order = tester.buy_order.unwrap();
2559        assert_eq!(order.time_in_force(), TimeInForce::Ioc);
2560        assert!(order.expire_time().is_none());
2561    }
2562
2563    #[rstest]
2564    fn test_submit_limit_order_limit_time_in_force_overrides_expire(
2565        mut config: ExecTesterConfig,
2566        instrument: InstrumentAny,
2567    ) {
2568        // limit_time_in_force takes priority over order_expire_time_delta_mins
2569        config.limit_time_in_force = Some(TimeInForce::Day);
2570        config.order_expire_time_delta_mins = Some(30);
2571        let cache = create_cache_with_instrument(&instrument);
2572        let mut tester = ExecTester::new(config);
2573        register_exec_tester(&mut tester, cache);
2574        tester.instrument = Some(instrument);
2575
2576        let result = tester.submit_limit_order(OrderSide::Buy, Price::from("3000.0"));
2577
2578        assert!(result.is_ok());
2579        let order = tester.buy_order.unwrap();
2580        assert_eq!(order.time_in_force(), TimeInForce::Day);
2581        assert!(order.expire_time().is_none());
2582    }
2583
2584    #[rstest]
2585    fn test_submit_stop_order_with_stop_time_in_force(
2586        mut config: ExecTesterConfig,
2587        instrument: InstrumentAny,
2588    ) {
2589        config.enable_stop_buys = true;
2590        config.stop_time_in_force = Some(TimeInForce::Day);
2591        let cache = create_cache_with_instrument(&instrument);
2592        let mut tester = ExecTester::new(config);
2593        register_exec_tester(&mut tester, cache);
2594        tester.instrument = Some(instrument);
2595
2596        let result = tester.submit_stop_order(OrderSide::Buy, Price::from("3200.0"), None);
2597
2598        assert!(result.is_ok());
2599        let order = tester.buy_stop_order.unwrap();
2600        assert_eq!(order.time_in_force(), TimeInForce::Day);
2601        assert!(order.expire_time().is_none());
2602    }
2603
2604    #[rstest]
2605    fn test_submit_stop_order_stop_time_in_force_overrides_expire(
2606        mut config: ExecTesterConfig,
2607        instrument: InstrumentAny,
2608    ) {
2609        config.enable_stop_buys = true;
2610        config.stop_time_in_force = Some(TimeInForce::Ioc);
2611        config.order_expire_time_delta_mins = Some(30);
2612        let cache = create_cache_with_instrument(&instrument);
2613        let mut tester = ExecTester::new(config);
2614        register_exec_tester(&mut tester, cache);
2615        tester.instrument = Some(instrument);
2616
2617        let result = tester.submit_stop_order(OrderSide::Buy, Price::from("3200.0"), None);
2618
2619        assert!(result.is_ok());
2620        let order = tester.buy_stop_order.unwrap();
2621        assert_eq!(order.time_in_force(), TimeInForce::Ioc);
2622        assert!(order.expire_time().is_none());
2623    }
2624}