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