1use std::num::NonZeroUsize;
17
18use ahash::AHashSet;
19use nautilus_common::{
20 actor::{DataActor, DataActorNative},
21 config::ConfigError,
22 enums::LogColor,
23 log_info, log_warn,
24};
25use nautilus_core::{DurationNanos, UnixNanos};
26use nautilus_model::{
27 data::{Bar, IndexPriceUpdate, MarkPriceUpdate, OrderBookDeltas, QuoteTick, TradeTick},
28 enums::{ContingencyType, OrderSide, OrderStatus, OrderType, TimeInForce},
29 events::{
30 OrderAccepted, OrderCancelRejected, OrderCanceled, OrderDenied, OrderEvent, OrderEventAny,
31 OrderExpired, OrderFilled, OrderModifyRejected, OrderRejected, OrderUpdated,
32 },
33 identifiers::{ClientId, ClientOrderId, InstrumentId, StrategyId},
34 instruments::{Instrument, InstrumentAny},
35 orderbook::OrderBook,
36 orders::{Order, OrderAny, OrderCore},
37 types::{Price, Quantity},
38};
39use nautilus_trading::{
40 nautilus_strategy,
41 strategy::{Strategy, StrategyCore, StrategyNative},
42};
43use rust_decimal::{Decimal, prelude::ToPrimitive};
44
45use super::config::ExecTesterConfig;
46use crate::testers::timestamps::warn_if_implausible_unix_nanos;
47
48#[derive(Debug)]
57#[expect(
58 clippy::struct_excessive_bools,
59 reason = "tester state tracks independent execution scenarios"
60)]
61pub struct ExecTester {
62 pub(super) core: StrategyCore,
63 pub(super) config: ExecTesterConfig,
64 pub(super) instrument: Option<InstrumentAny>,
65 pub(super) price_offset: Option<u64>,
66 pub(super) preinitialized_market_data: bool,
67
68 pub(super) buy_order: Option<OrderAny>,
70 pub(super) sell_order: Option<OrderAny>,
71 pub(super) buy_stop_order: Option<OrderAny>,
72 pub(super) sell_stop_order: Option<OrderAny>,
73 pub(super) open_position_submitted: bool,
74
75 pub(super) modify_rejected_attempted: bool,
78 pub(super) pending_open_position_qty: Option<Decimal>,
79 pub(super) buy_stop_cancel_replace_attempted: bool,
80 pub(super) sell_stop_cancel_replace_attempted: bool,
81 buy_stop_replacement: Option<StopOrderReplacement>,
82 sell_stop_replacement: Option<StopOrderReplacement>,
83 pub(super) buy_limit_maintenance_state: LimitOrderMaintenanceState,
84 pub(super) sell_limit_maintenance_state: LimitOrderMaintenanceState,
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub(super) enum LimitOrderMaintenanceState {
89 Disabled,
90 Ready,
91 AwaitingAcceptance {
92 client_order_id: ClientOrderId,
93 },
94 Active {
95 client_order_id: ClientOrderId,
96 },
97 AwaitingUpdate {
98 client_order_id: ClientOrderId,
99 price: Price,
100 },
101 AwaitingCancel {
102 client_order_id: ClientOrderId,
103 replacement_price: Price,
104 },
105 AwaitingReplacementAcceptance {
106 client_order_id: ClientOrderId,
107 price: Price,
108 },
109 Completed,
110 Failed,
111}
112
113nautilus_strategy!(ExecTester, {
114 fn external_order_instrument_ids(&self) -> Option<Vec<InstrumentId>> {
115 self.core.config.external_order_instrument_ids.clone()
116 }
117
118 fn on_order_accepted(&mut self, event: OrderAccepted) {
119 self.handle_limit_order_accepted(event.client_order_id);
120 }
121
122 fn on_order_updated(&mut self, event: OrderUpdated) {
123 self.handle_limit_order_updated(event.client_order_id, event.price);
124 }
125
126 fn on_order_canceled(&mut self, event: &OrderCanceled) {
127 self.handle_limit_order_canceled(event.client_order_id);
128 self.handle_stop_order_canceled(event);
129 }
130
131 fn on_order_modify_rejected(&mut self, event: OrderModifyRejected) {
132 self.finish_limit_order_maintenance(event.client_order_id);
133 }
134
135 fn on_order_cancel_rejected(&mut self, event: OrderCancelRejected) {
136 self.finish_limit_order_maintenance(event.client_order_id);
137 self.clear_stop_order_replacement(event.client_order_id);
138 }
139
140 fn on_order_denied(&mut self, event: OrderDenied) {
141 self.handle_limit_order_closed(event.client_order_id);
142 self.clear_stop_order_replacement(event.client_order_id);
143 }
144
145 fn on_order_rejected(&mut self, event: OrderRejected) {
146 self.handle_limit_order_closed(event.client_order_id);
147 self.clear_stop_order_replacement(event.client_order_id);
148 }
149
150 fn on_order_expired(&mut self, event: OrderExpired) {
151 self.handle_limit_order_closed(event.client_order_id);
152 self.clear_stop_order_replacement(event.client_order_id);
153 }
154
155 fn on_order_filled(&mut self, event: &OrderFilled) {
156 let (is_closed, is_filled) = self
157 .cache()
158 .order(&event.client_order_id)
159 .map_or((true, false), |order| {
160 (order.is_closed(), order.status() == OrderStatus::Filled)
161 });
162
163 if is_closed {
164 self.handle_limit_order_closed(event.client_order_id);
165 }
166
167 if is_filled {
168 self.clear_stop_order_replacement(event.client_order_id);
169 }
170 }
171
172 fn on_order_event(&mut self, event: OrderEventAny) {
173 let event = event.into_boxed();
174 warn_if_implausible_unix_nanos(
175 "order event",
176 OrderEvent::ts_event(&*event),
177 OrderEvent::ts_init(&*event),
178 );
179 }
180});
181
182impl DataActor for ExecTester {
183 fn on_start(&mut self) -> anyhow::Result<()> {
184 Strategy::on_start(self)?;
185
186 let instrument_id = self.config.instrument_id;
187 let client_id = self.config.client_id;
188
189 let instrument = self.cache().instrument(&instrument_id);
190
191 if let Some(inst) = instrument {
192 self.initialize_with_instrument(inst, true)?;
193 } else {
194 log::info!("Instrument {instrument_id} not in cache, subscribing...");
195 self.subscribe_instrument(instrument_id, client_id, None);
196
197 if self.config.subscribe_quotes {
200 self.subscribe_quotes(instrument_id, client_id, None);
201 }
202
203 if self.config.subscribe_trades {
204 self.subscribe_trades(instrument_id, client_id, None);
205 }
206 self.preinitialized_market_data =
207 self.config.subscribe_quotes || self.config.subscribe_trades;
208 }
209
210 Ok(())
211 }
212
213 fn on_instrument(&mut self, instrument: &InstrumentAny) -> anyhow::Result<()> {
214 warn_if_implausible_unix_nanos("instrument", instrument.ts_event(), instrument.ts_init());
215
216 if instrument.id() == self.config.instrument_id && self.instrument.is_none() {
217 let id = instrument.id();
218 log::info!("Received instrument {id}, initializing...");
219 self.initialize_with_instrument(instrument.clone(), !self.preinitialized_market_data)?;
220 }
221 Ok(())
222 }
223
224 fn on_stop(&mut self) -> anyhow::Result<()> {
225 self.buy_stop_replacement = None;
226 self.sell_stop_replacement = None;
227
228 if self.config.dry_run {
229 log_warn!("Dry run mode, skipping cancel all orders and close all positions");
230 return Ok(());
231 }
232
233 let instrument_id = self.config.instrument_id;
234 let client_id = self.config.client_id;
235 let strategy_id = StrategyId::from(
236 DataActorNative::core(&self.core)
237 .actor_id()
238 .inner()
239 .as_str(),
240 );
241
242 if self.config.cancel_orders_on_stop {
243 self.cancel_active_orders(instrument_id, strategy_id, client_id);
244 }
245
246 if self.config.close_positions_on_stop {
247 let time_in_force = self
248 .config
249 .close_positions_time_in_force
250 .or(Some(TimeInForce::Gtc));
251
252 if let Err(e) = self.close_positions_on_stop(strategy_id, time_in_force) {
253 log::error!("Failed to close all positions: {e}");
254 }
255 }
256
257 if self.config.can_unsubscribe && self.instrument.is_some() {
258 if self.config.subscribe_quotes {
259 self.unsubscribe_quotes(instrument_id, client_id, None);
260 }
261
262 if self.config.subscribe_trades {
263 self.unsubscribe_trades(instrument_id, client_id, None);
264 }
265
266 if self.config.subscribe_book {
267 self.unsubscribe_book_at_interval(
268 instrument_id,
269 NonZeroUsize::new(self.config.book_interval_ms).ok_or_else(|| {
270 ConfigError::range("book_interval_ms", "must be positive, was 0")
271 })?,
272 client_id,
273 None,
274 );
275 }
276 }
277
278 Ok(())
279 }
280
281 fn on_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
282 warn_if_implausible_unix_nanos("quote", quote.ts_event, quote.ts_init);
283
284 if self.config.log_data {
285 log_info!("{quote:?}", color = LogColor::Cyan);
286 }
287
288 if quote.instrument_id == self.config.instrument_id
289 && self.config.open_position_on_first_quote
290 {
291 self.submit_pending_open_position();
292 }
293
294 self.maintain_orders(quote.bid_price, quote.ask_price);
295 Ok(())
296 }
297
298 fn on_trade(&mut self, trade: &TradeTick) -> anyhow::Result<()> {
299 warn_if_implausible_unix_nanos("trade", trade.ts_event, trade.ts_init);
300
301 if self.config.log_data {
302 log_info!("{trade:?}", color = LogColor::Cyan);
303 }
304 Ok(())
305 }
306
307 fn on_book(&mut self, book: &OrderBook) -> anyhow::Result<()> {
308 if self.config.log_data {
309 let num_levels = self.config.book_levels_to_print;
310 let instrument_id = book.instrument_id;
311 let book_str = book.pprint(num_levels, None);
312 log_info!("\n{instrument_id}\n{book_str}", color = LogColor::Cyan);
313
314 if DataActorNative::core(&self.core).is_registered() {
316 let cache = self.cache();
317 if let Some(own_book) = cache.own_order_book(&instrument_id) {
318 let own_book_str = own_book.pprint(num_levels, None);
319 log_info!(
320 "\n{instrument_id} (own)\n{own_book_str}",
321 color = LogColor::Magenta
322 );
323 }
324 }
325 }
326
327 let Some(best_bid) = book.best_bid_price() else {
328 return Ok(()); };
330 let Some(best_ask) = book.best_ask_price() else {
331 return Ok(()); };
333
334 self.maintain_orders(best_bid, best_ask);
335 Ok(())
336 }
337
338 fn on_book_deltas(&mut self, deltas: &OrderBookDeltas) -> anyhow::Result<()> {
339 warn_if_implausible_unix_nanos("book deltas", deltas.ts_event, deltas.ts_init);
340
341 if self.config.log_data {
342 log_info!("{deltas:?}", color = LogColor::Cyan);
343 }
344 Ok(())
345 }
346
347 fn on_bar(&mut self, bar: &Bar) -> anyhow::Result<()> {
348 warn_if_implausible_unix_nanos("bar", bar.ts_event, bar.ts_init);
349
350 if self.config.log_data {
351 log_info!("{bar:?}", color = LogColor::Cyan);
352 }
353 Ok(())
354 }
355
356 fn on_mark_price(&mut self, mark_price: &MarkPriceUpdate) -> anyhow::Result<()> {
357 warn_if_implausible_unix_nanos("mark price", mark_price.ts_event, mark_price.ts_init);
358
359 if self.config.log_data {
360 log_info!("{mark_price:?}", color = LogColor::Cyan);
361 }
362 Ok(())
363 }
364
365 fn on_index_price(&mut self, index_price: &IndexPriceUpdate) -> anyhow::Result<()> {
366 warn_if_implausible_unix_nanos("index price", index_price.ts_event, index_price.ts_init);
367
368 if self.config.log_data {
369 log_info!("{index_price:?}", color = LogColor::Cyan);
370 }
371 Ok(())
372 }
373}
374
375impl ExecTester {
376 #[must_use]
378 pub fn new(config: ExecTesterConfig) -> Self {
379 let pending_open_position_qty = config.open_position_on_start_qty;
380 let buy_limit_maintenance_state = LimitOrderMaintenanceState::new(
381 config.enable_limit_buys
382 && (config.trigger_limit_order_maintenance_once
383 || config.cancel_replace_orders_to_maintain_tob_offset),
384 );
385 let sell_limit_maintenance_state = LimitOrderMaintenanceState::new(
386 config.enable_limit_sells
387 && (config.trigger_limit_order_maintenance_once
388 || config.cancel_replace_orders_to_maintain_tob_offset),
389 );
390
391 Self {
392 core: StrategyCore::new(config.base.clone()),
393 config,
394 instrument: None,
395 price_offset: None,
396 preinitialized_market_data: false,
397 buy_order: None,
398 sell_order: None,
399 buy_stop_order: None,
400 sell_stop_order: None,
401 open_position_submitted: false,
402 modify_rejected_attempted: false,
403 pending_open_position_qty,
404 buy_stop_cancel_replace_attempted: false,
405 sell_stop_cancel_replace_attempted: false,
406 buy_stop_replacement: None,
407 sell_stop_replacement: None,
408 buy_limit_maintenance_state,
409 sell_limit_maintenance_state,
410 }
411 }
412
413 fn initialize_with_instrument(
414 &mut self,
415 instrument: InstrumentAny,
416 subscribe_market_data: bool,
417 ) -> anyhow::Result<()> {
418 let instrument_id = self.config.instrument_id;
419 let client_id = self.config.client_id;
420
421 self.price_offset = Some(self.get_price_offset(&instrument));
422 self.instrument = Some(instrument);
423
424 if subscribe_market_data && self.config.subscribe_quotes {
425 self.subscribe_quotes(instrument_id, client_id, None);
426 }
427
428 if subscribe_market_data && self.config.subscribe_trades {
429 self.subscribe_trades(instrument_id, client_id, None);
430 }
431
432 if self.config.subscribe_book {
433 self.subscribe_book_at_interval(
434 instrument_id,
435 self.config.book_type,
436 self.config
437 .book_depth
438 .map(|depth| {
439 NonZeroUsize::new(depth).ok_or_else(|| {
440 ConfigError::range("book_depth", "must be positive, was 0")
441 })
442 })
443 .transpose()?,
444 NonZeroUsize::new(self.config.book_interval_ms).ok_or_else(|| {
445 ConfigError::range("book_interval_ms", "must be positive, was 0")
446 })?,
447 client_id,
448 None,
449 );
450 }
451
452 if let Some(qty) = self.pending_open_position_qty {
453 let quote_ready = {
454 let cache = self.cache();
455 cache.quote(&instrument_id).is_some()
456 };
457
458 if self.config.open_position_on_first_quote
459 && self.config.subscribe_quotes
460 && !quote_ready
461 {
462 log::info!("Waiting for first quote before opening {instrument_id} position");
463 } else {
464 self.pending_open_position_qty = None;
465 self.open_position(qty)?;
466 self.open_position_submitted = true;
467 }
468 }
469
470 Ok(())
471 }
472
473 pub(super) fn get_price_offset(&self, _instrument: &InstrumentAny) -> u64 {
474 self.config.tob_offset_ticks
475 }
476
477 fn expire_time_from_delta(&self, mins: u64) -> UnixNanos {
478 let current_ns = DataActorNative::core(&self.core).timestamp_ns();
479 let delta = DurationNanos::try_from_mins(mins).unwrap_or(DurationNanos::MAX);
480 current_ns.saturating_add(delta)
481 }
482
483 fn resolve_time_in_force(
484 &self,
485 tif_override: Option<TimeInForce>,
486 ) -> (TimeInForce, Option<UnixNanos>) {
487 match (tif_override, self.config.order_expire_time_delta_mins) {
488 (Some(TimeInForce::Gtd), Some(mins)) => {
489 (TimeInForce::Gtd, Some(self.expire_time_from_delta(mins)))
490 }
491 (Some(TimeInForce::Gtd), None) => {
492 log_warn!(
493 "GTD time in force requires order_expire_time_delta_mins, falling back to GTC"
494 );
495 (TimeInForce::Gtc, None)
496 }
497 (Some(tif), _) => (tif, None),
498 (None, Some(mins)) => (TimeInForce::Gtd, Some(self.expire_time_from_delta(mins))),
499 (None, None) => (TimeInForce::Gtc, None),
500 }
501 }
502
503 fn submit_pending_open_position(&mut self) {
504 if self.instrument.is_none() {
505 return;
506 }
507
508 let Some(qty) = self.pending_open_position_qty.take() else {
509 return;
510 };
511
512 if let Err(e) = self.open_position(qty) {
513 log::error!("Failed to submit pending open position: {e}");
514 } else {
515 self.open_position_submitted = true;
516 }
517 }
518
519 pub(super) fn is_order_active(order: &OrderAny) -> bool {
520 order.is_active_local() || order.is_inflight() || order.is_open()
521 }
522
523 pub(super) fn limit_order_is_one_shot(&self) -> bool {
524 self.config.test_reject_post_only
525 || self.config.limit_aggressive
526 || self.config.order_expire_time_delta_mins.is_some()
527 || matches!(
528 self.config.limit_time_in_force,
529 Some(TimeInForce::Ioc | TimeInForce::Fok)
530 )
531 }
532
533 pub(super) fn stop_order_is_one_shot(&self) -> bool {
534 self.config.order_expire_time_delta_mins.is_some()
535 || matches!(
536 self.config.stop_time_in_force,
537 Some(TimeInForce::Ioc | TimeInForce::Fok)
538 )
539 || matches!(self.config.stop_order_type, OrderType::TrailingStopMarket)
540 }
541
542 pub(super) fn get_order_trigger_price(order: &OrderAny) -> Option<Price> {
543 order.trigger_price()
544 }
545
546 fn modify_stop_order(
547 &mut self,
548 order: &OrderAny,
549 trigger_price: Price,
550 limit_price: Option<Price>,
551 ) -> anyhow::Result<()> {
552 let client_id = self.config.client_id;
553
554 match order {
555 OrderAny::StopMarket(_)
556 | OrderAny::MarketIfTouched(_)
557 | OrderAny::TrailingStopMarket(_) => self.modify_order(
558 order.client_order_id(),
559 None,
560 None,
561 Some(trigger_price),
562 client_id,
563 None,
564 ),
565 OrderAny::StopLimit(_) | OrderAny::LimitIfTouched(_) => self.modify_order(
566 order.client_order_id(),
567 None,
568 limit_price,
569 Some(trigger_price),
570 client_id,
571 None,
572 ),
573 _ => {
574 log_warn!("Cannot modify order of type {:?}", order.order_type());
575 Ok(())
576 }
577 }
578 }
579
580 fn submit_order_apply_params(&mut self, order: OrderAny) -> anyhow::Result<()> {
582 let client_id = self.config.client_id;
583 if let Some(params) = &self.config.order_params {
584 self.submit_order(order, None, client_id, Some(params.clone()))
585 } else {
586 self.submit_order(order, None, client_id, None)
587 }
588 }
589
590 pub(super) fn maintain_orders(&mut self, best_bid: Price, best_ask: Price) {
592 if self.instrument.is_none() || self.config.dry_run {
593 return;
594 }
595
596 if self.config.batch_submit_limit_pair
597 && self.config.enable_limit_buys
598 && self.config.enable_limit_sells
599 {
600 self.maintain_batch_limit_pair(best_bid, best_ask);
601 return;
602 }
603
604 if self.config.enable_limit_buys {
605 self.maintain_buy_orders(best_bid, best_ask);
606 }
607
608 if self.config.enable_limit_sells {
609 self.maintain_sell_orders(best_bid, best_ask);
610 }
611
612 if self.config.enable_stop_buys {
613 self.maintain_stop_buy_orders(best_bid, best_ask);
614 }
615
616 if self.config.enable_stop_sells {
617 self.maintain_stop_sell_orders(best_bid, best_ask);
618 }
619 }
620
621 fn refresh_tracked_order(&mut self, side: OrderSide) {
625 let cid = match side {
626 OrderSide::Buy => self.buy_order.as_ref().map(OrderAny::client_order_id),
627 OrderSide::Sell => self.sell_order.as_ref().map(OrderAny::client_order_id),
628 };
629 let Some(cid) = cid else {
630 return;
631 };
632 let latest = self.cache().order(&cid);
633 if let Some(latest) = latest {
634 match side {
635 OrderSide::Buy => self.buy_order = Some(latest),
636 OrderSide::Sell => self.sell_order = Some(latest),
637 }
638 }
639 }
640
641 fn refresh_tracked_stop_order(&mut self, side: OrderSide) {
642 let cid = match side {
643 OrderSide::Buy => self.buy_stop_order.as_ref().map(OrderAny::client_order_id),
644 OrderSide::Sell => self.sell_stop_order.as_ref().map(OrderAny::client_order_id),
645 };
646 let Some(cid) = cid else {
647 return;
648 };
649 let latest = self.cache().order(&cid);
650 if let Some(latest) = latest {
651 match side {
652 OrderSide::Buy => self.buy_stop_order = Some(latest),
653 OrderSide::Sell => self.sell_stop_order = Some(latest),
654 }
655 }
656 }
657
658 fn maintain_buy_orders(&mut self, best_bid: Price, best_ask: Price) {
660 self.refresh_tracked_order(OrderSide::Buy);
664
665 let Some(instrument) = &self.instrument else {
666 return;
667 };
668 let Some(price_offset_ticks) = self.price_offset else {
669 return;
670 };
671
672 let increment = instrument.price_increment();
673 let precision = instrument.price_precision();
674
675 let cross_spread = self.config.test_reject_post_only || self.config.limit_aggressive;
680 let unclamped_price = if cross_spread {
681 add_price_ticks(best_ask, increment, price_offset_ticks, precision)
682 } else {
683 sub_price_ticks(best_bid, increment, price_offset_ticks, precision)
684 };
685 let price = clamp_price_to_range(
686 unclamped_price,
687 instrument,
688 self.config.clamp_to_instrument_price_range,
689 );
690
691 if self.limit_order_maintenance_suppressed(OrderSide::Buy) {
692 return;
693 }
694
695 let needs_new_order = match &self.buy_order {
696 None => true,
697 Some(order) => !Self::is_order_active(order) && !self.limit_order_is_one_shot(),
698 };
699
700 if needs_new_order {
701 let result = if self.config.enable_brackets {
702 self.submit_bracket_order(OrderSide::Buy, price)
703 } else {
704 self.submit_limit_order(OrderSide::Buy, price)
705 };
706
707 if let Err(e) = result {
708 log::error!("Failed to submit buy order: {e}");
709 }
710 } else if let Some(order) = &self.buy_order
711 && order.venue_order_id().is_some()
712 && !order.is_pending_update()
713 && !order.is_pending_cancel()
714 {
715 let client_id = self.config.client_id;
716
717 if self.config.test_modify_rejected && !self.modify_rejected_attempted {
720 self.modify_rejected_attempted = true;
721 let order_clone = order.clone();
722 let bumped = clamp_price_to_range(
723 add_price_ticks(price, increment, 1, precision),
724 instrument,
725 self.config.clamp_to_instrument_price_range,
726 );
727
728 if let Err(e) = self.modify_order(
729 order_clone.client_order_id(),
730 None,
731 Some(bumped),
732 None,
733 client_id,
734 None,
735 ) {
736 log::error!("Failed to submit test modify on buy order: {e}");
737 }
738 return;
739 }
740
741 if let Some(order_price) = order.price()
742 && order_price < price
743 {
744 if self.config.modify_orders_to_maintain_tob_offset {
745 let order_clone = order.clone();
746 if let Err(e) = self.modify_order(
747 order_clone.client_order_id(),
748 None,
749 Some(price),
750 None,
751 client_id,
752 None,
753 ) {
754 log::error!("Failed to modify buy order: {e}");
755 }
756 } else if self.config.cancel_replace_orders_to_maintain_tob_offset {
757 let client_order_id = order.client_order_id();
758 self.start_limit_order_cancel_replace(OrderSide::Buy, client_order_id, price);
759 }
760 }
761 }
762 }
763
764 fn maintain_sell_orders(&mut self, best_bid: Price, best_ask: Price) {
766 self.refresh_tracked_order(OrderSide::Sell);
769
770 let Some(instrument) = &self.instrument else {
771 return;
772 };
773 let Some(price_offset_ticks) = self.price_offset else {
774 return;
775 };
776
777 let increment = instrument.price_increment();
778 let precision = instrument.price_precision();
779
780 let cross_spread = self.config.test_reject_post_only || self.config.limit_aggressive;
782 let unclamped_price = if cross_spread {
783 sub_price_ticks(best_bid, increment, price_offset_ticks, precision)
784 } else {
785 add_price_ticks(best_ask, increment, price_offset_ticks, precision)
786 };
787 let price = clamp_price_to_range(
788 unclamped_price,
789 instrument,
790 self.config.clamp_to_instrument_price_range,
791 );
792
793 if self.limit_order_maintenance_suppressed(OrderSide::Sell) {
794 return;
795 }
796
797 let needs_new_order = match &self.sell_order {
798 None => true,
799 Some(order) => !Self::is_order_active(order) && !self.limit_order_is_one_shot(),
800 };
801
802 if needs_new_order {
803 let result = if self.config.enable_brackets {
804 self.submit_bracket_order(OrderSide::Sell, price)
805 } else {
806 self.submit_limit_order(OrderSide::Sell, price)
807 };
808
809 if let Err(e) = result {
810 log::error!("Failed to submit sell order: {e}");
811 }
812 } else if let Some(order) = &self.sell_order
813 && order.venue_order_id().is_some()
814 && !order.is_pending_update()
815 && !order.is_pending_cancel()
816 {
817 let client_id = self.config.client_id;
818
819 if self.config.test_modify_rejected && !self.modify_rejected_attempted {
821 self.modify_rejected_attempted = true;
822 let order_clone = order.clone();
823 let bumped = clamp_price_to_range(
824 sub_price_ticks(price, increment, 1, precision),
825 instrument,
826 self.config.clamp_to_instrument_price_range,
827 );
828
829 if let Err(e) = self.modify_order(
830 order_clone.client_order_id(),
831 None,
832 Some(bumped),
833 None,
834 client_id,
835 None,
836 ) {
837 log::error!("Failed to submit test modify on sell order: {e}");
838 }
839 return;
840 }
841
842 if let Some(order_price) = order.price()
843 && order_price > price
844 {
845 if self.config.modify_orders_to_maintain_tob_offset {
846 let order_clone = order.clone();
847 if let Err(e) = self.modify_order(
848 order_clone.client_order_id(),
849 None,
850 Some(price),
851 None,
852 client_id,
853 None,
854 ) {
855 log::error!("Failed to modify sell order: {e}");
856 }
857 } else if self.config.cancel_replace_orders_to_maintain_tob_offset {
858 let client_order_id = order.client_order_id();
859 self.start_limit_order_cancel_replace(OrderSide::Sell, client_order_id, price);
860 }
861 }
862 }
863 }
864
865 fn handle_limit_order_accepted(&mut self, client_order_id: ClientOrderId) {
866 let Some(side) = self.limit_order_side_for_state(client_order_id) else {
867 return;
868 };
869
870 match self.limit_order_maintenance_state(side) {
871 LimitOrderMaintenanceState::AwaitingAcceptance { .. } => {
872 self.refresh_tracked_order(side);
873 if self.config.trigger_limit_order_maintenance_once {
874 self.trigger_limit_order_maintenance(side, client_order_id);
875 } else {
876 self.set_limit_order_maintenance_state(
877 side,
878 LimitOrderMaintenanceState::Active { client_order_id },
879 );
880 }
881 }
882 LimitOrderMaintenanceState::AwaitingReplacementAcceptance { price, .. } => {
883 self.refresh_tracked_order(side);
884 let accepted_at_expected_price = self
885 .tracked_limit_order(side)
886 .is_some_and(|order| order.price() == Some(price) && order.is_open());
887 if !accepted_at_expected_price {
888 log::error!(
889 "Replacement {side} order {client_order_id} was accepted with unexpected state"
890 );
891 self.set_limit_order_maintenance_state(
892 side,
893 LimitOrderMaintenanceState::Failed,
894 );
895 return;
896 }
897 self.set_limit_order_maintenance_state(side, LimitOrderMaintenanceState::Completed);
898 }
899 _ => {}
900 }
901 }
902
903 fn trigger_limit_order_maintenance(&mut self, side: OrderSide, client_order_id: ClientOrderId) {
904 let Some(order_price) = self.tracked_limit_order(side).and_then(Order::price) else {
905 log::error!("Accepted {side} limit order {client_order_id} has no price");
906 self.set_limit_order_maintenance_state(side, LimitOrderMaintenanceState::Failed);
907 return;
908 };
909 let Some(price) = self.limit_order_trigger_price(side, order_price) else {
910 log::error!(
911 "Cannot find a different valid price for accepted {side} limit order {client_order_id}"
912 );
913 self.set_limit_order_maintenance_state(side, LimitOrderMaintenanceState::Failed);
914 return;
915 };
916
917 if self.config.modify_orders_to_maintain_tob_offset {
918 self.set_limit_order_maintenance_state(
919 side,
920 LimitOrderMaintenanceState::AwaitingUpdate {
921 client_order_id,
922 price,
923 },
924 );
925
926 if let Err(e) = self.modify_order(
927 client_order_id,
928 None,
929 Some(price),
930 None,
931 self.config.client_id,
932 None,
933 ) {
934 self.set_limit_order_maintenance_state(side, LimitOrderMaintenanceState::Failed);
935 log::error!("Failed to submit one-shot {side} limit order modify: {e}");
936 }
937 } else if self.config.cancel_replace_orders_to_maintain_tob_offset {
938 self.start_limit_order_cancel_replace(side, client_order_id, price);
939 } else {
940 self.set_limit_order_maintenance_state(side, LimitOrderMaintenanceState::Failed);
941 log::error!("No limit order maintenance mode configured for one-shot trigger");
942 }
943 }
944
945 fn start_limit_order_cancel_replace(
946 &mut self,
947 side: OrderSide,
948 client_order_id: ClientOrderId,
949 replacement_price: Price,
950 ) {
951 let state = self.limit_order_maintenance_state(side);
952 if !matches!(
953 state,
954 LimitOrderMaintenanceState::AwaitingAcceptance {
955 client_order_id: expected,
956 } | LimitOrderMaintenanceState::Active {
957 client_order_id: expected,
958 } if expected == client_order_id
959 ) {
960 return;
961 }
962
963 self.set_limit_order_maintenance_state(
964 side,
965 LimitOrderMaintenanceState::AwaitingCancel {
966 client_order_id,
967 replacement_price,
968 },
969 );
970
971 if let Err(e) = self.cancel_order(client_order_id, self.config.client_id, None) {
972 self.set_limit_order_maintenance_state(side, LimitOrderMaintenanceState::Failed);
973 log::error!("Failed to cancel {side} limit order for replacement: {e}");
974 }
975 }
976
977 fn handle_limit_order_updated(
978 &mut self,
979 client_order_id: ClientOrderId,
980 updated_price: Option<Price>,
981 ) {
982 let Some(side) = self.limit_order_side_for_state(client_order_id) else {
983 return;
984 };
985 let LimitOrderMaintenanceState::AwaitingUpdate { price, .. } =
986 self.limit_order_maintenance_state(side)
987 else {
988 return;
989 };
990
991 self.refresh_tracked_order(side);
992 let updated_at_expected_price = updated_price == Some(price)
993 && self
994 .tracked_limit_order(side)
995 .is_some_and(|order| order.price() == Some(price) && order.is_open());
996 if !updated_at_expected_price {
997 log::error!(
998 "Updated {side} limit order {client_order_id} has unexpected price or state"
999 );
1000 self.set_limit_order_maintenance_state(side, LimitOrderMaintenanceState::Failed);
1001 return;
1002 }
1003 self.set_limit_order_maintenance_state(side, LimitOrderMaintenanceState::Completed);
1004 }
1005
1006 fn handle_limit_order_canceled(&mut self, client_order_id: ClientOrderId) {
1007 let Some(side) = self.limit_order_side_for_state(client_order_id) else {
1008 return;
1009 };
1010 let LimitOrderMaintenanceState::AwaitingCancel {
1011 replacement_price, ..
1012 } = self.limit_order_maintenance_state(side)
1013 else {
1014 self.handle_limit_order_closed(client_order_id);
1015 return;
1016 };
1017
1018 self.refresh_tracked_order(side);
1019 let Some(leaves_qty) = self
1020 .tracked_limit_order(side)
1021 .filter(|order| order.client_order_id() == client_order_id)
1022 .map(Order::leaves_qty)
1023 else {
1024 self.set_limit_order_maintenance_state(side, LimitOrderMaintenanceState::Failed);
1025 log::error!("Canceled {side} limit order {client_order_id} is not tracked");
1026 return;
1027 };
1028
1029 if leaves_qty.is_zero() {
1030 self.set_limit_order_maintenance_state(side, LimitOrderMaintenanceState::Failed);
1031 log::warn!(
1032 "Canceled {side} limit order {client_order_id} has no remaining quantity to replace"
1033 );
1034 return;
1035 }
1036
1037 if let Err(e) = self.submit_limit_order_replacement(side, replacement_price, leaves_qty) {
1038 self.set_limit_order_maintenance_state(side, LimitOrderMaintenanceState::Failed);
1039 log::error!("Failed to submit replacement {side} limit order: {e}");
1040 }
1041 }
1042
1043 fn finish_limit_order_maintenance(&mut self, client_order_id: ClientOrderId) {
1044 let Some(side) = self.limit_order_side_for_state(client_order_id) else {
1045 return;
1046 };
1047
1048 if matches!(
1049 self.limit_order_maintenance_state(side),
1050 LimitOrderMaintenanceState::AwaitingUpdate { .. }
1051 | LimitOrderMaintenanceState::AwaitingCancel { .. }
1052 | LimitOrderMaintenanceState::AwaitingReplacementAcceptance { .. }
1053 ) {
1054 self.set_limit_order_maintenance_state(side, LimitOrderMaintenanceState::Failed);
1055 }
1056 }
1057
1058 fn handle_limit_order_closed(&mut self, client_order_id: ClientOrderId) {
1059 let Some(side) = self.limit_order_side_for_state(client_order_id) else {
1060 return;
1061 };
1062 self.refresh_tracked_order(side);
1063 let state = match self.limit_order_maintenance_state(side) {
1064 LimitOrderMaintenanceState::AwaitingAcceptance { .. }
1065 | LimitOrderMaintenanceState::Active { .. } => LimitOrderMaintenanceState::Ready,
1066 LimitOrderMaintenanceState::AwaitingUpdate { .. }
1067 | LimitOrderMaintenanceState::AwaitingCancel { .. }
1068 | LimitOrderMaintenanceState::AwaitingReplacementAcceptance { .. } => {
1069 LimitOrderMaintenanceState::Failed
1070 }
1071 state => state,
1072 };
1073 self.set_limit_order_maintenance_state(side, state);
1074 }
1075
1076 fn limit_order_trigger_price(&self, side: OrderSide, order_price: Price) -> Option<Price> {
1077 let instrument = self.instrument.as_ref()?;
1078 let increment = instrument.price_increment();
1079 let precision = instrument.price_precision();
1080 let passive = match side {
1081 OrderSide::Buy => sub_price_ticks(order_price, increment, 1, precision),
1082 OrderSide::Sell => add_price_ticks(order_price, increment, 1, precision),
1083 };
1084 let passive = clamp_price_to_range(passive, instrument, true);
1085 if passive != order_price {
1086 return Some(passive);
1087 }
1088
1089 let aggressive = match side {
1090 OrderSide::Buy => add_price_ticks(order_price, increment, 1, precision),
1091 OrderSide::Sell => sub_price_ticks(order_price, increment, 1, precision),
1092 };
1093 let aggressive = clamp_price_to_range(aggressive, instrument, true);
1094 (aggressive != order_price).then_some(aggressive)
1095 }
1096
1097 fn limit_order_maintenance_suppressed(&self, side: OrderSide) -> bool {
1098 let state = self.limit_order_maintenance_state(side);
1099 matches!(
1100 state,
1101 LimitOrderMaintenanceState::AwaitingUpdate { .. }
1102 | LimitOrderMaintenanceState::AwaitingCancel { .. }
1103 | LimitOrderMaintenanceState::AwaitingReplacementAcceptance { .. }
1104 ) || (self.config.trigger_limit_order_maintenance_once
1105 && matches!(
1106 state,
1107 LimitOrderMaintenanceState::Completed | LimitOrderMaintenanceState::Failed
1108 ))
1109 }
1110
1111 fn limit_order_side_for_state(&self, client_order_id: ClientOrderId) -> Option<OrderSide> {
1112 [OrderSide::Buy, OrderSide::Sell].into_iter().find(|side| {
1113 self.limit_order_maintenance_state(*side).client_order_id() == Some(client_order_id)
1114 })
1115 }
1116
1117 const fn limit_order_maintenance_state(&self, side: OrderSide) -> LimitOrderMaintenanceState {
1118 match side {
1119 OrderSide::Buy => self.buy_limit_maintenance_state,
1120 OrderSide::Sell => self.sell_limit_maintenance_state,
1121 }
1122 }
1123
1124 fn set_limit_order_maintenance_state(
1125 &mut self,
1126 side: OrderSide,
1127 state: LimitOrderMaintenanceState,
1128 ) {
1129 match side {
1130 OrderSide::Buy => self.buy_limit_maintenance_state = state,
1131 OrderSide::Sell => self.sell_limit_maintenance_state = state,
1132 }
1133 }
1134
1135 fn tracked_limit_order(&self, side: OrderSide) -> Option<&OrderAny> {
1136 match side {
1137 OrderSide::Buy => self.buy_order.as_ref(),
1138 OrderSide::Sell => self.sell_order.as_ref(),
1139 }
1140 }
1141
1142 fn maintain_batch_limit_pair(&mut self, best_bid: Price, best_ask: Price) {
1144 self.refresh_tracked_order(OrderSide::Buy);
1148 self.refresh_tracked_order(OrderSide::Sell);
1149
1150 let Some(instrument) = &self.instrument else {
1151 return;
1152 };
1153 let Some(price_offset_ticks) = self.price_offset else {
1154 return;
1155 };
1156
1157 let buy_needs = match &self.buy_order {
1158 None => true,
1159 Some(order) => !Self::is_order_active(order) && !self.limit_order_is_one_shot(),
1160 };
1161 let sell_needs = match &self.sell_order {
1162 None => true,
1163 Some(order) => !Self::is_order_active(order) && !self.limit_order_is_one_shot(),
1164 };
1165
1166 if !buy_needs || !sell_needs {
1167 return;
1168 }
1169
1170 let increment = instrument.price_increment();
1171 let precision = instrument.price_precision();
1172
1173 let cross_spread = self.config.test_reject_post_only || self.config.limit_aggressive;
1177 let (unclamped_buy_price, unclamped_sell_price) = if cross_spread {
1178 (
1179 add_price_ticks(best_ask, increment, price_offset_ticks, precision),
1180 sub_price_ticks(best_bid, increment, price_offset_ticks, precision),
1181 )
1182 } else {
1183 (
1184 sub_price_ticks(best_bid, increment, price_offset_ticks, precision),
1185 add_price_ticks(best_ask, increment, price_offset_ticks, precision),
1186 )
1187 };
1188 let clamp = self.config.clamp_to_instrument_price_range;
1189 let buy_price = clamp_price_to_range(unclamped_buy_price, instrument, clamp);
1190 let sell_price = clamp_price_to_range(unclamped_sell_price, instrument, clamp);
1191 let quantity = instrument.make_qty(self.config.order_qty.as_f64(), None);
1192 let (time_in_force, expire_time) =
1193 self.resolve_time_in_force(self.config.limit_time_in_force);
1194 let instrument_id = self.config.instrument_id;
1195 let post_only = self.config.use_post_only || self.config.test_reject_post_only;
1196 let quote_quantity = self.config.use_quote_quantity;
1197 let display_qty = self.config.order_display_qty;
1198 let emulation_trigger = self.config.emulation_trigger;
1199
1200 let buy_order = self.order_factory().limit(
1201 instrument_id,
1202 OrderSide::Buy,
1203 quantity,
1204 buy_price,
1205 Some(time_in_force),
1206 expire_time,
1207 Some(post_only),
1208 None,
1209 Some(quote_quantity),
1210 display_qty,
1211 emulation_trigger,
1212 None,
1213 None,
1214 None,
1215 None,
1216 None,
1217 );
1218
1219 let sell_order = self.order_factory().limit(
1220 instrument_id,
1221 OrderSide::Sell,
1222 quantity,
1223 sell_price,
1224 Some(time_in_force),
1225 expire_time,
1226 Some(post_only),
1227 None,
1228 Some(quote_quantity),
1229 display_qty,
1230 emulation_trigger,
1231 None,
1232 None,
1233 None,
1234 None,
1235 None,
1236 );
1237
1238 self.buy_order = Some(buy_order.clone());
1239 self.sell_order = Some(sell_order.clone());
1240
1241 let client_id = self.config.client_id;
1242 if let Err(e) = self.submit_order_list(vec![buy_order, sell_order], None, client_id, None) {
1243 log::error!("Failed to submit batch limit pair: {e}");
1244 }
1245 }
1246
1247 fn maintain_stop_buy_orders(&mut self, best_bid: Price, best_ask: Price) {
1249 self.refresh_tracked_stop_order(OrderSide::Buy);
1250
1251 if let Some(order) = self.buy_stop_order.as_ref()
1253 && matches!(order.status(), OrderStatus::Rejected | OrderStatus::Denied)
1254 {
1255 return;
1256 }
1257
1258 let Some(instrument) = &self.instrument else {
1259 return;
1260 };
1261
1262 let increment = instrument.price_increment();
1263 let precision = instrument.price_precision();
1264 let stop_offset_ticks = self.config.stop_offset_ticks;
1265
1266 let unclamped_trigger_price = if matches!(
1268 self.config.stop_order_type,
1269 OrderType::LimitIfTouched | OrderType::MarketIfTouched | OrderType::TrailingStopMarket
1270 ) {
1271 sub_price_ticks(best_bid, increment, stop_offset_ticks, precision)
1273 } else {
1274 add_price_ticks(best_ask, increment, stop_offset_ticks, precision)
1276 };
1277 let clamp = self.config.clamp_to_instrument_price_range;
1278 let trigger_price = clamp_price_to_range(unclamped_trigger_price, instrument, clamp);
1279
1280 let limit_price = if matches!(
1282 self.config.stop_order_type,
1283 OrderType::StopLimit | OrderType::LimitIfTouched
1284 ) {
1285 let unclamped_limit_price =
1286 if let Some(limit_offset_ticks) = self.config.stop_limit_offset_ticks {
1287 add_price_ticks(trigger_price, increment, limit_offset_ticks, precision)
1289 } else {
1290 trigger_price
1291 };
1292 Some(clamp_price_to_range(
1293 unclamped_limit_price,
1294 instrument,
1295 clamp,
1296 ))
1297 } else {
1298 None
1299 };
1300
1301 if let Some(replacement) = &mut self.buy_stop_replacement {
1302 replacement.trigger_price = trigger_price;
1303 replacement.limit_price = limit_price;
1304 return;
1305 }
1306
1307 let needs_new_order = match &self.buy_stop_order {
1308 None => true,
1309 Some(order) => !Self::is_order_active(order) && !self.stop_order_is_one_shot(),
1310 };
1311
1312 if needs_new_order {
1313 if let Err(e) = self.submit_stop_order(OrderSide::Buy, trigger_price, limit_price) {
1314 log::error!("Failed to submit buy stop order: {e}");
1315 }
1316 } else if let Some(order) = &self.buy_stop_order
1317 && order.venue_order_id().is_some()
1318 && !order.is_pending_update()
1319 && !order.is_pending_cancel()
1320 {
1321 let current_trigger = Self::get_order_trigger_price(order);
1322 if current_trigger.is_some() && current_trigger != Some(trigger_price) {
1323 if self.config.modify_stop_orders_to_maintain_offset {
1324 let order_clone = order.clone();
1325 if let Err(e) = self.modify_stop_order(&order_clone, trigger_price, limit_price)
1326 {
1327 log::error!("Failed to modify buy stop order: {e}");
1328 }
1329 } else if self.config.cancel_replace_stop_orders_to_maintain_offset
1330 && !self.buy_stop_cancel_replace_attempted
1331 {
1332 let client_order_id = order.client_order_id();
1333 self.start_stop_order_cancel_replace(
1334 OrderSide::Buy,
1335 client_order_id,
1336 trigger_price,
1337 limit_price,
1338 );
1339 }
1340 }
1341 }
1342 }
1343
1344 fn maintain_stop_sell_orders(&mut self, best_bid: Price, best_ask: Price) {
1346 self.refresh_tracked_stop_order(OrderSide::Sell);
1347
1348 if let Some(order) = self.sell_stop_order.as_ref()
1350 && matches!(order.status(), OrderStatus::Rejected | OrderStatus::Denied)
1351 {
1352 return;
1353 }
1354
1355 let Some(instrument) = &self.instrument else {
1356 return;
1357 };
1358
1359 let increment = instrument.price_increment();
1360 let precision = instrument.price_precision();
1361 let stop_offset_ticks = self.config.stop_offset_ticks;
1362
1363 let unclamped_trigger_price = if matches!(
1365 self.config.stop_order_type,
1366 OrderType::LimitIfTouched | OrderType::MarketIfTouched | OrderType::TrailingStopMarket
1367 ) {
1368 add_price_ticks(best_ask, increment, stop_offset_ticks, precision)
1370 } else {
1371 sub_price_ticks(best_bid, increment, stop_offset_ticks, precision)
1373 };
1374 let clamp = self.config.clamp_to_instrument_price_range;
1375 let trigger_price = clamp_price_to_range(unclamped_trigger_price, instrument, clamp);
1376
1377 let limit_price = if matches!(
1379 self.config.stop_order_type,
1380 OrderType::StopLimit | OrderType::LimitIfTouched
1381 ) {
1382 let unclamped_limit_price =
1383 if let Some(limit_offset_ticks) = self.config.stop_limit_offset_ticks {
1384 sub_price_ticks(trigger_price, increment, limit_offset_ticks, precision)
1386 } else {
1387 trigger_price
1388 };
1389 Some(clamp_price_to_range(
1390 unclamped_limit_price,
1391 instrument,
1392 clamp,
1393 ))
1394 } else {
1395 None
1396 };
1397
1398 if let Some(replacement) = &mut self.sell_stop_replacement {
1399 replacement.trigger_price = trigger_price;
1400 replacement.limit_price = limit_price;
1401 return;
1402 }
1403
1404 let needs_new_order = match &self.sell_stop_order {
1405 None => true,
1406 Some(order) => !Self::is_order_active(order) && !self.stop_order_is_one_shot(),
1407 };
1408
1409 if needs_new_order {
1410 if let Err(e) = self.submit_stop_order(OrderSide::Sell, trigger_price, limit_price) {
1411 log::error!("Failed to submit sell stop order: {e}");
1412 }
1413 } else if let Some(order) = &self.sell_stop_order
1414 && order.venue_order_id().is_some()
1415 && !order.is_pending_update()
1416 && !order.is_pending_cancel()
1417 {
1418 let current_trigger = Self::get_order_trigger_price(order);
1419 if current_trigger.is_some() && current_trigger != Some(trigger_price) {
1420 if self.config.modify_stop_orders_to_maintain_offset {
1421 let order_clone = order.clone();
1422 if let Err(e) = self.modify_stop_order(&order_clone, trigger_price, limit_price)
1423 {
1424 log::error!("Failed to modify sell stop order: {e}");
1425 }
1426 } else if self.config.cancel_replace_stop_orders_to_maintain_offset
1427 && !self.sell_stop_cancel_replace_attempted
1428 {
1429 let client_order_id = order.client_order_id();
1430 self.start_stop_order_cancel_replace(
1431 OrderSide::Sell,
1432 client_order_id,
1433 trigger_price,
1434 limit_price,
1435 );
1436 }
1437 }
1438 }
1439 }
1440
1441 fn start_stop_order_cancel_replace(
1442 &mut self,
1443 side: OrderSide,
1444 client_order_id: ClientOrderId,
1445 trigger_price: Price,
1446 limit_price: Option<Price>,
1447 ) {
1448 let replacement = Some(StopOrderReplacement {
1449 client_order_id,
1450 trigger_price,
1451 limit_price,
1452 });
1453
1454 match side {
1455 OrderSide::Buy => {
1456 self.buy_stop_cancel_replace_attempted = true;
1457 self.buy_stop_replacement = replacement;
1458 }
1459 OrderSide::Sell => {
1460 self.sell_stop_cancel_replace_attempted = true;
1461 self.sell_stop_replacement = replacement;
1462 }
1463 }
1464
1465 if let Err(e) = self.cancel_order(client_order_id, self.config.client_id, None) {
1466 self.clear_stop_order_replacement(client_order_id);
1467 log::error!("Failed to cancel {side} stop order for replacement: {e}");
1468 }
1469 }
1470
1471 fn handle_stop_order_canceled(&mut self, event: &OrderCanceled) {
1472 let client_order_id = event.client_order_id;
1473
1474 let replacement = [
1475 (OrderSide::Buy, &mut self.buy_stop_replacement),
1476 (OrderSide::Sell, &mut self.sell_stop_replacement),
1477 ]
1478 .into_iter()
1479 .find_map(|(side, pending)| {
1480 if pending
1481 .as_ref()
1482 .is_some_and(|replacement| replacement.client_order_id == client_order_id)
1483 {
1484 if event.reconciliation {
1485 log::error!(
1486 "Cannot complete {side} stop cancel-replace from a reconciliation event for {client_order_id}, replacement remains pending"
1487 );
1488 return None;
1489 }
1490
1491 pending.take().map(|replacement| (side, replacement))
1492 } else {
1493 None
1494 }
1495 });
1496
1497 let Some((side, replacement)) = replacement else {
1498 return;
1499 };
1500
1501 if let Err(e) =
1502 self.submit_stop_order(side, replacement.trigger_price, replacement.limit_price)
1503 {
1504 log::error!("Failed to submit replacement {side} stop order: {e}");
1505 }
1506 }
1507
1508 fn clear_stop_order_replacement(&mut self, client_order_id: ClientOrderId) {
1509 for pending in [
1510 &mut self.buy_stop_replacement,
1511 &mut self.sell_stop_replacement,
1512 ] {
1513 if pending
1514 .as_ref()
1515 .is_some_and(|replacement| replacement.client_order_id == client_order_id)
1516 {
1517 *pending = None;
1518 }
1519 }
1520 }
1521
1522 pub(super) fn submit_limit_order(
1528 &mut self,
1529 order_side: OrderSide,
1530 price: Price,
1531 ) -> anyhow::Result<()> {
1532 let Some(instrument) = self.instrument.as_ref() else {
1533 anyhow::bail!("No instrument loaded");
1534 };
1535 let quantity = instrument.make_qty(self.config.order_qty.as_f64(), None);
1536 let Some(order) = self.create_tracked_limit_order(order_side, price, quantity)? else {
1537 return Ok(());
1538 };
1539 let client_order_id = order.client_order_id();
1540 self.arm_limit_order_maintenance(order_side, client_order_id);
1541
1542 if let Err(e) = self.submit_order_apply_params(order) {
1543 self.handle_limit_order_closed(client_order_id);
1544 return Err(e);
1545 }
1546 Ok(())
1547 }
1548
1549 fn submit_limit_order_replacement(
1550 &mut self,
1551 order_side: OrderSide,
1552 price: Price,
1553 quantity: Quantity,
1554 ) -> anyhow::Result<()> {
1555 let Some(order) = self.create_tracked_limit_order(order_side, price, quantity)? else {
1556 anyhow::bail!("Replacement {order_side} limit order was not created");
1557 };
1558 let client_order_id = order.client_order_id();
1559 self.set_limit_order_maintenance_state(
1560 order_side,
1561 LimitOrderMaintenanceState::AwaitingReplacementAcceptance {
1562 client_order_id,
1563 price,
1564 },
1565 );
1566 self.submit_order_apply_params(order)
1567 }
1568
1569 fn create_tracked_limit_order(
1570 &mut self,
1571 order_side: OrderSide,
1572 price: Price,
1573 quantity: Quantity,
1574 ) -> anyhow::Result<Option<OrderAny>> {
1575 if self.instrument.is_none() {
1576 anyhow::bail!("No instrument loaded");
1577 }
1578
1579 if self.config.dry_run {
1580 log_warn!("Dry run, skipping create {order_side:?} order");
1581 return Ok(None);
1582 }
1583
1584 if order_side == OrderSide::Buy && !self.config.enable_limit_buys {
1585 log_warn!("BUY orders not enabled, skipping");
1586 return Ok(None);
1587 } else if order_side == OrderSide::Sell && !self.config.enable_limit_sells {
1588 log_warn!("SELL orders not enabled, skipping");
1589 return Ok(None);
1590 }
1591
1592 let (time_in_force, expire_time) =
1593 self.resolve_time_in_force(self.config.limit_time_in_force);
1594
1595 let instrument_id = self.config.instrument_id;
1596 let post_only = self.config.use_post_only || self.config.test_reject_post_only;
1597 let quote_quantity = self.config.use_quote_quantity;
1598 let display_qty = self
1599 .config
1600 .order_display_qty
1601 .map(|display_qty| display_qty.min(quantity));
1602 let emulation_trigger = self.config.emulation_trigger;
1603
1604 let order = self.order_factory().limit(
1605 instrument_id,
1606 order_side,
1607 quantity,
1608 price,
1609 Some(time_in_force),
1610 expire_time,
1611 Some(post_only),
1612 None, Some(quote_quantity),
1614 display_qty,
1615 emulation_trigger,
1616 None, None, None, None, None, );
1622
1623 if order_side == OrderSide::Buy {
1624 self.buy_order = Some(order.clone());
1625 } else {
1626 self.sell_order = Some(order.clone());
1627 }
1628
1629 Ok(Some(order))
1630 }
1631
1632 fn arm_limit_order_maintenance(&mut self, side: OrderSide, client_order_id: ClientOrderId) {
1633 if matches!(
1634 self.limit_order_maintenance_state(side),
1635 LimitOrderMaintenanceState::Ready
1636 | LimitOrderMaintenanceState::AwaitingAcceptance { .. }
1637 | LimitOrderMaintenanceState::Active { .. }
1638 ) {
1639 self.set_limit_order_maintenance_state(
1640 side,
1641 LimitOrderMaintenanceState::AwaitingAcceptance { client_order_id },
1642 );
1643 }
1644 }
1645
1646 #[expect(
1652 clippy::too_many_lines,
1653 reason = "stop order submission covers all supported stop order scenarios"
1654 )]
1655 pub(super) fn submit_stop_order(
1656 &mut self,
1657 order_side: OrderSide,
1658 trigger_price: Price,
1659 limit_price: Option<Price>,
1660 ) -> anyhow::Result<()> {
1661 let Some(instrument) = &self.instrument else {
1662 anyhow::bail!("No instrument loaded");
1663 };
1664
1665 if self.config.dry_run {
1666 log_warn!("Dry run, skipping create {order_side:?} stop order");
1667 return Ok(());
1668 }
1669
1670 if order_side == OrderSide::Buy && !self.config.enable_stop_buys {
1671 log_warn!("BUY stop orders not enabled, skipping");
1672 return Ok(());
1673 } else if order_side == OrderSide::Sell && !self.config.enable_stop_sells {
1674 log_warn!("SELL stop orders not enabled, skipping");
1675 return Ok(());
1676 }
1677
1678 let (time_in_force, expire_time) =
1679 self.resolve_time_in_force(self.config.stop_time_in_force);
1680
1681 let quantity = instrument.make_qty(self.config.order_qty.as_f64(), None);
1683 let instrument_id = self.config.instrument_id;
1684 let trigger_type = self.config.stop_trigger_type;
1685 let quote_quantity = self.config.use_quote_quantity;
1686 let display_qty = self.config.order_display_qty;
1687 let emulation_trigger = self.config.emulation_trigger;
1688 let stop_order_type = self.config.stop_order_type;
1689 let trailing_offset = self.config.trailing_offset;
1690 let trailing_offset_type = self.config.trailing_offset_type;
1691
1692 let mut factory = self.order_factory();
1693
1694 let mut order: OrderAny = match stop_order_type {
1695 OrderType::StopMarket => factory.stop_market(
1696 instrument_id,
1697 order_side,
1698 quantity,
1699 trigger_price,
1700 Some(trigger_type),
1701 Some(time_in_force),
1702 expire_time,
1703 None, Some(quote_quantity),
1705 None, emulation_trigger,
1707 None, None, None, None, None, ),
1713 OrderType::StopLimit => {
1714 let Some(limit_price) = limit_price else {
1715 anyhow::bail!("STOP_LIMIT order requires limit_price");
1716 };
1717 factory.stop_limit(
1718 instrument_id,
1719 order_side,
1720 quantity,
1721 limit_price,
1722 trigger_price,
1723 Some(trigger_type),
1724 Some(time_in_force),
1725 expire_time,
1726 None, None, Some(quote_quantity),
1729 display_qty,
1730 emulation_trigger,
1731 None, None, None, None, None, )
1737 }
1738 OrderType::MarketIfTouched => factory.market_if_touched(
1739 instrument_id,
1740 order_side,
1741 quantity,
1742 trigger_price,
1743 Some(trigger_type),
1744 Some(time_in_force),
1745 expire_time,
1746 None, Some(quote_quantity),
1748 emulation_trigger,
1749 None, None, None, None, None, ),
1755 OrderType::LimitIfTouched => {
1756 let Some(limit_price) = limit_price else {
1757 anyhow::bail!("LIMIT_IF_TOUCHED order requires limit_price");
1758 };
1759 factory.limit_if_touched(
1760 instrument_id,
1761 order_side,
1762 quantity,
1763 limit_price,
1764 trigger_price,
1765 Some(trigger_type),
1766 Some(time_in_force),
1767 expire_time,
1768 None, None, Some(quote_quantity),
1771 display_qty,
1772 emulation_trigger,
1773 None, None, None, None, None, )
1779 }
1780 OrderType::TrailingStopMarket => {
1781 let Some(trailing_offset) = trailing_offset else {
1782 anyhow::bail!("TRAILING_STOP_MARKET order requires trailing_offset config");
1783 };
1784 factory.trailing_stop_market(
1785 instrument_id,
1786 order_side,
1787 quantity,
1788 trailing_offset,
1789 Some(trailing_offset_type),
1790 None,
1791 Some(trigger_price),
1792 Some(trigger_type),
1793 Some(time_in_force),
1794 expire_time,
1795 None, Some(quote_quantity),
1797 None, emulation_trigger,
1799 None, None, None, None, None, )
1805 }
1806 _ => {
1807 anyhow::bail!("Unknown stop order type: {stop_order_type:?}");
1808 }
1809 };
1810 drop(factory);
1811
1812 if let OrderAny::TrailingStopMarket(order) = &mut order {
1813 order.activation_price = Some(trigger_price);
1814 }
1815
1816 if order_side == OrderSide::Buy {
1817 self.buy_stop_order = Some(order.clone());
1818 } else {
1819 self.sell_stop_order = Some(order.clone());
1820 }
1821
1822 self.submit_order_apply_params(order)
1823 }
1824
1825 pub(super) fn submit_bracket_order(
1831 &mut self,
1832 order_side: OrderSide,
1833 entry_price: Price,
1834 ) -> anyhow::Result<()> {
1835 let Some(instrument) = &self.instrument else {
1836 anyhow::bail!("No instrument loaded");
1837 };
1838
1839 if self.config.dry_run {
1840 log_warn!("Dry run, skipping create {order_side:?} bracket order");
1841 return Ok(());
1842 }
1843
1844 if self.config.bracket_entry_order_type != OrderType::Limit {
1845 anyhow::bail!(
1846 "Only Limit entry orders are supported for brackets, was {:?}",
1847 self.config.bracket_entry_order_type
1848 );
1849 }
1850
1851 if order_side == OrderSide::Buy && !self.config.enable_limit_buys {
1852 log_warn!("BUY orders not enabled, skipping bracket");
1853 return Ok(());
1854 } else if order_side == OrderSide::Sell && !self.config.enable_limit_sells {
1855 log_warn!("SELL orders not enabled, skipping bracket");
1856 return Ok(());
1857 }
1858
1859 let (time_in_force, expire_time) =
1860 self.resolve_time_in_force(self.config.limit_time_in_force);
1861 let sl_time_in_force = self.config.stop_time_in_force.unwrap_or(TimeInForce::Gtc);
1862 if sl_time_in_force == TimeInForce::Gtd {
1863 anyhow::bail!("GTD time in force not supported for bracket stop-loss legs");
1864 }
1865
1866 let quantity = instrument.make_qty(self.config.order_qty.as_f64(), None);
1867 let increment = instrument.price_increment();
1868 let precision = instrument.price_precision();
1869 let bracket_offset_ticks = self.config.bracket_offset_ticks;
1870
1871 let (unclamped_tp_price, unclamped_sl_trigger_price) = match order_side {
1872 OrderSide::Buy => {
1873 let tp = add_price_ticks(entry_price, increment, bracket_offset_ticks, precision);
1874 let sl = sub_price_ticks(entry_price, increment, bracket_offset_ticks, precision);
1875 (tp, sl)
1876 }
1877 OrderSide::Sell => {
1878 let tp = sub_price_ticks(entry_price, increment, bracket_offset_ticks, precision);
1879 let sl = add_price_ticks(entry_price, increment, bracket_offset_ticks, precision);
1880 (tp, sl)
1881 }
1882 };
1883 let clamp = self.config.clamp_to_instrument_price_range;
1884 let tp_price = clamp_price_to_range(unclamped_tp_price, instrument, clamp);
1885 let sl_trigger_price = clamp_price_to_range(unclamped_sl_trigger_price, instrument, clamp);
1886
1887 let entry_post_only = self.config.use_post_only || self.config.test_reject_post_only;
1888 let instrument_id = self.config.instrument_id;
1889 let quote_quantity = self.config.use_quote_quantity;
1890 let emulation_trigger = self.config.emulation_trigger;
1891 let stop_trigger_type = self.config.stop_trigger_type;
1892 let orders = self
1893 .order_factory()
1894 .bracket()
1895 .instrument_id(instrument_id)
1896 .order_side(order_side)
1897 .quantity(quantity)
1898 .quote_quantity(quote_quantity)
1899 .entry_order_type(OrderType::Limit)
1900 .entry_price(entry_price)
1901 .time_in_force(time_in_force)
1902 .entry_post_only(entry_post_only)
1903 .maybe_emulation_trigger(emulation_trigger)
1904 .maybe_expire_time(expire_time)
1905 .tp_price(tp_price)
1906 .tp_post_only(entry_post_only)
1907 .tp_time_in_force(time_in_force)
1908 .sl_trigger_price(sl_trigger_price)
1909 .sl_trigger_type(stop_trigger_type)
1910 .sl_time_in_force(sl_time_in_force)
1911 .call();
1912
1913 if let Some(entry_order) = orders.first() {
1914 if order_side == OrderSide::Buy {
1915 self.buy_order = Some(entry_order.clone());
1916 } else {
1917 self.sell_order = Some(entry_order.clone());
1918 }
1919 self.arm_limit_order_maintenance(order_side, entry_order.client_order_id());
1920 }
1921
1922 let client_id = self.config.client_id;
1923 if let Some(params) = &self.config.order_params {
1924 self.submit_order_list(orders, None, client_id, Some(params.clone()))
1925 } else {
1926 self.submit_order_list(orders, None, client_id, None)
1927 }
1928 }
1929
1930 fn close_positions_on_stop(
1931 &mut self,
1932 strategy_id: StrategyId,
1933 time_in_force: Option<TimeInForce>,
1934 ) -> anyhow::Result<()> {
1935 let instrument_id = self.config.instrument_id;
1936 let client_id = self.config.client_id;
1937 let reduce_only = Some(self.config.reduce_only_on_stop);
1938 let Some(precision) = self.config.close_positions_qty_precision else {
1939 return self.close_all_positions(
1940 instrument_id,
1941 None,
1942 client_id,
1943 None,
1944 time_in_force,
1945 reduce_only,
1946 None,
1947 None,
1948 );
1949 };
1950
1951 let positions =
1952 self.cache()
1953 .positions_open(None, Some(&instrument_id), Some(&strategy_id), None, None);
1954
1955 if positions.is_empty() {
1956 log::info!("No {instrument_id} open positions to close");
1957 return Ok(());
1958 }
1959
1960 log::info!(
1961 "Closing {} open position{}",
1962 positions.len(),
1963 if positions.len() == 1 { "" } else { "s" },
1964 );
1965
1966 for position in positions {
1967 let position_id = position.id;
1968 let (close_quantity, residual) = split_position_quantity(position.quantity, precision)?;
1969
1970 if close_quantity.is_zero() {
1971 log_warn!(
1972 "Position {position_id} has no venue-fillable close quantity at {precision}-decimal precision; exact residual remains {residual}"
1973 );
1974 continue;
1975 }
1976
1977 if residual > Decimal::ZERO {
1978 log_warn!(
1979 "Position {position_id} close quantity {close_quantity} leaves exact residual {residual} after a full fill"
1980 );
1981 }
1982
1983 let Some(closing_side) = OrderCore::closing_side(position.side) else {
1984 continue;
1985 };
1986 let order = self.order_factory().market(
1987 position.instrument_id,
1988 closing_side,
1989 close_quantity,
1990 time_in_force,
1991 reduce_only,
1992 None,
1993 None,
1994 None,
1995 None,
1996 None,
1997 );
1998
1999 self.submit_order(order, Some(position_id), client_id, None)?;
2000 }
2001
2002 Ok(())
2003 }
2004
2005 pub(super) fn open_position(&mut self, net_qty: Decimal) -> anyhow::Result<()> {
2011 let Some(instrument) = &self.instrument else {
2012 anyhow::bail!("No instrument loaded");
2013 };
2014
2015 if self.config.dry_run {
2016 log_warn!("Dry run, skipping open position");
2017 return Ok(());
2018 }
2019
2020 if net_qty == Decimal::ZERO {
2021 log_warn!("Open position with zero quantity, skipping");
2022 return Ok(());
2023 }
2024
2025 let order_side = if net_qty > Decimal::ZERO {
2026 OrderSide::Buy
2027 } else {
2028 OrderSide::Sell
2029 };
2030
2031 let quantity = instrument.make_qty(net_qty.abs().to_f64().unwrap_or(0.0), None);
2032
2033 let reduce_only = if self.config.test_reject_reduce_only {
2035 Some(true)
2036 } else {
2037 None
2038 };
2039 let instrument_id = self.config.instrument_id;
2040 let time_in_force = self.config.open_position_time_in_force;
2041 let quote_quantity = self.config.use_quote_quantity;
2042
2043 let order = self.order_factory().market(
2044 instrument_id,
2045 order_side,
2046 quantity,
2047 Some(time_in_force),
2048 reduce_only,
2049 Some(quote_quantity),
2050 None, None, None, None, );
2055
2056 self.submit_order_apply_params(order)
2057 }
2058
2059 pub(super) fn cancel_active_orders(
2060 &mut self,
2061 instrument_id: InstrumentId,
2062 strategy_id: StrategyId,
2063 client_id: Option<ClientId>,
2064 ) {
2065 let bracket_targets: Vec<ClientOrderId> = {
2068 let cache = self.cache();
2069 let mut targets = Vec::new();
2070
2071 for order_list in
2072 cache.order_lists(None, Some(&instrument_id), Some(&strategy_id), None)
2073 {
2074 let is_bracket = order_list.client_order_ids.iter().any(|cid| {
2075 cache
2076 .order(cid)
2077 .is_some_and(|o| is_in_contingency_group(&o))
2078 });
2079
2080 if !is_bracket {
2081 continue;
2082 }
2083
2084 for cid in &order_list.client_order_ids {
2085 if let Some(order) = cache.order(cid)
2086 && !order.is_closed()
2087 && !order.is_pending_cancel()
2088 {
2089 targets.push(*cid);
2090 }
2091 }
2092 }
2093 targets
2094 };
2095
2096 for cid in bracket_targets {
2097 if let Err(e) = self.cancel_order(cid, client_id, None) {
2098 log::error!("Failed to cancel bracket leg {cid}: {e}");
2099 }
2100 }
2101
2102 if self.config.use_individual_cancels_on_stop {
2103 for cid in self.collect_cancellable_order_ids(instrument_id, strategy_id) {
2104 if let Err(e) = self.cancel_order(cid, client_id, None) {
2105 log::error!("Failed to cancel order {cid}: {e}");
2106 }
2107 }
2108 } else if self.config.use_batch_cancel_on_stop {
2109 let candidates = self.collect_cancellable_orders(instrument_id, strategy_id);
2110 let mut batchable: Vec<ClientOrderId> = Vec::new();
2111
2112 for order in candidates {
2113 let cid = order.client_order_id();
2114 if order.is_emulated() || order.is_active_local() {
2115 if let Err(e) = self.cancel_order(cid, client_id, None) {
2116 log::error!("Failed to cancel local order {cid}: {e}");
2117 }
2118 } else {
2119 batchable.push(cid);
2120 }
2121 }
2122
2123 if !batchable.is_empty()
2124 && let Err(e) = self.cancel_orders(batchable, client_id, None)
2125 {
2126 log::error!("Failed to batch cancel orders: {e}");
2127 }
2128 } else {
2129 let local_ids: Vec<ClientOrderId> = {
2132 let cache = self.cache();
2133 cache
2134 .orders_active_local(None, Some(&instrument_id), Some(&strategy_id), None, None)
2135 .into_iter()
2136 .filter(|o| {
2137 !o.is_closed() && !o.is_pending_cancel() && !is_in_contingency_group(o)
2138 })
2139 .map(|o| o.client_order_id())
2140 .collect()
2141 };
2142
2143 for cid in local_ids {
2144 if let Err(e) = self.cancel_order(cid, client_id, None) {
2145 log::error!("Failed to cancel active-local order {cid}: {e}");
2146 }
2147 }
2148
2149 if let Err(e) = self.cancel_all_orders(instrument_id, None, client_id, false, None) {
2150 log::error!("Failed to cancel all orders: {e}");
2151 }
2152 }
2153 }
2154
2155 pub(super) fn collect_cancellable_orders(
2156 &self,
2157 instrument_id: InstrumentId,
2158 strategy_id: StrategyId,
2159 ) -> Vec<OrderAny> {
2160 let cache = self.cache();
2161 let mut seen: AHashSet<ClientOrderId> = AHashSet::new();
2162 let mut candidates: Vec<OrderAny> = Vec::new();
2163 let sources = [
2166 cache.orders_active_local(None, Some(&instrument_id), Some(&strategy_id), None, None),
2167 cache.orders_emulated(None, Some(&instrument_id), Some(&strategy_id), None, None),
2168 cache.orders_inflight(None, Some(&instrument_id), Some(&strategy_id), None, None),
2169 cache.orders_open(None, Some(&instrument_id), Some(&strategy_id), None, None),
2170 ];
2171
2172 for orders in sources {
2173 for order in orders {
2174 if order.is_closed() || order.is_pending_cancel() || is_in_contingency_group(&order)
2175 {
2176 continue;
2177 }
2178 let cid = order.client_order_id();
2179 if seen.insert(cid) {
2180 candidates.push(order);
2181 }
2182 }
2183 }
2184 candidates
2185 }
2186
2187 pub(super) fn collect_cancellable_order_ids(
2188 &self,
2189 instrument_id: InstrumentId,
2190 strategy_id: StrategyId,
2191 ) -> Vec<ClientOrderId> {
2192 self.collect_cancellable_orders(instrument_id, strategy_id)
2193 .into_iter()
2194 .map(|o| o.client_order_id())
2195 .collect()
2196 }
2197}
2198
2199impl LimitOrderMaintenanceState {
2200 const fn new(enabled: bool) -> Self {
2201 if enabled { Self::Ready } else { Self::Disabled }
2202 }
2203
2204 const fn client_order_id(self) -> Option<ClientOrderId> {
2205 match self {
2206 Self::AwaitingAcceptance { client_order_id }
2207 | Self::Active { client_order_id }
2208 | Self::AwaitingUpdate {
2209 client_order_id, ..
2210 }
2211 | Self::AwaitingCancel {
2212 client_order_id, ..
2213 }
2214 | Self::AwaitingReplacementAcceptance {
2215 client_order_id, ..
2216 } => Some(client_order_id),
2217 Self::Disabled | Self::Ready | Self::Completed | Self::Failed => None,
2218 }
2219 }
2220}
2221
2222fn split_position_quantity(
2223 quantity: Quantity,
2224 precision: u8,
2225) -> anyhow::Result<(Quantity, Decimal)> {
2226 let quantity_decimal = quantity.as_decimal();
2227 let close_decimal = quantity_decimal.trunc_with_scale(u32::from(precision));
2228 let close_quantity = Quantity::from_decimal_dp(close_decimal, quantity.precision)
2229 .map_err(|e| anyhow::anyhow!("Invalid position close quantity {close_decimal}: {e}"))?;
2230 let residual = quantity_decimal - close_quantity.as_decimal();
2231 Ok((close_quantity, residual))
2232}
2233
2234fn add_price_ticks(base: Price, increment: Price, ticks: u64, precision: u8) -> Price {
2235 let offset = price_tick_offset(increment, ticks, precision);
2236 base + offset
2237}
2238
2239fn sub_price_ticks(base: Price, increment: Price, ticks: u64, precision: u8) -> Price {
2240 let offset = price_tick_offset(increment, ticks, precision);
2241 base - offset
2242}
2243
2244fn price_tick_offset(increment: Price, ticks: u64, precision: u8) -> Price {
2245 let offset = increment * Decimal::from(ticks);
2246 Price::from_decimal_dp(offset, precision)
2247 .unwrap_or_else(|e| panic!("Failed to calculate price tick offset: {e}"))
2248}
2249
2250fn is_in_contingency_group(order: &OrderAny) -> bool {
2251 matches!(
2252 order.contingency_type(),
2253 Some(ContingencyType::Oto | ContingencyType::Oco | ContingencyType::Ouo)
2254 )
2255}
2256
2257fn clamp_price_to_range(price: Price, instrument: &InstrumentAny, enabled: bool) -> Price {
2258 if !enabled {
2259 return price;
2260 }
2261 let mut clamped = price;
2262 if let Some(min) = instrument.min_price()
2263 && clamped < min
2264 {
2265 clamped = min;
2266 }
2267
2268 if let Some(max) = instrument.max_price()
2269 && clamped > max
2270 {
2271 clamped = max;
2272 }
2273
2274 clamped
2275}
2276
2277#[derive(Debug)]
2278struct StopOrderReplacement {
2279 client_order_id: ClientOrderId,
2280 trigger_price: Price,
2281 limit_price: Option<Price>,
2282}