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
453 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 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(()); };
626 let Some(best_ask) = book.best_ask_price() else {
627 return Ok(()); };
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 #[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 fn get_price_offset(&self, instrument: &InstrumentAny) -> f64 {
728 instrument.price_increment().as_f64() * self.config.tob_offset_ticks as f64
729 }
730
731 fn is_order_active(&self, order: &OrderAny) -> bool {
733 order.is_active_local() || order.is_inflight() || order.is_open()
734 }
735
736 fn get_order_trigger_price(&self, order: &OrderAny) -> Option<Price> {
738 order.trigger_price()
739 }
740
741 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 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 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 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 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 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 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 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 let trigger_price = if matches!(
914 self.config.stop_order_type,
915 OrderType::LimitIfTouched | OrderType::MarketIfTouched
916 ) {
917 instrument.make_price(best_bid.as_f64() - stop_offset)
919 } else {
920 instrument.make_price(best_ask.as_f64() + stop_offset)
922 };
923
924 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 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 let trigger_price = if matches!(
989 self.config.stop_order_type,
990 OrderType::LimitIfTouched | OrderType::MarketIfTouched
991 ) {
992 instrument.make_price(best_ask.as_f64() + stop_offset)
994 } else {
995 instrument.make_price(best_bid.as_f64() - stop_offset)
997 };
998
999 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 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, Some(self.config.use_quote_quantity),
1102 self.config.order_display_qty,
1103 self.config.emulation_trigger,
1104 None, None, None, None, None, );
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 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 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, Some(self.config.use_quote_quantity),
1176 None, self.config.emulation_trigger,
1178 None, None, None, None, None, ),
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, None, Some(self.config.use_quote_quantity),
1200 self.config.order_display_qty,
1201 self.config.emulation_trigger,
1202 None, None, None, None, None, )
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, Some(self.config.use_quote_quantity),
1219 self.config.emulation_trigger,
1220 None, None, None, None, None, ),
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, None, Some(self.config.use_quote_quantity),
1242 self.config.order_display_qty,
1243 self.config.emulation_trigger,
1244 None, None, None, None, None, )
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 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), sl_trigger_price, Some(self.config.stop_trigger_type), tp_price, None, Some(time_in_force),
1341 expire_time,
1342 Some(self.config.use_post_only),
1343 None, Some(self.config.use_quote_quantity),
1345 self.config.emulation_trigger,
1346 None, None, None, None, );
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 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 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, None, None, None, );
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 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 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 config.stop_order_type = OrderType::StopMarket;
1590 assert_eq!(config.stop_order_type, OrderType::StopMarket);
1591
1592 config.stop_order_type = OrderType::StopLimit;
1594 assert_eq!(config.stop_order_type, OrderType::StopLimit);
1595
1596 config.stop_order_type = OrderType::MarketIfTouched;
1598 assert_eq!(config.stop_order_type, OrderType::MarketIfTouched);
1599
1600 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 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 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 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("e);
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("e);
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 }
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 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}