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