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