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