1use nautilus_common::{actor::DataActor, enums::LogColor, log_info, log_warn, timer::TimeEvent};
17use nautilus_core::{UnixNanos, datetime::secs_to_nanos_unchecked};
18use nautilus_model::{
19 data::{Bar, IndexPriceUpdate, MarkPriceUpdate, OrderBookDeltas, QuoteTick, TradeTick},
20 enums::{OrderSide, OrderType, TimeInForce},
21 identifiers::{InstrumentId, StrategyId},
22 instruments::{Instrument, InstrumentAny},
23 orderbook::OrderBook,
24 orders::{Order, OrderAny},
25 types::Price,
26};
27use nautilus_trading::{
28 nautilus_strategy,
29 strategy::{Strategy, StrategyCore},
30};
31use rust_decimal::{Decimal, prelude::ToPrimitive};
32
33use super::config::ExecTesterConfig;
34
35#[derive(Debug)]
44pub struct ExecTester {
45 pub(super) core: StrategyCore,
46 pub(super) config: ExecTesterConfig,
47 pub(super) instrument: Option<InstrumentAny>,
48 pub(super) price_offset: Option<f64>,
49 pub(super) preinitialized_market_data: bool,
50
51 pub(super) buy_order: Option<OrderAny>,
53 pub(super) sell_order: Option<OrderAny>,
54 pub(super) buy_stop_order: Option<OrderAny>,
55 pub(super) sell_stop_order: Option<OrderAny>,
56}
57
58nautilus_strategy!(ExecTester, {
59 fn external_order_claims(&self) -> Option<Vec<InstrumentId>> {
60 self.config.base.external_order_claims.clone()
61 }
62});
63
64impl DataActor for ExecTester {
65 fn on_start(&mut self) -> anyhow::Result<()> {
66 Strategy::on_start(self)?;
67
68 let instrument_id = self.config.instrument_id;
69 let client_id = self.config.client_id;
70
71 let instrument = {
72 let cache = self.cache();
73 cache.instrument(&instrument_id).cloned()
74 };
75
76 if let Some(inst) = instrument {
77 self.initialize_with_instrument(inst, true)?;
78 } else {
79 log::info!("Instrument {instrument_id} not in cache, subscribing...");
80 self.subscribe_instrument(instrument_id, client_id, None);
81
82 if self.config.subscribe_quotes {
85 self.subscribe_quotes(instrument_id, client_id, None);
86 }
87
88 if self.config.subscribe_trades {
89 self.subscribe_trades(instrument_id, client_id, None);
90 }
91 self.preinitialized_market_data =
92 self.config.subscribe_quotes || self.config.subscribe_trades;
93 }
94
95 Ok(())
96 }
97
98 fn on_instrument(&mut self, instrument: &InstrumentAny) -> anyhow::Result<()> {
99 if instrument.id() == self.config.instrument_id && self.instrument.is_none() {
100 let id = instrument.id();
101 log::info!("Received instrument {id}, initializing...");
102 self.initialize_with_instrument(instrument.clone(), !self.preinitialized_market_data)?;
103 }
104 Ok(())
105 }
106
107 fn on_stop(&mut self) -> anyhow::Result<()> {
108 if self.config.dry_run {
109 log_warn!("Dry run mode, skipping cancel all orders and close all positions");
110 return Ok(());
111 }
112
113 let instrument_id = self.config.instrument_id;
114 let client_id = self.config.client_id;
115
116 if self.config.cancel_orders_on_stop {
117 let strategy_id = StrategyId::from(self.core.actor_id.inner().as_str());
118
119 if self.config.use_individual_cancels_on_stop {
120 let cache = self.cache();
121 let open_orders: Vec<OrderAny> = cache
122 .orders_open(None, Some(&instrument_id), Some(&strategy_id), None, None)
123 .iter()
124 .map(|o| (*o).clone())
125 .collect();
126 drop(cache);
127
128 for order in open_orders {
129 if let Err(e) = self.cancel_order(order, client_id) {
130 log::error!("Failed to cancel order: {e}");
131 }
132 }
133 } else if self.config.use_batch_cancel_on_stop {
134 let cache = self.cache();
135 let open_orders: Vec<OrderAny> = cache
136 .orders_open(None, Some(&instrument_id), Some(&strategy_id), None, None)
137 .iter()
138 .map(|o| (*o).clone())
139 .collect();
140 drop(cache);
141
142 if let Err(e) = self.cancel_orders(open_orders, client_id, None) {
143 log::error!("Failed to batch cancel orders: {e}");
144 }
145 } else if let Err(e) = self.cancel_all_orders(instrument_id, None, client_id) {
146 log::error!("Failed to cancel all orders: {e}");
147 }
148 }
149
150 if self.config.close_positions_on_stop {
151 let time_in_force = self
152 .config
153 .close_positions_time_in_force
154 .or(Some(TimeInForce::Gtc));
155
156 if let Err(e) = self.close_all_positions(
157 instrument_id,
158 None,
159 client_id,
160 None,
161 time_in_force,
162 Some(self.config.reduce_only_on_stop),
163 None,
164 ) {
165 log::error!("Failed to close all positions: {e}");
166 }
167 }
168
169 if self.config.can_unsubscribe && self.instrument.is_some() {
170 if self.config.subscribe_quotes {
171 self.unsubscribe_quotes(instrument_id, client_id, None);
172 }
173
174 if self.config.subscribe_trades {
175 self.unsubscribe_trades(instrument_id, client_id, None);
176 }
177
178 if self.config.subscribe_book {
179 self.unsubscribe_book_at_interval(
180 instrument_id,
181 self.config.book_interval_ms,
182 client_id,
183 None,
184 );
185 }
186 }
187
188 Ok(())
189 }
190
191 fn on_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
192 if self.config.log_data {
193 log_info!("{quote:?}", color = LogColor::Cyan);
194 }
195
196 self.maintain_orders(quote.bid_price, quote.ask_price);
197 Ok(())
198 }
199
200 fn on_trade(&mut self, trade: &TradeTick) -> anyhow::Result<()> {
201 if self.config.log_data {
202 log_info!("{trade:?}", color = LogColor::Cyan);
203 }
204 Ok(())
205 }
206
207 fn on_book(&mut self, book: &OrderBook) -> anyhow::Result<()> {
208 if self.config.log_data {
209 let num_levels = self.config.book_levels_to_print;
210 let instrument_id = book.instrument_id;
211 let book_str = book.pprint(num_levels, None);
212 log_info!("\n{instrument_id}\n{book_str}", color = LogColor::Cyan);
213
214 if self.is_registered() {
216 let cache = self.cache();
217 if let Some(own_book) = cache.own_order_book(&instrument_id) {
218 let own_book_str = own_book.pprint(num_levels, None);
219 log_info!(
220 "\n{instrument_id} (own)\n{own_book_str}",
221 color = LogColor::Magenta
222 );
223 }
224 }
225 }
226
227 let Some(best_bid) = book.best_bid_price() else {
228 return Ok(()); };
230 let Some(best_ask) = book.best_ask_price() else {
231 return Ok(()); };
233
234 self.maintain_orders(best_bid, best_ask);
235 Ok(())
236 }
237
238 fn on_book_deltas(&mut self, deltas: &OrderBookDeltas) -> anyhow::Result<()> {
239 if self.config.log_data {
240 log_info!("{deltas:?}", color = LogColor::Cyan);
241 }
242 Ok(())
243 }
244
245 fn on_bar(&mut self, bar: &Bar) -> anyhow::Result<()> {
246 if self.config.log_data {
247 log_info!("{bar:?}", color = LogColor::Cyan);
248 }
249 Ok(())
250 }
251
252 fn on_mark_price(&mut self, mark_price: &MarkPriceUpdate) -> anyhow::Result<()> {
253 if self.config.log_data {
254 log_info!("{mark_price:?}", color = LogColor::Cyan);
255 }
256 Ok(())
257 }
258
259 fn on_index_price(&mut self, index_price: &IndexPriceUpdate) -> anyhow::Result<()> {
260 if self.config.log_data {
261 log_info!("{index_price:?}", color = LogColor::Cyan);
262 }
263 Ok(())
264 }
265
266 fn on_time_event(&mut self, event: &TimeEvent) -> anyhow::Result<()> {
267 Strategy::on_time_event(self, event)
268 }
269}
270
271impl ExecTester {
272 #[must_use]
274 pub fn new(config: ExecTesterConfig) -> Self {
275 Self {
276 core: StrategyCore::new(config.base.clone()),
277 config,
278 instrument: None,
279 price_offset: None,
280 preinitialized_market_data: false,
281 buy_order: None,
282 sell_order: None,
283 buy_stop_order: None,
284 sell_stop_order: None,
285 }
286 }
287
288 fn initialize_with_instrument(
289 &mut self,
290 instrument: InstrumentAny,
291 subscribe_market_data: bool,
292 ) -> anyhow::Result<()> {
293 let instrument_id = self.config.instrument_id;
294 let client_id = self.config.client_id;
295
296 self.price_offset = Some(self.get_price_offset(&instrument));
297 self.instrument = Some(instrument);
298
299 if subscribe_market_data && self.config.subscribe_quotes {
300 self.subscribe_quotes(instrument_id, client_id, None);
301 }
302
303 if subscribe_market_data && self.config.subscribe_trades {
304 self.subscribe_trades(instrument_id, client_id, None);
305 }
306
307 if self.config.subscribe_book {
308 self.subscribe_book_at_interval(
309 instrument_id,
310 self.config.book_type,
311 self.config.book_depth,
312 self.config.book_interval_ms,
313 client_id,
314 None,
315 );
316 }
317
318 if let Some(qty) = self.config.open_position_on_start_qty {
319 self.open_position(qty)?;
320 }
321
322 Ok(())
323 }
324
325 pub(super) fn get_price_offset(&self, instrument: &InstrumentAny) -> f64 {
326 instrument.price_increment().as_f64() * self.config.tob_offset_ticks as f64
327 }
328
329 fn expire_time_from_delta(&self, mins: u64) -> UnixNanos {
330 let current_ns = self.timestamp_ns();
331 let delta_ns = secs_to_nanos_unchecked((mins * 60) as f64);
332 UnixNanos::from(current_ns.as_u64() + delta_ns)
333 }
334
335 fn resolve_time_in_force(
336 &self,
337 tif_override: Option<TimeInForce>,
338 ) -> (TimeInForce, Option<UnixNanos>) {
339 match (tif_override, self.config.order_expire_time_delta_mins) {
340 (Some(TimeInForce::Gtd), Some(mins)) => {
341 (TimeInForce::Gtd, Some(self.expire_time_from_delta(mins)))
342 }
343 (Some(TimeInForce::Gtd), None) => {
344 log_warn!(
345 "GTD time in force requires order_expire_time_delta_mins, falling back to GTC"
346 );
347 (TimeInForce::Gtc, None)
348 }
349 (Some(tif), _) => (tif, None),
350 (None, Some(mins)) => (TimeInForce::Gtd, Some(self.expire_time_from_delta(mins))),
351 (None, None) => (TimeInForce::Gtc, None),
352 }
353 }
354
355 pub(super) fn is_order_active(&self, order: &OrderAny) -> bool {
356 order.is_active_local() || order.is_inflight() || order.is_open()
357 }
358
359 pub(super) fn get_order_trigger_price(&self, order: &OrderAny) -> Option<Price> {
360 order.trigger_price()
361 }
362
363 fn modify_stop_order(
364 &mut self,
365 order: OrderAny,
366 trigger_price: Price,
367 limit_price: Option<Price>,
368 ) -> anyhow::Result<()> {
369 let client_id = self.config.client_id;
370
371 match &order {
372 OrderAny::StopMarket(_)
373 | OrderAny::MarketIfTouched(_)
374 | OrderAny::TrailingStopMarket(_) => {
375 self.modify_order(order, None, None, Some(trigger_price), client_id)
376 }
377 OrderAny::StopLimit(_) | OrderAny::LimitIfTouched(_) => {
378 self.modify_order(order, None, limit_price, Some(trigger_price), client_id)
379 }
380 _ => {
381 log_warn!("Cannot modify order of type {:?}", order.order_type());
382 Ok(())
383 }
384 }
385 }
386
387 fn submit_order_apply_params(&mut self, order: OrderAny) -> anyhow::Result<()> {
389 let client_id = self.config.client_id;
390 if let Some(params) = &self.config.order_params {
391 self.submit_order_with_params(order, None, client_id, params.clone())
392 } else {
393 self.submit_order(order, None, client_id)
394 }
395 }
396
397 pub(super) fn maintain_orders(&mut self, best_bid: Price, best_ask: Price) {
399 if self.instrument.is_none() || self.config.dry_run {
400 return;
401 }
402
403 if self.config.batch_submit_limit_pair
404 && self.config.enable_limit_buys
405 && self.config.enable_limit_sells
406 {
407 self.maintain_batch_limit_pair(best_bid, best_ask);
408 return;
409 }
410
411 if self.config.enable_limit_buys {
412 self.maintain_buy_orders(best_bid, best_ask);
413 }
414
415 if self.config.enable_limit_sells {
416 self.maintain_sell_orders(best_bid, best_ask);
417 }
418
419 if self.config.enable_stop_buys {
420 self.maintain_stop_buy_orders(best_bid, best_ask);
421 }
422
423 if self.config.enable_stop_sells {
424 self.maintain_stop_sell_orders(best_bid, best_ask);
425 }
426 }
427
428 fn maintain_buy_orders(&mut self, best_bid: Price, best_ask: Price) {
430 let Some(instrument) = &self.instrument else {
431 return;
432 };
433 let Some(price_offset) = self.price_offset else {
434 return;
435 };
436
437 let price = if self.config.test_reject_post_only {
439 instrument.make_price(best_ask.as_f64() + price_offset)
440 } else {
441 instrument.make_price(best_bid.as_f64() - price_offset)
442 };
443
444 let needs_new_order = match &self.buy_order {
445 None => true,
446 Some(order) => !self.is_order_active(order),
447 };
448
449 if needs_new_order {
450 let result = if self.config.enable_brackets {
451 self.submit_bracket_order(OrderSide::Buy, price)
452 } else {
453 self.submit_limit_order(OrderSide::Buy, price)
454 };
455
456 if let Err(e) = result {
457 log::error!("Failed to submit buy order: {e}");
458 }
459 } else if let Some(order) = &self.buy_order
460 && order.venue_order_id().is_some()
461 && !order.is_pending_update()
462 && !order.is_pending_cancel()
463 && let Some(order_price) = order.price()
464 && order_price < price
465 {
466 let client_id = self.config.client_id;
467 if self.config.modify_orders_to_maintain_tob_offset {
468 let order_clone = order.clone();
469 if let Err(e) = self.modify_order(order_clone, None, Some(price), None, client_id) {
470 log::error!("Failed to modify buy order: {e}");
471 }
472 } else if self.config.cancel_replace_orders_to_maintain_tob_offset {
473 let order_clone = order.clone();
474 let _ = self.cancel_order(order_clone, client_id);
475
476 if let Err(e) = self.submit_limit_order(OrderSide::Buy, price) {
477 log::error!("Failed to submit replacement buy order: {e}");
478 }
479 }
480 }
481 }
482
483 fn maintain_sell_orders(&mut self, best_bid: Price, best_ask: Price) {
485 let Some(instrument) = &self.instrument else {
486 return;
487 };
488 let Some(price_offset) = self.price_offset else {
489 return;
490 };
491
492 let price = if self.config.test_reject_post_only {
494 instrument.make_price(best_bid.as_f64() - price_offset)
495 } else {
496 instrument.make_price(best_ask.as_f64() + price_offset)
497 };
498
499 let needs_new_order = match &self.sell_order {
500 None => true,
501 Some(order) => !self.is_order_active(order),
502 };
503
504 if needs_new_order {
505 let result = if self.config.enable_brackets {
506 self.submit_bracket_order(OrderSide::Sell, price)
507 } else {
508 self.submit_limit_order(OrderSide::Sell, price)
509 };
510
511 if let Err(e) = result {
512 log::error!("Failed to submit sell order: {e}");
513 }
514 } else if let Some(order) = &self.sell_order
515 && order.venue_order_id().is_some()
516 && !order.is_pending_update()
517 && !order.is_pending_cancel()
518 && let Some(order_price) = order.price()
519 && order_price > price
520 {
521 let client_id = self.config.client_id;
522 if self.config.modify_orders_to_maintain_tob_offset {
523 let order_clone = order.clone();
524 if let Err(e) = self.modify_order(order_clone, None, Some(price), None, client_id) {
525 log::error!("Failed to modify sell order: {e}");
526 }
527 } else if self.config.cancel_replace_orders_to_maintain_tob_offset {
528 let order_clone = order.clone();
529 let _ = self.cancel_order(order_clone, client_id);
530
531 if let Err(e) = self.submit_limit_order(OrderSide::Sell, price) {
532 log::error!("Failed to submit replacement sell order: {e}");
533 }
534 }
535 }
536 }
537
538 fn maintain_batch_limit_pair(&mut self, best_bid: Price, best_ask: Price) {
540 let Some(instrument) = &self.instrument else {
541 return;
542 };
543 let Some(price_offset) = self.price_offset else {
544 return;
545 };
546
547 let buy_needs = match &self.buy_order {
548 None => true,
549 Some(order) => !self.is_order_active(order),
550 };
551 let sell_needs = match &self.sell_order {
552 None => true,
553 Some(order) => !self.is_order_active(order),
554 };
555
556 if !buy_needs || !sell_needs {
557 return;
558 }
559
560 let buy_price = instrument.make_price(best_bid.as_f64() - price_offset);
561 let sell_price = instrument.make_price(best_ask.as_f64() + price_offset);
562 let quantity = instrument.make_qty(self.config.order_qty.as_f64(), None);
563 let (time_in_force, expire_time) =
564 self.resolve_time_in_force(self.config.limit_time_in_force);
565
566 let buy_order = self.core.order_factory().limit(
567 self.config.instrument_id,
568 OrderSide::Buy,
569 quantity,
570 buy_price,
571 Some(time_in_force),
572 expire_time,
573 Some(self.config.use_post_only || self.config.test_reject_post_only),
574 None,
575 Some(self.config.use_quote_quantity),
576 self.config.order_display_qty,
577 self.config.emulation_trigger,
578 None,
579 None,
580 None,
581 None,
582 None,
583 );
584
585 let sell_order = self.core.order_factory().limit(
586 self.config.instrument_id,
587 OrderSide::Sell,
588 quantity,
589 sell_price,
590 Some(time_in_force),
591 expire_time,
592 Some(self.config.use_post_only || self.config.test_reject_post_only),
593 None,
594 Some(self.config.use_quote_quantity),
595 self.config.order_display_qty,
596 self.config.emulation_trigger,
597 None,
598 None,
599 None,
600 None,
601 None,
602 );
603
604 self.buy_order = Some(buy_order.clone());
605 self.sell_order = Some(sell_order.clone());
606
607 let client_id = self.config.client_id;
608 if let Err(e) = self.submit_order_list(vec![buy_order, sell_order], None, client_id) {
609 log::error!("Failed to submit batch limit pair: {e}");
610 }
611 }
612
613 fn maintain_stop_buy_orders(&mut self, best_bid: Price, best_ask: Price) {
615 let Some(instrument) = &self.instrument else {
616 return;
617 };
618
619 let price_increment = instrument.price_increment().as_f64();
620 let stop_offset = price_increment * self.config.stop_offset_ticks as f64;
621
622 let trigger_price = if matches!(
624 self.config.stop_order_type,
625 OrderType::LimitIfTouched | OrderType::MarketIfTouched | OrderType::TrailingStopMarket
626 ) {
627 instrument.make_price(best_bid.as_f64() - stop_offset)
629 } else {
630 instrument.make_price(best_ask.as_f64() + stop_offset)
632 };
633
634 let limit_price = if matches!(
636 self.config.stop_order_type,
637 OrderType::StopLimit | OrderType::LimitIfTouched
638 ) {
639 if let Some(limit_offset_ticks) = self.config.stop_limit_offset_ticks {
640 let limit_offset = price_increment * limit_offset_ticks as f64;
641
642 if self.config.stop_order_type == OrderType::LimitIfTouched {
643 Some(instrument.make_price(trigger_price.as_f64() - limit_offset))
644 } else {
645 Some(instrument.make_price(trigger_price.as_f64() + limit_offset))
646 }
647 } else {
648 Some(trigger_price)
649 }
650 } else {
651 None
652 };
653
654 let needs_new_order = match &self.buy_stop_order {
655 None => true,
656 Some(order) => !self.is_order_active(order),
657 };
658
659 if needs_new_order {
660 if let Err(e) = self.submit_stop_order(OrderSide::Buy, trigger_price, limit_price) {
661 log::error!("Failed to submit buy stop order: {e}");
662 }
663 } else if let Some(order) = &self.buy_stop_order
664 && order.venue_order_id().is_some()
665 && !order.is_pending_update()
666 && !order.is_pending_cancel()
667 {
668 let current_trigger = self.get_order_trigger_price(order);
669 if current_trigger.is_some() && current_trigger != Some(trigger_price) {
670 if self.config.modify_stop_orders_to_maintain_offset {
671 let order_clone = order.clone();
672 if let Err(e) = self.modify_stop_order(order_clone, trigger_price, limit_price)
673 {
674 log::error!("Failed to modify buy stop order: {e}");
675 }
676 } else if self.config.cancel_replace_stop_orders_to_maintain_offset {
677 let order_clone = order.clone();
678 let _ = self.cancel_order(order_clone, self.config.client_id);
679
680 if let Err(e) =
681 self.submit_stop_order(OrderSide::Buy, trigger_price, limit_price)
682 {
683 log::error!("Failed to submit replacement buy stop order: {e}");
684 }
685 }
686 }
687 }
688 }
689
690 fn maintain_stop_sell_orders(&mut self, best_bid: Price, best_ask: Price) {
692 let Some(instrument) = &self.instrument else {
693 return;
694 };
695
696 let price_increment = instrument.price_increment().as_f64();
697 let stop_offset = price_increment * self.config.stop_offset_ticks as f64;
698
699 let trigger_price = if matches!(
701 self.config.stop_order_type,
702 OrderType::LimitIfTouched | OrderType::MarketIfTouched | OrderType::TrailingStopMarket
703 ) {
704 instrument.make_price(best_ask.as_f64() + stop_offset)
706 } else {
707 instrument.make_price(best_bid.as_f64() - stop_offset)
709 };
710
711 let limit_price = if matches!(
713 self.config.stop_order_type,
714 OrderType::StopLimit | OrderType::LimitIfTouched
715 ) {
716 if let Some(limit_offset_ticks) = self.config.stop_limit_offset_ticks {
717 let limit_offset = price_increment * limit_offset_ticks as f64;
718
719 if self.config.stop_order_type == OrderType::LimitIfTouched {
720 Some(instrument.make_price(trigger_price.as_f64() + limit_offset))
721 } else {
722 Some(instrument.make_price(trigger_price.as_f64() - limit_offset))
723 }
724 } else {
725 Some(trigger_price)
726 }
727 } else {
728 None
729 };
730
731 let needs_new_order = match &self.sell_stop_order {
732 None => true,
733 Some(order) => !self.is_order_active(order),
734 };
735
736 if needs_new_order {
737 if let Err(e) = self.submit_stop_order(OrderSide::Sell, trigger_price, limit_price) {
738 log::error!("Failed to submit sell stop order: {e}");
739 }
740 } else if let Some(order) = &self.sell_stop_order
741 && order.venue_order_id().is_some()
742 && !order.is_pending_update()
743 && !order.is_pending_cancel()
744 {
745 let current_trigger = self.get_order_trigger_price(order);
746 if current_trigger.is_some() && current_trigger != Some(trigger_price) {
747 if self.config.modify_stop_orders_to_maintain_offset {
748 let order_clone = order.clone();
749 if let Err(e) = self.modify_stop_order(order_clone, trigger_price, limit_price)
750 {
751 log::error!("Failed to modify sell stop order: {e}");
752 }
753 } else if self.config.cancel_replace_stop_orders_to_maintain_offset {
754 let order_clone = order.clone();
755 let _ = self.cancel_order(order_clone, self.config.client_id);
756
757 if let Err(e) =
758 self.submit_stop_order(OrderSide::Sell, trigger_price, limit_price)
759 {
760 log::error!("Failed to submit replacement sell stop order: {e}");
761 }
762 }
763 }
764 }
765 }
766
767 pub(super) fn submit_limit_order(
773 &mut self,
774 order_side: OrderSide,
775 price: Price,
776 ) -> anyhow::Result<()> {
777 let Some(instrument) = &self.instrument else {
778 anyhow::bail!("No instrument loaded");
779 };
780
781 if self.config.dry_run {
782 log_warn!("Dry run, skipping create {order_side:?} order");
783 return Ok(());
784 }
785
786 if order_side == OrderSide::Buy && !self.config.enable_limit_buys {
787 log_warn!("BUY orders not enabled, skipping");
788 return Ok(());
789 } else if order_side == OrderSide::Sell && !self.config.enable_limit_sells {
790 log_warn!("SELL orders not enabled, skipping");
791 return Ok(());
792 }
793
794 let (time_in_force, expire_time) =
795 self.resolve_time_in_force(self.config.limit_time_in_force);
796
797 let quantity = instrument.make_qty(self.config.order_qty.as_f64(), None);
798
799 let order = self.core.order_factory().limit(
800 self.config.instrument_id,
801 order_side,
802 quantity,
803 price,
804 Some(time_in_force),
805 expire_time,
806 Some(self.config.use_post_only || self.config.test_reject_post_only),
807 None, Some(self.config.use_quote_quantity),
809 self.config.order_display_qty,
810 self.config.emulation_trigger,
811 None, None, None, None, None, );
817
818 if order_side == OrderSide::Buy {
819 self.buy_order = Some(order.clone());
820 } else {
821 self.sell_order = Some(order.clone());
822 }
823
824 self.submit_order_apply_params(order)
825 }
826
827 pub(super) fn submit_stop_order(
833 &mut self,
834 order_side: OrderSide,
835 trigger_price: Price,
836 limit_price: Option<Price>,
837 ) -> anyhow::Result<()> {
838 let Some(instrument) = &self.instrument else {
839 anyhow::bail!("No instrument loaded");
840 };
841
842 if self.config.dry_run {
843 log_warn!("Dry run, skipping create {order_side:?} stop order");
844 return Ok(());
845 }
846
847 if order_side == OrderSide::Buy && !self.config.enable_stop_buys {
848 log_warn!("BUY stop orders not enabled, skipping");
849 return Ok(());
850 } else if order_side == OrderSide::Sell && !self.config.enable_stop_sells {
851 log_warn!("SELL stop orders not enabled, skipping");
852 return Ok(());
853 }
854
855 let (time_in_force, expire_time) =
856 self.resolve_time_in_force(self.config.stop_time_in_force);
857
858 let quantity = instrument.make_qty(self.config.order_qty.as_f64(), None);
860
861 let factory = self.core.order_factory();
862
863 let mut order: OrderAny = match self.config.stop_order_type {
864 OrderType::StopMarket => factory.stop_market(
865 self.config.instrument_id,
866 order_side,
867 quantity,
868 trigger_price,
869 Some(self.config.stop_trigger_type),
870 Some(time_in_force),
871 expire_time,
872 None, Some(self.config.use_quote_quantity),
874 None, self.config.emulation_trigger,
876 None, None, None, None, None, ),
882 OrderType::StopLimit => {
883 let Some(limit_price) = limit_price else {
884 anyhow::bail!("STOP_LIMIT order requires limit_price");
885 };
886 factory.stop_limit(
887 self.config.instrument_id,
888 order_side,
889 quantity,
890 limit_price,
891 trigger_price,
892 Some(self.config.stop_trigger_type),
893 Some(time_in_force),
894 expire_time,
895 None, None, Some(self.config.use_quote_quantity),
898 self.config.order_display_qty,
899 self.config.emulation_trigger,
900 None, None, None, None, None, )
906 }
907 OrderType::MarketIfTouched => factory.market_if_touched(
908 self.config.instrument_id,
909 order_side,
910 quantity,
911 trigger_price,
912 Some(self.config.stop_trigger_type),
913 Some(time_in_force),
914 expire_time,
915 None, Some(self.config.use_quote_quantity),
917 self.config.emulation_trigger,
918 None, None, None, None, None, ),
924 OrderType::LimitIfTouched => {
925 let Some(limit_price) = limit_price else {
926 anyhow::bail!("LIMIT_IF_TOUCHED order requires limit_price");
927 };
928 factory.limit_if_touched(
929 self.config.instrument_id,
930 order_side,
931 quantity,
932 limit_price,
933 trigger_price,
934 Some(self.config.stop_trigger_type),
935 Some(time_in_force),
936 expire_time,
937 None, None, Some(self.config.use_quote_quantity),
940 self.config.order_display_qty,
941 self.config.emulation_trigger,
942 None, None, None, None, None, )
948 }
949 OrderType::TrailingStopMarket => {
950 let Some(trailing_offset) = self.config.trailing_offset else {
951 anyhow::bail!("TRAILING_STOP_MARKET order requires trailing_offset config");
952 };
953 factory.trailing_stop_market(
954 self.config.instrument_id,
955 order_side,
956 quantity,
957 trailing_offset,
958 Some(self.config.trailing_offset_type),
959 None,
960 Some(trigger_price),
961 Some(self.config.stop_trigger_type),
962 Some(time_in_force),
963 expire_time,
964 None, Some(self.config.use_quote_quantity),
966 None, self.config.emulation_trigger,
968 None, None, None, None, None, )
974 }
975 _ => {
976 anyhow::bail!("Unknown stop order type: {:?}", self.config.stop_order_type);
977 }
978 };
979
980 if let OrderAny::TrailingStopMarket(order) = &mut order {
981 order.activation_price = Some(trigger_price);
982 }
983
984 if order_side == OrderSide::Buy {
985 self.buy_stop_order = Some(order.clone());
986 } else {
987 self.sell_stop_order = Some(order.clone());
988 }
989
990 self.submit_order_apply_params(order)
991 }
992
993 pub(super) fn submit_bracket_order(
999 &mut self,
1000 order_side: OrderSide,
1001 entry_price: Price,
1002 ) -> anyhow::Result<()> {
1003 let Some(instrument) = &self.instrument else {
1004 anyhow::bail!("No instrument loaded");
1005 };
1006
1007 if self.config.dry_run {
1008 log_warn!("Dry run, skipping create {order_side:?} bracket order");
1009 return Ok(());
1010 }
1011
1012 if self.config.bracket_entry_order_type != OrderType::Limit {
1013 anyhow::bail!(
1014 "Only Limit entry orders are supported for brackets, was {:?}",
1015 self.config.bracket_entry_order_type
1016 );
1017 }
1018
1019 if order_side == OrderSide::Buy && !self.config.enable_limit_buys {
1020 log_warn!("BUY orders not enabled, skipping bracket");
1021 return Ok(());
1022 } else if order_side == OrderSide::Sell && !self.config.enable_limit_sells {
1023 log_warn!("SELL orders not enabled, skipping bracket");
1024 return Ok(());
1025 }
1026
1027 let (time_in_force, expire_time) =
1028 self.resolve_time_in_force(self.config.limit_time_in_force);
1029 let sl_time_in_force = self.config.stop_time_in_force.unwrap_or(TimeInForce::Gtc);
1030 if sl_time_in_force == TimeInForce::Gtd {
1031 anyhow::bail!("GTD time in force not supported for bracket stop-loss legs");
1032 }
1033
1034 let quantity = instrument.make_qty(self.config.order_qty.as_f64(), None);
1035 let price_increment = instrument.price_increment().as_f64();
1036 let bracket_offset = price_increment * self.config.bracket_offset_ticks as f64;
1037
1038 let (tp_price, sl_trigger_price) = match order_side {
1039 OrderSide::Buy => {
1040 let tp = instrument.make_price(entry_price.as_f64() + bracket_offset);
1041 let sl = instrument.make_price(entry_price.as_f64() - bracket_offset);
1042 (tp, sl)
1043 }
1044 OrderSide::Sell => {
1045 let tp = instrument.make_price(entry_price.as_f64() - bracket_offset);
1046 let sl = instrument.make_price(entry_price.as_f64() + bracket_offset);
1047 (tp, sl)
1048 }
1049 _ => anyhow::bail!("Invalid order side for bracket: {order_side:?}"),
1050 };
1051
1052 let orders = self.core.order_factory().bracket(
1053 self.config.instrument_id,
1054 order_side,
1055 quantity,
1056 Some(entry_price), sl_trigger_price, Some(self.config.stop_trigger_type), tp_price, None, Some(time_in_force),
1062 expire_time,
1063 Some(sl_time_in_force),
1064 Some(self.config.use_post_only || self.config.test_reject_post_only),
1065 None, Some(self.config.use_quote_quantity),
1067 self.config.emulation_trigger,
1068 None, None, None, None, );
1073
1074 if let Some(entry_order) = orders.first() {
1075 if order_side == OrderSide::Buy {
1076 self.buy_order = Some(entry_order.clone());
1077 } else {
1078 self.sell_order = Some(entry_order.clone());
1079 }
1080 }
1081
1082 let client_id = self.config.client_id;
1083 if let Some(params) = &self.config.order_params {
1084 self.submit_order_list_with_params(orders, None, client_id, params.clone())
1085 } else {
1086 self.submit_order_list(orders, None, client_id)
1087 }
1088 }
1089
1090 pub(super) fn open_position(&mut self, net_qty: Decimal) -> anyhow::Result<()> {
1096 let Some(instrument) = &self.instrument else {
1097 anyhow::bail!("No instrument loaded");
1098 };
1099
1100 if net_qty == Decimal::ZERO {
1101 log_warn!("Open position with zero quantity, skipping");
1102 return Ok(());
1103 }
1104
1105 let order_side = if net_qty > Decimal::ZERO {
1106 OrderSide::Buy
1107 } else {
1108 OrderSide::Sell
1109 };
1110
1111 let quantity = instrument.make_qty(net_qty.abs().to_f64().unwrap_or(0.0), None);
1112
1113 let reduce_only = if self.config.test_reject_reduce_only {
1115 Some(true)
1116 } else {
1117 None
1118 };
1119
1120 let order = self.core.order_factory().market(
1121 self.config.instrument_id,
1122 order_side,
1123 quantity,
1124 Some(self.config.open_position_time_in_force),
1125 reduce_only,
1126 Some(self.config.use_quote_quantity),
1127 None, None, None, None, );
1132
1133 self.submit_order_apply_params(order)
1134 }
1135}