Skip to main content

nautilus_testkit/testers/exec/
strategy.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use 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/// An execution tester strategy for live testing order execution functionality.
49///
50/// This strategy is designed for testing execution adapters by submitting
51/// limit orders, stop orders, and managing positions. It can maintain orders
52/// at a configurable offset from the top of book.
53///
54/// **WARNING**: This strategy has no alpha advantage whatsoever.
55/// It is not intended to be used for live trading with real money.
56#[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    // Order tracking
69    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    // One-shot guard for `test_modify_rejected`: ensures the programmatic
76    // modify is attempted at most once across the strategy's lifetime.
77    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            // Also subscribe to market data to trigger instrument definitions from data providers
198            // (e.g., Databento sends instrument definitions as part of market data subscriptions)
199            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            // Log own order book if available
315            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(()); // Wait for market
329        };
330        let Some(best_ask) = book.best_ask_price() else {
331            return Ok(()); // Wait for market
332        };
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    /// Creates a new [`ExecTester`] instance.
377    #[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    /// Submit an order, applying `order_params` if configured.
581    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    /// Maintain orders based on current market prices.
591    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    /// Refreshes the locally-tracked order for `side` from the cache so that
622    /// downstream checks (`venue_order_id()`, `is_pending_*`, status) see the
623    /// latest event-driven state instead of the stale clone captured at submit.
624    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    /// Maintain buy limit orders.
659    fn maintain_buy_orders(&mut self, best_bid: Price, best_ask: Price) {
660        // Refresh from cache first so post-submit event state (venue_order_id,
661        // status) is visible. Done before binding `&self.instrument` to avoid
662        // holding an immutable borrow across the mutable refresh call.
663        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        // `test_reject_post_only` and `limit_aggressive` both cross the spread for
676        // BUY (place at/above the ask). `test_reject_post_only` additionally sets
677        // post_only=true downstream to trigger venue rejection; `limit_aggressive`
678        // pairs with IOC/FOK TIF for marketable-fill scenarios.
679        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            // One-shot programmatic modify to exercise the adapter's modify-rejection
718            // path (TC-E36). Uses a small price bump rather than waiting for drift.
719            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    /// Maintain sell limit orders.
765    fn maintain_sell_orders(&mut self, best_bid: Price, best_ask: Price) {
766        // Refresh from cache before borrowing `&self.instrument`; see the
767        // matching comment in `maintain_buy_orders`.
768        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        // See `maintain_buy_orders` for the cross_spread and refresh rationale.
781        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            // One-shot programmatic modify (TC-E36); see maintain_buy_orders.
820            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    /// Submits a buy and sell limit order as an order list (batch).
1143    fn maintain_batch_limit_pair(&mut self, best_bid: Price, best_ask: Price) {
1144        // Same rationale as the non-batch path: refresh from cache so the
1145        // active-order check sees the latest status. Done before binding
1146        // `&self.instrument` to avoid an immutable-vs-mutable borrow conflict.
1147        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        // `test_reject_post_only` and `limit_aggressive` flip the BUY/SELL
1174        // pricing to cross the spread; mirrored from `maintain_buy_orders` /
1175        // `maintain_sell_orders` so batch mode supports the same scenarios.
1176        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    /// Maintain stop buy orders.
1248    fn maintain_stop_buy_orders(&mut self, best_bid: Price, best_ask: Price) {
1249        self.refresh_tracked_stop_order(OrderSide::Buy);
1250
1251        // Avoid churn: leave a rejected/denied stop alone (no resubmit or modify)
1252        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        // Determine trigger price based on order type
1267        let unclamped_trigger_price = if matches!(
1268            self.config.stop_order_type,
1269            OrderType::LimitIfTouched | OrderType::MarketIfTouched | OrderType::TrailingStopMarket
1270        ) {
1271            // IF_TOUCHED and trailing-stop buy: place BELOW market
1272            sub_price_ticks(best_bid, increment, stop_offset_ticks, precision)
1273        } else {
1274            // STOP buy orders are placed ABOVE the market (stop loss on short)
1275            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        // Calculate limit price if needed
1281        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                    // BUY LIT/StopLimit both require trigger_price <= price.
1288                    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    /// Maintain stop sell orders.
1345    fn maintain_stop_sell_orders(&mut self, best_bid: Price, best_ask: Price) {
1346        self.refresh_tracked_stop_order(OrderSide::Sell);
1347
1348        // Avoid churn: leave a rejected/denied stop alone (no resubmit or modify)
1349        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        // Determine trigger price based on order type
1364        let unclamped_trigger_price = if matches!(
1365            self.config.stop_order_type,
1366            OrderType::LimitIfTouched | OrderType::MarketIfTouched | OrderType::TrailingStopMarket
1367        ) {
1368            // IF_TOUCHED and trailing-stop sell: place ABOVE market
1369            add_price_ticks(best_ask, increment, stop_offset_ticks, precision)
1370        } else {
1371            // STOP sell orders are placed BELOW the market (stop loss on long)
1372            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        // Calculate limit price if needed
1378        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                    // SELL LIT/StopLimit both require trigger_price >= price.
1385                    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    /// Submit a limit order.
1523    ///
1524    /// # Errors
1525    ///
1526    /// Returns an error if order creation or submission fails.
1527    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, // reduce_only
1613            Some(quote_quantity),
1614            display_qty,
1615            emulation_trigger,
1616            None, // trigger_instrument_id
1617            None, // exec_algorithm_id
1618            None, // exec_algorithm_params
1619            None, // tags
1620            None, // client_order_id
1621        );
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    /// Submit a stop order.
1647    ///
1648    /// # Errors
1649    ///
1650    /// Returns an error if order creation or submission fails.
1651    #[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        // Use instrument's make_qty to ensure correct precision
1682        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, // reduce_only
1704                Some(quote_quantity),
1705                None, // display_qty
1706                emulation_trigger,
1707                None, // trigger_instrument_id
1708                None, // exec_algorithm_id
1709                None, // exec_algorithm_params
1710                None, // tags
1711                None, // client_order_id
1712            ),
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, // post_only
1727                    None, // reduce_only
1728                    Some(quote_quantity),
1729                    display_qty,
1730                    emulation_trigger,
1731                    None, // trigger_instrument_id
1732                    None, // exec_algorithm_id
1733                    None, // exec_algorithm_params
1734                    None, // tags
1735                    None, // client_order_id
1736                )
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, // reduce_only
1747                Some(quote_quantity),
1748                emulation_trigger,
1749                None, // trigger_instrument_id
1750                None, // exec_algorithm_id
1751                None, // exec_algorithm_params
1752                None, // tags
1753                None, // client_order_id
1754            ),
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, // post_only
1769                    None, // reduce_only
1770                    Some(quote_quantity),
1771                    display_qty,
1772                    emulation_trigger,
1773                    None, // trigger_instrument_id
1774                    None, // exec_algorithm_id
1775                    None, // exec_algorithm_params
1776                    None, // tags
1777                    None, // client_order_id
1778                )
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, // reduce_only
1796                    Some(quote_quantity),
1797                    None, // display_qty
1798                    emulation_trigger,
1799                    None, // trigger_instrument_id
1800                    None, // exec_algorithm_id
1801                    None, // exec_algorithm_params
1802                    None, // tags
1803                    None, // client_order_id
1804                )
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    /// Submit a bracket order (entry with stop-loss and take-profit).
1826    ///
1827    /// # Errors
1828    ///
1829    /// Returns an error if order creation or submission fails.
1830    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    /// Open a position with a market order.
2006    ///
2007    /// # Errors
2008    ///
2009    /// Returns an error if order creation or submission fails.
2010    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        // Test reduce_only rejection by setting reduce_only on open position order
2034        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, // exec_algorithm_id
2051            None, // exec_algorithm_params
2052            None, // tags
2053            None, // client_order_id
2054        );
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        // Reach INITIALIZED contingent legs that the open/emulated/inflight indexes
2066        // miss. Skip non-bracket lists so the configured cancel mode owns them.
2067        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            // `cancel_all_orders` does not reach active-local orders; cancel those
2130            // individually first. Brackets are handled by the sweep above.
2131            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        // `orders_active_local` catches just-submitted orders not yet in the other
2164        // indexes. Bracket legs are excluded; the sweep in `cancel_active_orders` owns them.
2165        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}