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