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