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