Skip to main content

nautilus_trading/strategy/
mod.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
16pub mod api;
17#[doc(hidden)]
18pub mod binding;
19pub mod config;
20pub mod core;
21
22pub use core::{StrategyCore, StrategyNative};
23use std::panic::{AssertUnwindSafe, catch_unwind};
24
25use ahash::AHashSet;
26pub use api::{OrderApi, PortfolioApi};
27use binding::StrategyBinding;
28pub use config::{ImportableStrategyConfig, StrategyConfig};
29use nautilus_common::{
30    actor::DataActor,
31    component::Component,
32    enums::ComponentState,
33    logging::{CMD, EVT, RECV, SEND},
34    messages::execution::{
35        BatchCancelOrders, BatchModifyOrders, CancelAllOrders, CancelOrder, ModifyOrder,
36        QueryAccount, QueryOrder, SubmitOrder, SubmitOrderList, TradingCommand,
37    },
38    msgbus::{self, MessagingSwitchboard},
39    timer::TimeEvent,
40};
41use nautilus_core::{DurationNanos, Params, UUID4};
42use nautilus_execution::order_manager::OrderManagerAction;
43use nautilus_model::{
44    enums::{OrderSide, OrderStatus, OrderType, PositionSide, TimeInForce},
45    events::{
46        OrderAccepted, OrderCancelDenied, OrderCancelRejected, OrderCanceled, OrderDenied,
47        OrderEmulated, OrderEventAny, OrderExpired, OrderFillVoided, OrderFilled, OrderInitialized,
48        OrderModifyDenied, OrderModifyRejected, OrderPendingCancel, OrderPendingUpdate,
49        OrderRejected, OrderReleased, OrderSubmitted, OrderTriggered, OrderUpdated,
50        PositionChanged, PositionClosed, PositionEvent, PositionOpened,
51    },
52    identifiers::{
53        AccountId, ClientId, ClientOrderId, ExecAlgorithmId, InstrumentId, PositionId, StrategyId,
54        TraderId,
55    },
56    instruments::Instrument,
57    orders::{
58        LIMIT_ORDER_TYPES, Order, OrderAny, OrderCore, OrderError, OrderList, STOP_ORDER_TYPES,
59    },
60    position::Position,
61    types::{Price, Quantity},
62};
63use ustr::Ustr;
64
65/// Describes one child update in a batch modify request.
66pub type BatchModifyOrder = (
67    ClientOrderId,
68    Option<Quantity>,
69    Option<Price>,
70    Option<Price>,
71);
72
73/// Core trait for implementing trading strategies in NautilusTrader.
74///
75/// Strategies are specialized [`DataActor`]s that combine data ingestion capabilities with
76/// order and position management functionality. By implementing this trait,
77/// custom strategies gain access to the full trading execution stack including order
78/// submission, modification, cancellation, and position management.
79///
80/// # Key Capabilities
81///
82/// - All [`DataActor`] capabilities (data subscriptions, event handling, timers).
83/// - Order lifecycle management (submit, modify, cancel).
84/// - Position management (open, close, monitor).
85/// - Access to the trading cache and portfolio.
86/// - Event routing for orders and emulator events.
87///
88/// # Implementation
89///
90/// Use the `nautilus_strategy!` macro to generate the native runtime wiring
91/// and `Strategy` implementations. Normal strategy logic should call facade
92/// methods such as `strategy_id()`, `clock()`, `cache()`, `order()`, and
93/// `portfolio()`. Native runtime code that needs the internal core should use
94/// [`StrategyNative`]. For strategies that override additional trait methods,
95/// pass them in a block:
96///
97/// ```ignore
98/// nautilus_strategy!(MyStrategy, {
99///     fn on_order_rejected(&mut self, event: OrderRejected) {
100///         // custom handling
101///     }
102/// });
103/// ```
104///
105/// Default methods that read or mutate native runtime state carry explicit
106/// [`StrategyNative`] and [`Component`] bounds. Implementations that only need
107/// behavioral callbacks do not own or implement native runtime state.
108pub trait Strategy: DataActor {
109    /// Returns the instrument IDs this strategy intends to claim for external order routing.
110    ///
111    /// Live strategy registration materializes this configuration intent as active cache claims.
112    fn external_order_instrument_ids(&self) -> Option<Vec<InstrumentId>> {
113        None
114    }
115
116    /// Replaces this strategy's active external order claims with `instrument_ids`.
117    ///
118    /// External orders, fills, and materialized reconciliation activity for matching instrument
119    /// IDs are assigned to the strategy. Passing an empty vector releases every claim owned by the
120    /// strategy. Existing cached orders keep their assigned strategy ID.
121    ///
122    /// # Errors
123    ///
124    /// Returns an error if the strategy is not registered, the cache is already borrowed, an
125    /// instrument is repeated, or an instrument is claimed by another strategy.
126    fn set_external_order_instrument_ids(
127        &mut self,
128        instrument_ids: Vec<InstrumentId>,
129    ) -> anyhow::Result<()>
130    where
131        Self: StrategyNative,
132    {
133        let core = StrategyNative::strategy_core_mut(self);
134        let strategy_id = registered_strategy_id(core)?;
135        if !core.actor.is_registered() {
136            anyhow::bail!("Strategy {strategy_id} is not registered with a trader");
137        }
138        let cache = core.cache_rc();
139        cache
140            .try_borrow_mut()
141            .map_err(|e| anyhow::anyhow!("Cannot set external order claims: {e}"))?
142            .set_external_order_claims(strategy_id, &instrument_ids)?;
143        core.config.external_order_instrument_ids = Some(instrument_ids);
144        Ok(())
145    }
146
147    /// Returns the runtime strategy ID, when configured or registered.
148    fn strategy_id(&self) -> Option<StrategyId>
149    where
150        Self: StrategyNative,
151    {
152        StrategyNative::strategy_core(self).strategy_id()
153    }
154
155    /// Returns the user-facing order creation API.
156    fn order(&self) -> OrderApi<'_>
157    where
158        Self: StrategyNative,
159    {
160        StrategyNative::strategy_core(self).order()
161    }
162
163    /// Returns the user-facing portfolio read API.
164    fn portfolio(&self) -> PortfolioApi<'_>
165    where
166        Self: StrategyNative,
167    {
168        StrategyNative::strategy_core(self).portfolio_api()
169    }
170
171    /// Submits an order.
172    ///
173    /// # Errors
174    ///
175    /// Returns an error if:
176    /// - The strategy is not registered.
177    /// - The supplied order is not `INITIALIZED`.
178    /// - A cached order with the same client order ID is not `INITIALIZED`, `EMULATED`,
179    ///   or `RELEASED`.
180    /// - Order submission fails.
181    fn submit_order(
182        &mut self,
183        order: OrderAny,
184        position_id: Option<PositionId>,
185        client_id: Option<ClientId>,
186        params: Option<Params>,
187    ) -> anyhow::Result<()>
188    where
189        Self: StrategyBinding,
190    {
191        self.binding_submit_order(order, position_id, client_id, params)
192    }
193
194    /// Submits an order list.
195    ///
196    /// # Errors
197    ///
198    /// Returns an error if the strategy is not registered, the order list is invalid,
199    /// or order list submission fails.
200    fn submit_order_list(
201        &mut self,
202        mut orders: Vec<OrderAny>,
203        position_id: Option<PositionId>,
204        client_id: Option<ClientId>,
205        params: Option<Params>,
206    ) -> anyhow::Result<()>
207    where
208        Self: StrategyNative,
209    {
210        if orders.is_empty() {
211            anyhow::bail!("OrderList denied: no orders to submit");
212        }
213
214        for order in &orders {
215            if order.status() != OrderStatus::Initialized {
216                anyhow::bail!(
217                    "Order in list denied: invalid status for {}, expected INITIALIZED",
218                    order.client_order_id()
219                );
220            }
221        }
222
223        let first_venue = orders[0].instrument_id().venue;
224        for order in &orders {
225            if order.instrument_id().venue != first_venue {
226                anyhow::bail!(
227                    "OrderList denied: orders must share the same venue; \
228                     expected {first_venue}, found {} on {}",
229                    order.instrument_id().venue,
230                    order.client_order_id(),
231                );
232            }
233        }
234
235        let should_deny = {
236            let core = StrategyNative::strategy_core_mut(self);
237            let tag = core.market_exit_tag;
238            core.is_exiting
239                && orders.iter().any(|o| {
240                    !o.is_reduce_only() && !o.tags().is_some_and(|tags| tags.contains(&tag))
241                })
242        };
243
244        if should_deny {
245            self.deny_order_list(&orders, Ustr::from("MARKET_EXIT_IN_PROGRESS"));
246            return Ok(());
247        }
248
249        let core = StrategyNative::strategy_core_mut(self);
250
251        let trader_id = registered_trader_id(core)?;
252        let strategy_id = registered_strategy_id(core)?;
253        let ts_init = core.clock_mut().timestamp_ns();
254
255        // TODO: Replace with fluent builder API for order list construction
256        let order_list = if orders.first().is_some_and(|o| o.order_list_id().is_some()) {
257            OrderList::from_orders(&orders, ts_init)
258        } else {
259            core.order_factory().create_list(&mut orders, ts_init)
260        };
261
262        if let Err(e) = order_list.validate() {
263            anyhow::bail!("OrderList denied: {e}");
264        }
265
266        {
267            let cache_rc = core.cache_rc();
268            let mut cache = cache_rc.try_borrow_mut().map_err(|_| {
269                anyhow::anyhow!(
270                    "Cannot submit order list {}: cache is currently borrowed",
271                    order_list.id
272                )
273            })?;
274
275            if cache.order_list_exists(&order_list.id) {
276                anyhow::bail!("OrderList denied: duplicate {}", order_list.id);
277            }
278
279            for order in &orders {
280                if cache.order_exists(&order.client_order_id()) {
281                    anyhow::bail!(
282                        "Order in list denied: duplicate {}",
283                        order.client_order_id()
284                    );
285                }
286            }
287
288            cache.add_order_list(order_list.clone())?;
289            for order in &orders {
290                cache.add_order(order.clone(), position_id, client_id, true)?;
291            }
292        }
293
294        for order in &orders {
295            publish_order_initialized(order);
296        }
297
298        let params = params.filter(|params| !params.is_empty());
299
300        let first_order = orders.first();
301        let order_inits: Vec<_> = orders.iter().map(|o| o.init_event().clone()).collect();
302        let exec_algorithm_id = first_order.and_then(Order::exec_algorithm_id);
303
304        let command = SubmitOrderList::new(
305            trader_id,
306            client_id,
307            strategy_id,
308            order_list,
309            order_inits,
310            exec_algorithm_id,
311            position_id,
312            params,
313            UUID4::new(),
314            ts_init,
315            None, // correlation_id
316        );
317
318        let has_emulated_order = orders
319            .iter()
320            .any(|o| o.emulation_trigger().is_some() || o.is_emulated());
321
322        if has_emulated_order {
323            send_emulator_command(TradingCommand::SubmitOrderList(command));
324        } else if let Some(algo_id) = exec_algorithm_id {
325            let endpoint = format!("{algo_id}.execute");
326            msgbus::send_any(endpoint.into(), &TradingCommand::SubmitOrderList(command));
327        } else {
328            send_risk_command(TradingCommand::SubmitOrderList(command));
329        }
330
331        for order in &orders {
332            self.set_gtd_expiry(order)?;
333        }
334
335        Ok(())
336    }
337
338    /// Modifies an order.
339    ///
340    /// # Errors
341    ///
342    /// Returns an error if the strategy is not registered, no supplied value changes,
343    /// or order modification fails. A supplied quantity counts as an update while
344    /// the order is pending update, even when the quantity is unchanged.
345    fn modify_order(
346        &mut self,
347        client_order_id: ClientOrderId,
348        quantity: Option<Quantity>,
349        price: Option<Price>,
350        trigger_price: Option<Price>,
351        client_id: Option<ClientId>,
352        params: Option<Params>,
353    ) -> anyhow::Result<()>
354    where
355        Self: StrategyNative,
356    {
357        let (trader_id, strategy_id) = {
358            let core = StrategyNative::strategy_core_mut(self);
359            (registered_trader_id(core)?, registered_strategy_id(core)?)
360        };
361
362        let params = params.filter(|params| !params.is_empty());
363
364        // TODO: Snapshot the order from the cache. See `cancel_order` for the rationale.
365        let order = StrategyNative::strategy_core_mut(self)
366            .cache_rc()
367            .borrow()
368            .try_order_owned(&client_order_id)
369            .map_err(|e| anyhow::anyhow!("Cannot modify order: {e}"))?;
370
371        let mut updating = false;
372
373        if quantity.is_some_and(|q| q != order.quantity() || order.is_pending_update()) {
374            updating = true;
375        }
376
377        if let Some(price) = price {
378            if !LIMIT_ORDER_TYPES.contains(&order.order_type()) {
379                anyhow::bail!("{} orders do not have a LIMIT price", order.order_type());
380            }
381
382            if Some(price) != order.price() {
383                updating = true;
384            }
385        }
386
387        if let Some(trigger_price) = trigger_price {
388            if !has_trigger_price(order.order_type()) {
389                anyhow::bail!(
390                    "{} orders do not have a STOP trigger price",
391                    order.order_type()
392                );
393            }
394
395            if Some(trigger_price) != order.trigger_price() {
396                updating = true;
397            }
398        }
399
400        if !updating {
401            anyhow::bail!(
402                "Cannot create command ModifyOrder: quantity, price, and trigger were either None \
403                or the same as existing values"
404            );
405        }
406
407        if order.is_closed() || order.is_pending_cancel() {
408            log::warn!(
409                "Cannot create command ModifyOrder: state is {:?}, {order:?}",
410                order.status()
411            );
412            return Ok(());
413        }
414
415        let command_id = UUID4::new();
416
417        if !self.mark_order_pending_update(&order, command_id)? {
418            return Ok(());
419        }
420
421        let command = ModifyOrder::new(
422            trader_id,
423            client_id,
424            strategy_id,
425            order.instrument_id(),
426            order.client_order_id(),
427            order.venue_order_id(),
428            quantity,
429            price,
430            trigger_price,
431            command_id,
432            StrategyNative::strategy_core_mut(self)
433                .clock_mut()
434                .timestamp_ns(),
435            params,
436            None, // correlation_id
437        );
438
439        if order.emulation_trigger().is_some() || order.is_emulated() {
440            send_emulator_command(TradingCommand::ModifyOrder(command));
441        } else if let Some(algo_id) = order
442            .exec_algorithm_id()
443            .filter(|_| order.is_active_local())
444        {
445            let endpoint = format!("{algo_id}.execute");
446            msgbus::send_any(endpoint.into(), &TradingCommand::ModifyOrder(command));
447        } else {
448            send_risk_command(TradingCommand::ModifyOrder(command));
449        }
450        Ok(())
451    }
452
453    /// Batch modifies multiple orders for the same instrument.
454    ///
455    /// Each tuple is `(client_order_id, quantity, price, trigger_price)`.
456    ///
457    /// # Errors
458    ///
459    /// Returns an error if the strategy is not registered, the orders span multiple instruments,
460    /// contain emulated/local orders, or a child modify is invalid.
461    fn modify_orders(
462        &mut self,
463        updates: Vec<BatchModifyOrder>,
464        client_id: Option<ClientId>,
465        params: Option<Params>,
466    ) -> anyhow::Result<()>
467    where
468        Self: StrategyNative,
469    {
470        if updates.is_empty() {
471            anyhow::bail!("Cannot batch modify empty order list");
472        }
473
474        let (trader_id, strategy_id, ts_init) = {
475            let core = StrategyNative::strategy_core_mut(self);
476            (
477                registered_trader_id(core)?,
478                registered_strategy_id(core)?,
479                core.clock_mut().timestamp_ns(),
480            )
481        };
482
483        let orders: Vec<OrderAny> = {
484            let cache_rc = StrategyNative::strategy_core_mut(self).cache_rc();
485            let cache = cache_rc.borrow();
486            updates
487                .iter()
488                .map(|(client_order_id, _, _, _)| {
489                    cache
490                        .try_order_owned(client_order_id)
491                        .map_err(|e| anyhow::anyhow!("Cannot modify order: {e}"))
492                })
493                .collect::<Result<_, _>>()?
494        };
495
496        let instrument_id = orders[0].instrument_id();
497
498        for (order, (_, quantity, price, trigger_price)) in orders.iter().zip(updates.iter()) {
499            if order.instrument_id() != instrument_id {
500                anyhow::bail!(
501                    "Cannot batch modify orders for different instruments: {} vs {}",
502                    instrument_id,
503                    order.instrument_id()
504                );
505            }
506
507            if order.is_emulated() || order.is_active_local() {
508                anyhow::bail!("Cannot include emulated or local orders in batch modify");
509            }
510
511            let mut updating = false;
512
513            if quantity.is_some_and(|q| q != order.quantity()) {
514                updating = true;
515            }
516
517            if let Some(price) = price {
518                if !LIMIT_ORDER_TYPES.contains(&order.order_type()) {
519                    anyhow::bail!("{} orders do not have a LIMIT price", order.order_type());
520                }
521
522                if Some(*price) != order.price() {
523                    updating = true;
524                }
525            }
526
527            if let Some(trigger_price) = trigger_price {
528                if !has_trigger_price(order.order_type()) {
529                    anyhow::bail!(
530                        "{} orders do not have a STOP trigger price",
531                        order.order_type()
532                    );
533                }
534
535                if Some(*trigger_price) != order.trigger_price() {
536                    updating = true;
537                }
538            }
539
540            if !updating {
541                anyhow::bail!(
542                    "Cannot create command BatchModifyOrders: quantity, price, and trigger were \
543                    either None or the same as existing values for {}",
544                    order.client_order_id()
545                );
546            }
547
548            if order.is_closed() || order.is_pending_cancel() {
549                anyhow::bail!(
550                    "Cannot create command BatchModifyOrders: state is {:?}, {order:?}",
551                    order.status()
552                );
553            }
554        }
555
556        let params = params.filter(|params| !params.is_empty());
557        let mut modifies = Vec::with_capacity(orders.len());
558
559        for (order, (_, quantity, price, trigger_price)) in orders.into_iter().zip(updates) {
560            let command_id = UUID4::new();
561            if !self.mark_order_pending_update(&order, command_id)? {
562                continue;
563            }
564
565            modifies.push(ModifyOrder::new(
566                trader_id,
567                client_id,
568                strategy_id,
569                instrument_id,
570                order.client_order_id(),
571                order.venue_order_id(),
572                quantity,
573                price,
574                trigger_price,
575                command_id,
576                ts_init,
577                params.clone(),
578                None, // correlation_id
579            ));
580        }
581
582        if modifies.is_empty() {
583            log::warn!("Cannot send `BatchModifyOrders`, no valid modify commands");
584            return Ok(());
585        }
586
587        let command = BatchModifyOrders::new(
588            trader_id,
589            client_id,
590            strategy_id,
591            instrument_id,
592            modifies,
593            UUID4::new(),
594            ts_init,
595            params,
596            None, // correlation_id
597        );
598
599        send_risk_command(TradingCommand::ModifyOrders(command));
600        Ok(())
601    }
602
603    /// Cancels an order.
604    ///
605    /// # Errors
606    ///
607    /// Returns an error if the strategy is not registered or order cancellation fails.
608    fn cancel_order(
609        &mut self,
610        client_order_id: ClientOrderId,
611        client_id: Option<ClientId>,
612        params: Option<Params>,
613    ) -> anyhow::Result<()>
614    where
615        Self: StrategyNative,
616    {
617        let (trader_id, strategy_id, ts_init) = {
618            let core = StrategyNative::strategy_core_mut(self);
619            (
620                registered_trader_id(core)?,
621                registered_strategy_id(core)?,
622                core.clock_mut().timestamp_ns(),
623            )
624        };
625
626        let params = params.filter(|params| !params.is_empty());
627
628        // TODO: Snapshot the order from the cache. Callers identify it by ID; we own the
629        // snapshot so later calls (which take `&OrderAny` and may re-enter the cache)
630        // run without holding a live cache borrow.
631        let order = StrategyNative::strategy_core_mut(self)
632            .cache_rc()
633            .borrow()
634            .try_order_owned(&client_order_id)
635            .map_err(|e| anyhow::anyhow!("Cannot cancel order: {e}"))?;
636
637        let command_id = UUID4::new();
638
639        if !self.mark_order_pending_cancel(&order, command_id)? {
640            return Ok(());
641        }
642
643        let command = CancelOrder::new(
644            trader_id,
645            client_id,
646            strategy_id,
647            order.instrument_id(),
648            order.client_order_id(),
649            order.venue_order_id(),
650            command_id,
651            ts_init,
652            params,
653            None, // correlation_id
654        );
655
656        if order.emulation_trigger().is_some() || order.is_emulated() {
657            send_emulator_command(TradingCommand::CancelOrder(command));
658        } else if let Some(algo_id) = order
659            .exec_algorithm_id()
660            .filter(|_| order.is_active_local())
661        {
662            let endpoint = format!("{algo_id}.execute");
663            msgbus::send_any(endpoint.into(), &TradingCommand::CancelOrder(command));
664        } else {
665            send_exec_command(TradingCommand::CancelOrder(command));
666        }
667
668        if StrategyNative::strategy_core(self).config.manage_gtd_expiry
669            && order.time_in_force() == TimeInForce::Gtd
670            && StrategyNative::strategy_core(self)
671                .cache_ref()
672                .is_order_pending_cancel_local(&order.client_order_id())
673            && self.has_gtd_expiry_timer(&order.client_order_id())
674        {
675            self.cancel_gtd_expiry(&order.client_order_id());
676        }
677
678        Ok(())
679    }
680
681    /// Batch cancels multiple orders for the same instrument.
682    ///
683    /// # Errors
684    ///
685    /// Returns an error if the strategy is not registered, the orders span multiple instruments,
686    /// or contain emulated/local orders.
687    fn cancel_orders(
688        &mut self,
689        client_order_ids: Vec<ClientOrderId>,
690        client_id: Option<ClientId>,
691        params: Option<Params>,
692    ) -> anyhow::Result<()>
693    where
694        Self: StrategyNative,
695    {
696        if client_order_ids.is_empty() {
697            anyhow::bail!("Cannot batch cancel empty order list");
698        }
699
700        let (trader_id, strategy_id, ts_init) = {
701            let core = StrategyNative::strategy_core_mut(self);
702            (
703                registered_trader_id(core)?,
704                registered_strategy_id(core)?,
705                core.clock_mut().timestamp_ns(),
706            )
707        };
708
709        // TODO: Snapshot all orders from the cache. See `cancel_order` for the rationale.
710        let orders: Vec<OrderAny> = {
711            let cache_rc = StrategyNative::strategy_core_mut(self).cache_rc();
712            let cache = cache_rc.borrow();
713            client_order_ids
714                .iter()
715                .map(|id| {
716                    cache
717                        .try_order_owned(id)
718                        .map_err(|e| anyhow::anyhow!("Cannot cancel order: {e}"))
719                })
720                .collect::<Result<_, _>>()?
721        };
722
723        let instrument_id = orders[0].instrument_id();
724
725        for order in &orders {
726            if order.instrument_id() != instrument_id {
727                anyhow::bail!(
728                    "Cannot batch cancel orders for different instruments: {} vs {}",
729                    instrument_id,
730                    order.instrument_id()
731                );
732            }
733
734            if order.is_emulated() || order.is_active_local() {
735                anyhow::bail!("Cannot include emulated or local orders in batch cancel");
736            }
737        }
738
739        let mut cancels = Vec::with_capacity(orders.len());
740
741        for order in orders {
742            let command_id = UUID4::new();
743            if !self.mark_order_pending_cancel(&order, command_id)? {
744                continue;
745            }
746
747            cancels.push(CancelOrder::new(
748                trader_id,
749                client_id,
750                strategy_id,
751                instrument_id,
752                order.client_order_id(),
753                order.venue_order_id(),
754                command_id,
755                ts_init,
756                params.clone(),
757                None, // correlation_id
758            ));
759        }
760
761        if cancels.is_empty() {
762            log::warn!("Cannot send `BatchCancelOrders`, no valid cancel commands");
763            return Ok(());
764        }
765
766        let command = BatchCancelOrders::new(
767            trader_id,
768            client_id,
769            strategy_id,
770            instrument_id,
771            cancels,
772            UUID4::new(),
773            ts_init,
774            params,
775            None, // correlation_id
776        );
777
778        send_exec_command(TradingCommand::CancelOrders(command));
779        Ok(())
780    }
781
782    /// Marks an order as pending update locally before the modify command leaves the strategy.
783    ///
784    /// # Errors
785    ///
786    /// Returns an error if applying the pending update event to the cache fails.
787    fn mark_order_pending_update(
788        &mut self,
789        order: &OrderAny,
790        command_id: UUID4,
791    ) -> anyhow::Result<bool>
792    where
793        Self: StrategyNative,
794    {
795        if order.is_active_local() {
796            return Ok(true);
797        }
798
799        let strategy_id = order.strategy_id();
800        required_account_id(order, "pending update")?;
801        let event =
802            OrderEventAny::PendingUpdate(self.generate_order_pending_update(order, command_id));
803
804        {
805            let cache_rc = StrategyNative::strategy_core_mut(self).cache_rc();
806            let mut cache = cache_rc.borrow_mut();
807            match cache.update_order(&event) {
808                Ok(_) => {}
809                Err(e)
810                    if matches!(
811                        e.downcast_ref::<OrderError>(),
812                        Some(OrderError::InvalidStateTransition)
813                    ) =>
814                {
815                    log::warn!("InvalidStateTrigger: {e}, did not apply pending update event");
816                    return Ok(false);
817                }
818                Err(e) => return Err(e),
819            }
820        }
821
822        let topic = format!("events.order.{strategy_id}");
823        msgbus::publish_order_event(topic.into(), &event);
824        msgbus::publish_order_event(
825            msgbus::switchboard::get_order_pending_update_topic(order.instrument_id()),
826            &event,
827        );
828
829        Ok(true)
830    }
831
832    /// Marks an order as pending cancel locally before the cancel command leaves the strategy.
833    ///
834    /// # Errors
835    ///
836    /// Returns an error if applying the pending cancel event to the cache fails.
837    fn mark_order_pending_cancel(
838        &mut self,
839        order: &OrderAny,
840        command_id: UUID4,
841    ) -> anyhow::Result<bool>
842    where
843        Self: StrategyNative,
844    {
845        if order.is_closed() || order.is_pending_cancel() {
846            log::warn!(
847                "Cannot cancel order: state is {:?}, {order:?}",
848                order.status()
849            );
850            return Ok(false);
851        }
852
853        if order.is_active_local() {
854            return Ok(true);
855        }
856
857        let strategy_id = order.strategy_id();
858        required_account_id(order, "pending cancel")?;
859        let event =
860            OrderEventAny::PendingCancel(self.generate_order_pending_cancel(order, command_id));
861
862        {
863            let cache_rc = StrategyNative::strategy_core_mut(self).cache_rc();
864            let mut cache = cache_rc.borrow_mut();
865            match cache.update_order(&event) {
866                Ok(_) => {}
867                Err(e)
868                    if matches!(
869                        e.downcast_ref::<OrderError>(),
870                        Some(OrderError::InvalidStateTransition)
871                    ) =>
872                {
873                    log::warn!("InvalidStateTrigger: {e}, did not apply pending cancel event");
874                    return Ok(false);
875                }
876                Err(e) => return Err(e),
877            }
878            cache.update_order_pending_cancel_local(order);
879        }
880
881        let topic = format!("events.order.{strategy_id}");
882        msgbus::publish_order_event(topic.into(), &event);
883        msgbus::publish_order_event(
884            msgbus::switchboard::get_order_pending_cancel_topic(order.instrument_id()),
885            &event,
886        );
887
888        Ok(true)
889    }
890
891    /// Generates an `OrderPendingUpdate` event for an order.
892    fn generate_order_pending_update(
893        &mut self,
894        order: &OrderAny,
895        command_id: UUID4,
896    ) -> OrderPendingUpdate
897    where
898        Self: StrategyNative,
899    {
900        let ts_now = StrategyNative::strategy_core_mut(self)
901            .clock_mut()
902            .timestamp_ns();
903
904        let mut event = OrderPendingUpdate::new(
905            order.trader_id(),
906            order.strategy_id(),
907            order.instrument_id(),
908            order.client_order_id(),
909            order.account_id(),
910            UUID4::new(),
911            ts_now,
912            ts_now,
913            false,
914            order.venue_order_id(),
915        );
916        event.causation_id = Some(command_id);
917        event
918    }
919
920    /// Generates an `OrderPendingCancel` event for an order.
921    fn generate_order_pending_cancel(
922        &mut self,
923        order: &OrderAny,
924        command_id: UUID4,
925    ) -> OrderPendingCancel
926    where
927        Self: StrategyNative,
928    {
929        let ts_now = StrategyNative::strategy_core_mut(self)
930            .clock_mut()
931            .timestamp_ns();
932
933        let mut event = OrderPendingCancel::new(
934            order.trader_id(),
935            order.strategy_id(),
936            order.instrument_id(),
937            order.client_order_id(),
938            order.account_id(),
939            UUID4::new(),
940            ts_now,
941            ts_now,
942            false,
943            order.venue_order_id(),
944        );
945        event.causation_id = Some(command_id);
946        event
947    }
948
949    /// Cancels all open orders for the given instrument.
950    ///
951    /// When `strategy_only` is `true`, only orders associated with this strategy are canceled. When
952    /// `false`, one [`CancelAllOrders`] command is sent even when the cache has no matching order.
953    /// The execution engine selects the explicit, venue-routed, or default client and may cancel
954    /// orders associated with other strategies for the same instrument, client, and account. It
955    /// does not broadcast across execution clients.
956    ///
957    /// # Errors
958    ///
959    /// Returns an error if the strategy is not registered or order cancellation fails.
960    fn cancel_all_orders(
961        &mut self,
962        instrument_id: InstrumentId,
963        order_side: Option<OrderSide>,
964        client_id: Option<ClientId>,
965        strategy_only: bool,
966        params: Option<Params>,
967    ) -> anyhow::Result<()>
968    where
969        Self: StrategyNative,
970    {
971        let params = params.filter(|params| !params.is_empty());
972        let core = StrategyNative::strategy_core_mut(self);
973
974        let trader_id = registered_trader_id(core)?;
975        let strategy_id = registered_strategy_id(core)?;
976        let ts_init = core.clock_mut().timestamp_ns();
977
978        if !strategy_only {
979            let command_id = UUID4::new();
980            let command = CancelAllOrders::new(
981                trader_id,
982                client_id,
983                strategy_id,
984                instrument_id,
985                order_side,
986                command_id,
987                ts_init,
988                params,
989                Some(command_id),
990            );
991
992            send_exec_command(TradingCommand::CancelAllOrders(command));
993            return Ok(());
994        }
995
996        let cache = core.cache_ref();
997
998        let mut open_order_ids: Vec<ClientOrderId> = cache
999            .orders_open(
1000                None,
1001                Some(&instrument_id),
1002                Some(&strategy_id),
1003                None,
1004                order_side,
1005            )
1006            .into_iter()
1007            .map(|order| order.client_order_id())
1008            .collect();
1009
1010        let mut emulated_order_ids: Vec<ClientOrderId> = cache
1011            .orders_emulated(
1012                None,
1013                Some(&instrument_id),
1014                Some(&strategy_id),
1015                None,
1016                order_side,
1017            )
1018            .into_iter()
1019            .map(|order| order.client_order_id())
1020            .collect();
1021
1022        let mut inflight_order_ids: Vec<ClientOrderId> = cache
1023            .orders_inflight(
1024                None,
1025                Some(&instrument_id),
1026                Some(&strategy_id),
1027                None,
1028                order_side,
1029            )
1030            .into_iter()
1031            .map(|order| order.client_order_id())
1032            .collect();
1033
1034        // An order submitted earlier in the same handler stays local until its queued
1035        // command is processed, so it is not yet open or in flight
1036        let mut local_order_ids: Vec<ClientOrderId> = cache
1037            .orders_active_local(
1038                None,
1039                Some(&instrument_id),
1040                Some(&strategy_id),
1041                None,
1042                order_side,
1043            )
1044            .into_iter()
1045            .map(|order| order.client_order_id())
1046            .collect();
1047
1048        // Sort the algorithm IDs so the per-algo cancel cascade fires msgbus
1049        // events in a deterministic order across runs; the cache returns an
1050        // unordered AHashSet.
1051        let mut exec_algorithm_ids: Vec<_> = cache.exec_algorithm_ids().into_iter().collect();
1052        exec_algorithm_ids.sort();
1053        let mut algo_order_ids: Vec<ClientOrderId> = Vec::new();
1054
1055        for algo_id in &exec_algorithm_ids {
1056            algo_order_ids.extend(
1057                cache
1058                    .orders_for_exec_algorithm(
1059                        algo_id,
1060                        None,
1061                        Some(&instrument_id),
1062                        Some(&strategy_id),
1063                        None,
1064                        order_side,
1065                    )
1066                    .into_iter()
1067                    .map(|order| order.client_order_id()),
1068            );
1069        }
1070
1071        let matches_client = |client_order_id: &ClientOrderId| {
1072            client_id.is_none_or(|client_id| {
1073                cache
1074                    .client_id(client_order_id)
1075                    .is_none_or(|order_client_id| *order_client_id == client_id)
1076            })
1077        };
1078
1079        open_order_ids.retain(&matches_client);
1080        emulated_order_ids.retain(&matches_client);
1081        inflight_order_ids.retain(&matches_client);
1082        local_order_ids.retain(&matches_client);
1083        algo_order_ids.retain(&matches_client);
1084
1085        let open_count = open_order_ids.len();
1086        let emulated_count = emulated_order_ids.len();
1087        let inflight_count = inflight_order_ids.len();
1088        let local_count = local_order_ids.len();
1089        let algo_count = algo_order_ids.len();
1090
1091        let mut cancel_routes: Vec<_> = open_order_ids
1092            .iter()
1093            .chain(&emulated_order_ids)
1094            .chain(&inflight_order_ids)
1095            .chain(&local_order_ids)
1096            .chain(&algo_order_ids)
1097            .map(|client_order_id| {
1098                (
1099                    *client_order_id,
1100                    client_id.or_else(|| cache.client_id(client_order_id).copied()),
1101                )
1102            })
1103            .collect();
1104        cancel_routes.sort_by_key(|(client_order_id, _)| *client_order_id);
1105        cancel_routes.dedup_by_key(|(client_order_id, _)| *client_order_id);
1106
1107        drop(cache);
1108
1109        if open_count == 0
1110            && emulated_count == 0
1111            && inflight_count == 0
1112            && local_count == 0
1113            && algo_count == 0
1114        {
1115            let side_str = order_side.map(|s| format!(" {s}")).unwrap_or_default();
1116            log::info!("No {instrument_id} open, emulated, or inflight{side_str} orders to cancel");
1117            return Ok(());
1118        }
1119
1120        let side_str = order_side.map(|s| format!(" {s}")).unwrap_or_default();
1121
1122        if open_count > 0 {
1123            log::info!(
1124                "Canceling {open_count} open{side_str} {instrument_id} order{}",
1125                if open_count == 1 { "" } else { "s" }
1126            );
1127        }
1128
1129        if emulated_count > 0 {
1130            log::info!(
1131                "Canceling {emulated_count} emulated{side_str} {instrument_id} order{}",
1132                if emulated_count == 1 { "" } else { "s" }
1133            );
1134        }
1135
1136        if inflight_count > 0 {
1137            log::info!(
1138                "Canceling {inflight_count} inflight{side_str} {instrument_id} order{}",
1139                if inflight_count == 1 { "" } else { "s" }
1140            );
1141        }
1142
1143        let mut error = None;
1144
1145        for (client_order_id, client_id) in cancel_routes {
1146            if let Err(e) = self.cancel_order(client_order_id, client_id, params.clone()) {
1147                error = Some(match error {
1148                    Some(previous) => anyhow::anyhow!("{previous}; {e}"),
1149                    None => e,
1150                });
1151            }
1152        }
1153
1154        error.map_or(Ok(()), Err)
1155    }
1156
1157    /// Closes a position by submitting a market order for the opposite side.
1158    ///
1159    /// Quantities round down to a multiple of the instrument's size increment.
1160    /// Any remaining dust stays in the position; no order is submitted for dust alone.
1161    ///
1162    /// # Errors
1163    ///
1164    /// Returns an error if the strategy is not registered, a required instrument is not cached,
1165    /// or position closing fails.
1166    #[expect(clippy::too_many_arguments)]
1167    fn close_position(
1168        &mut self,
1169        position: &Position,
1170        client_id: Option<ClientId>,
1171        tags: Option<Vec<Ustr>>,
1172        time_in_force: Option<TimeInForce>,
1173        reduce_only: Option<bool>,
1174        quote_quantity: Option<bool>,
1175        params: Option<Params>,
1176    ) -> anyhow::Result<()>
1177    where
1178        Self: StrategyNative,
1179    {
1180        let core = StrategyNative::strategy_core_mut(self);
1181
1182        if position.is_closed() {
1183            log::warn!("Cannot close position (already closed): {}", position.id);
1184            return Ok(());
1185        }
1186
1187        let Some(closing_side) = OrderCore::closing_side(position.side) else {
1188            log::warn!("Cannot close flat position: {}", position.id);
1189            return Ok(());
1190        };
1191
1192        let Some(quantity) = closing_qty(core, position.instrument_id, position.quantity)? else {
1193            return Ok(());
1194        };
1195
1196        let order = core.order_factory().market(
1197            position.instrument_id,
1198            closing_side,
1199            quantity,
1200            time_in_force,
1201            reduce_only.or(Some(true)),
1202            quote_quantity,
1203            None,
1204            None,
1205            tags,
1206            None,
1207        );
1208
1209        self.submit_order(order, Some(position.id), client_id, params)
1210    }
1211
1212    /// Closes all open positions for the given instrument.
1213    ///
1214    /// Quantities round down to a multiple of the instrument's size increment.
1215    /// Positions with only dust remain open without an order.
1216    ///
1217    /// # Errors
1218    ///
1219    /// Returns an error if the strategy is not registered, a required instrument is not cached,
1220    /// or position closing fails.
1221    #[expect(clippy::too_many_arguments)]
1222    fn close_all_positions(
1223        &mut self,
1224        instrument_id: InstrumentId,
1225        position_side: Option<PositionSide>,
1226        client_id: Option<ClientId>,
1227        tags: Option<Vec<Ustr>>,
1228        time_in_force: Option<TimeInForce>,
1229        reduce_only: Option<bool>,
1230        quote_quantity: Option<bool>,
1231        params: Option<Params>,
1232    ) -> anyhow::Result<()>
1233    where
1234        Self: StrategyNative,
1235    {
1236        let core = StrategyNative::strategy_core_mut(self);
1237        let strategy_id = registered_strategy_id(core)?;
1238        let cache = core.cache_ref();
1239
1240        let positions_open = cache.positions_open(
1241            None,
1242            Some(&instrument_id),
1243            Some(&strategy_id),
1244            None,
1245            position_side,
1246        );
1247
1248        let side_str = position_side.map(|s| format!(" {s}")).unwrap_or_default();
1249
1250        if positions_open.is_empty() {
1251            log::info!("No {instrument_id} open{side_str} positions to close");
1252            return Ok(());
1253        }
1254
1255        let count = positions_open.len();
1256        log::info!(
1257            "Closing {count} open{side_str} position{}",
1258            if count == 1 { "" } else { "s" }
1259        );
1260
1261        let positions_data: Vec<_> = positions_open
1262            .iter()
1263            .map(|p| (p.id, p.instrument_id, p.side, p.quantity, p.is_closed()))
1264            .collect();
1265        drop(positions_open);
1266
1267        drop(cache);
1268
1269        for (pos_id, pos_instrument_id, pos_side, pos_quantity, is_closed) in positions_data {
1270            if is_closed {
1271                continue;
1272            }
1273
1274            let core = StrategyNative::strategy_core_mut(self);
1275            let Some(closing_side) = OrderCore::closing_side(pos_side) else {
1276                continue;
1277            };
1278
1279            let Some(quantity) = closing_qty(core, pos_instrument_id, pos_quantity)? else {
1280                continue;
1281            };
1282
1283            let order = core.order_factory().market(
1284                pos_instrument_id,
1285                closing_side,
1286                quantity,
1287                time_in_force,
1288                reduce_only.or(Some(true)),
1289                quote_quantity,
1290                None,
1291                None,
1292                tags.clone(),
1293                None,
1294            );
1295
1296            self.submit_order(order, Some(pos_id), client_id, params.clone())?;
1297        }
1298
1299        Ok(())
1300    }
1301
1302    /// Queries account state from the execution client.
1303    ///
1304    /// Creates a [`QueryAccount`] command and sends it to the execution engine,
1305    /// which will request the current account state from the execution client.
1306    ///
1307    /// # Errors
1308    ///
1309    /// Returns an error if the strategy is not registered.
1310    fn query_account(
1311        &mut self,
1312        account_id: AccountId,
1313        client_id: Option<ClientId>,
1314        params: Option<Params>,
1315    ) -> anyhow::Result<()>
1316    where
1317        Self: StrategyNative,
1318    {
1319        let core = StrategyNative::strategy_core_mut(self);
1320
1321        let trader_id = registered_trader_id(core)?;
1322        let ts_init = core.clock_mut().timestamp_ns();
1323
1324        let command = QueryAccount::new(
1325            trader_id,
1326            client_id,
1327            account_id,
1328            UUID4::new(),
1329            ts_init,
1330            params,
1331            None, // correlation_id
1332        );
1333
1334        send_exec_command(TradingCommand::QueryAccount(command));
1335        Ok(())
1336    }
1337
1338    /// Queries order state from the execution client.
1339    ///
1340    /// Creates a [`QueryOrder`] command and sends it to the execution engine,
1341    /// which will request the current order state from the execution client.
1342    ///
1343    /// # Errors
1344    ///
1345    /// Returns an error if the strategy is not registered.
1346    fn query_order(
1347        &mut self,
1348        order: &OrderAny,
1349        client_id: Option<ClientId>,
1350        params: Option<Params>,
1351    ) -> anyhow::Result<()>
1352    where
1353        Self: StrategyNative,
1354    {
1355        let core = StrategyNative::strategy_core_mut(self);
1356
1357        let trader_id = registered_trader_id(core)?;
1358        let strategy_id = registered_strategy_id(core)?;
1359        let ts_init = core.clock_mut().timestamp_ns();
1360
1361        let command = QueryOrder::new(
1362            trader_id,
1363            client_id,
1364            strategy_id,
1365            order.instrument_id(),
1366            order.client_order_id(),
1367            order.venue_order_id(),
1368            UUID4::new(),
1369            ts_init,
1370            params,
1371            None, // correlation_id
1372        );
1373
1374        send_exec_command(TradingCommand::QueryOrder(command));
1375        Ok(())
1376    }
1377
1378    /// Handles an order event, dispatching to the appropriate handler.
1379    fn handle_order_event(&mut self, event: OrderEventAny)
1380    where
1381        Self: StrategyNative,
1382    {
1383        let state = {
1384            let core = StrategyNative::strategy_core_mut(self);
1385            let id = &core.actor.actor_id;
1386
1387            let is_warning = match &event {
1388                OrderEventAny::Rejected(event) => {
1389                    !event.due_post_only || core.config.log_rejected_due_post_only_as_warning
1390                }
1391                OrderEventAny::Denied(_)
1392                | OrderEventAny::ModifyDenied(_)
1393                | OrderEventAny::CancelDenied(_)
1394                | OrderEventAny::CancelRejected(_)
1395                | OrderEventAny::ModifyRejected(_) => true,
1396                _ => false,
1397            };
1398
1399            if is_warning {
1400                log::warn!("{id} {RECV}{EVT} {event}");
1401            } else if core.actor.config.log_events {
1402                log::info!("{id} {RECV}{EVT} {event}");
1403            }
1404
1405            core.actor.state()
1406        };
1407
1408        let client_order_id = event.client_order_id();
1409        let cached_order_is_closed = {
1410            let core = StrategyNative::strategy_core_mut(self);
1411            core.cache_ref()
1412                .order(&client_order_id)
1413                .map(|order| order.is_closed())
1414        };
1415        let is_terminal = match &event {
1416            OrderEventAny::FillVoided(event) => {
1417                cached_order_is_closed.unwrap_or(!event.is_reopened)
1418            }
1419            OrderEventAny::Filled(_) => cached_order_is_closed.unwrap_or(true),
1420            OrderEventAny::Canceled(_)
1421            | OrderEventAny::Rejected(_)
1422            | OrderEventAny::Expired(_)
1423            | OrderEventAny::Denied(_) => true,
1424            _ => false,
1425        };
1426
1427        // GTD timer cleanup runs regardless of state so timers do not leak when
1428        // terminal events arrive during the post-stop delay.
1429        if is_terminal {
1430            self.cancel_gtd_expiry(&client_order_id);
1431        }
1432
1433        // Events are logged unconditionally so residual events received after stop
1434        // remain observable, but dispatch is gated on the running state.
1435        if state != ComponentState::Running {
1436            return;
1437        }
1438
1439        restore_gtd_timer(self, &event);
1440
1441        let manager_actions = {
1442            let core = StrategyNative::strategy_core_mut(self);
1443            if core.config.manage_contingent_orders {
1444                core.order_manager
1445                    .as_mut()
1446                    .map_or_else(Vec::new, |manager| manager.handle_event(&event))
1447            } else {
1448                Vec::new()
1449            }
1450        };
1451        self.dispatch_manager_actions(manager_actions);
1452
1453        match &event {
1454            OrderEventAny::Initialized(e) => self.on_order_initialized(e.clone()),
1455            OrderEventAny::Denied(e) => self.on_order_denied(*e),
1456            OrderEventAny::Emulated(e) => self.on_order_emulated(*e),
1457            OrderEventAny::Released(e) => self.on_order_released(*e),
1458            OrderEventAny::Submitted(e) => self.on_order_submitted(*e),
1459            OrderEventAny::Rejected(e) => self.on_order_rejected(*e),
1460            OrderEventAny::Accepted(e) => self.on_order_accepted(*e),
1461            OrderEventAny::Canceled(e) => self.on_order_canceled(e),
1462            OrderEventAny::Expired(e) => self.on_order_expired(*e),
1463            OrderEventAny::Triggered(e) => self.on_order_triggered(*e),
1464            OrderEventAny::PendingUpdate(e) => self.on_order_pending_update(*e),
1465            OrderEventAny::PendingCancel(e) => self.on_order_pending_cancel(*e),
1466            OrderEventAny::ModifyDenied(e) => self.on_order_modify_denied(*e),
1467            OrderEventAny::ModifyRejected(e) => self.on_order_modify_rejected(*e),
1468            OrderEventAny::CancelDenied(e) => self.on_order_cancel_denied(*e),
1469            OrderEventAny::CancelRejected(e) => self.on_order_cancel_rejected(*e),
1470            OrderEventAny::Updated(e) => self.on_order_updated(*e),
1471            OrderEventAny::Filled(e) => self.on_order_filled(e),
1472            OrderEventAny::FillVoided(e) => self.on_order_fill_voided(e),
1473        }
1474        self.on_order_event(event);
1475    }
1476
1477    fn dispatch_manager_actions(&mut self, actions: Vec<OrderManagerAction>)
1478    where
1479        Self: StrategyNative,
1480    {
1481        for action in actions {
1482            match action {
1483                OrderManagerAction::PublishInitialized(event) => {
1484                    let topic = msgbus::switchboard::get_event_order_topic(event.strategy_id());
1485                    msgbus::publish_order_event(topic, &event);
1486                }
1487                OrderManagerAction::SubmitToEmulator(command) => {
1488                    send_emulator_command(TradingCommand::SubmitOrder(command));
1489                }
1490                OrderManagerAction::SubmitToRisk(command) => {
1491                    send_risk_command(TradingCommand::SubmitOrder(command));
1492                }
1493                OrderManagerAction::SubmitToAlgorithm {
1494                    command,
1495                    exec_algorithm_id,
1496                } => send_algo_command(command, exec_algorithm_id),
1497                OrderManagerAction::CancelLocal(order) => {
1498                    let client_order_id = order.client_order_id();
1499                    if let Err(e) = self.cancel_order(client_order_id, None, None) {
1500                        log::error!(
1501                            "Failed to dispatch contingent cancel for {client_order_id}: {e}"
1502                        );
1503                    }
1504                }
1505                OrderManagerAction::ModifyLocalQuantity { order, quantity } => {
1506                    let client_order_id = order.client_order_id();
1507                    if let Err(e) =
1508                        self.modify_order(client_order_id, Some(quantity), None, None, None, None)
1509                    {
1510                        log::error!(
1511                            "Failed to dispatch contingent modify for {client_order_id}: {e}"
1512                        );
1513                    }
1514                }
1515            }
1516        }
1517    }
1518
1519    /// Handles a position event, dispatching to the appropriate handler.
1520    fn handle_position_event(&mut self, event: PositionEvent)
1521    where
1522        Self: StrategyNative,
1523    {
1524        let state = {
1525            let core = StrategyNative::strategy_core_mut(self);
1526
1527            if core.actor.config.log_events {
1528                let id = &core.actor.actor_id;
1529                log::info!("{id} {RECV}{EVT} {event:?}");
1530            }
1531
1532            core.actor.state()
1533        };
1534
1535        if state != ComponentState::Running {
1536            return;
1537        }
1538
1539        match &event {
1540            PositionEvent::PositionOpened(e) => self.on_position_opened(e.clone()),
1541            PositionEvent::PositionChanged(e) => self.on_position_changed(e.clone()),
1542            PositionEvent::PositionClosed(e) => self.on_position_closed(e.clone()),
1543            PositionEvent::PositionAdjusted(_) => {
1544                return;
1545            }
1546        }
1547        self.on_position_event(event);
1548    }
1549
1550    // -- LIFECYCLE METHODS -----------------------------------------------------------------------
1551
1552    /// Called when the strategy is started.
1553    ///
1554    /// Override this method to implement custom initialization logic.
1555    /// The default implementation reactivates GTD timers if `manage_gtd_expiry` is enabled.
1556    ///
1557    /// # Errors
1558    ///
1559    /// Returns an error if strategy initialization fails.
1560    fn on_start(&mut self) -> anyhow::Result<()>
1561    where
1562        Self: StrategyNative,
1563    {
1564        let core = StrategyNative::strategy_core_mut(self);
1565        let strategy_id = registered_strategy_id(core)?;
1566        log::info!("Starting {strategy_id}");
1567
1568        if core.config.manage_gtd_expiry {
1569            self.reactivate_gtd_timers();
1570        }
1571
1572        Ok(())
1573    }
1574
1575    /// Routes a time event through the framework-managed strategy handlers when called directly.
1576    ///
1577    /// Framework timer dispatch never calls this method. Implement [`DataActor::on_time_event`] for
1578    /// user timer callbacks. Runtime hosts call [`route_time_event`] directly so an override cannot
1579    /// replace framework-managed GTD expiry or market exit routing.
1580    ///
1581    /// # Errors
1582    ///
1583    /// Returns an error if time event handling fails.
1584    fn on_time_event(&mut self, event: &TimeEvent) -> anyhow::Result<()>
1585    where
1586        Self: StrategyNative + Component,
1587    {
1588        route_time_event(self, event);
1589        Ok(())
1590    }
1591
1592    // -- EVENT HANDLERS --------------------------------------------------------------------------
1593
1594    /// Called when an order is initialized.
1595    ///
1596    /// Override this method to implement custom logic when an order is first created.
1597    #[allow(unused_variables)]
1598    fn on_order_initialized(&mut self, event: OrderInitialized) {}
1599
1600    /// Called when any order event is received after the specific order handler runs.
1601    ///
1602    /// Override this method to implement custom logic for all order events.
1603    #[allow(unused_variables)]
1604    fn on_order_event(&mut self, event: OrderEventAny) {}
1605
1606    /// Called when an order is denied by the system.
1607    ///
1608    /// Override this method to implement custom logic when an order is denied before submission.
1609    #[allow(unused_variables)]
1610    fn on_order_denied(&mut self, event: OrderDenied) {}
1611
1612    /// Called when an order is emulated.
1613    ///
1614    /// Override this method to implement custom logic when an order is taken over by the emulator.
1615    #[allow(unused_variables)]
1616    fn on_order_emulated(&mut self, event: OrderEmulated) {}
1617
1618    /// Called when an order is released from emulation.
1619    ///
1620    /// Override this method to implement custom logic when an emulated order is released.
1621    #[allow(unused_variables)]
1622    fn on_order_released(&mut self, event: OrderReleased) {}
1623
1624    /// Called when an order is submitted to the venue.
1625    ///
1626    /// Override this method to implement custom logic when an order is submitted.
1627    #[allow(unused_variables)]
1628    fn on_order_submitted(&mut self, event: OrderSubmitted) {}
1629
1630    /// Called when an order is rejected by the venue.
1631    ///
1632    /// Override this method to implement custom logic when an order is rejected.
1633    #[allow(unused_variables)]
1634    fn on_order_rejected(&mut self, event: OrderRejected) {}
1635
1636    /// Called when an order is accepted by the venue.
1637    ///
1638    /// Override this method to implement custom logic when an order is accepted.
1639    #[allow(unused_variables)]
1640    fn on_order_accepted(&mut self, event: OrderAccepted) {}
1641
1642    /// Called when an order expires.
1643    ///
1644    /// Override this method to implement custom logic when an order expires.
1645    #[allow(unused_variables)]
1646    fn on_order_expired(&mut self, event: OrderExpired) {}
1647
1648    /// Called when an order is triggered.
1649    ///
1650    /// Override this method to implement custom logic when a stop or conditional order is triggered.
1651    #[allow(unused_variables)]
1652    fn on_order_triggered(&mut self, event: OrderTriggered) {}
1653
1654    /// Called when an order modification is pending.
1655    ///
1656    /// Override this method to implement custom logic when an order is pending modification.
1657    #[allow(unused_variables)]
1658    fn on_order_pending_update(&mut self, event: OrderPendingUpdate) {}
1659
1660    /// Called when an order cancellation is pending.
1661    ///
1662    /// Override this method to implement custom logic when an order is pending cancellation.
1663    #[allow(unused_variables)]
1664    fn on_order_pending_cancel(&mut self, event: OrderPendingCancel) {}
1665
1666    /// Called when a modify command is proven not sent; the order remains otherwise unchanged.
1667    #[allow(unused_variables)]
1668    fn on_order_modify_denied(&mut self, event: OrderModifyDenied) {}
1669
1670    /// Called when an order modification is rejected.
1671    ///
1672    /// Override this method to implement custom logic when an order modification is rejected.
1673    #[allow(unused_variables)]
1674    fn on_order_modify_rejected(&mut self, event: OrderModifyRejected) {}
1675
1676    /// Called when a cancel command is proven not sent; the order remains otherwise unchanged.
1677    #[allow(unused_variables)]
1678    fn on_order_cancel_denied(&mut self, event: OrderCancelDenied) {}
1679
1680    /// Called when an order cancellation is rejected.
1681    ///
1682    /// Override this method to implement custom logic when an order cancellation is rejected.
1683    #[allow(unused_variables)]
1684    fn on_order_cancel_rejected(&mut self, event: OrderCancelRejected) {}
1685
1686    /// Called when an order is updated.
1687    ///
1688    /// Override this method to implement custom logic when an order is modified.
1689    #[allow(unused_variables)]
1690    fn on_order_updated(&mut self, event: OrderUpdated) {}
1691
1692    /// Called when an order is canceled.
1693    ///
1694    /// Override this method to implement custom logic when an order is canceled.
1695    #[allow(unused_variables)]
1696    fn on_order_canceled(&mut self, event: &OrderCanceled) {}
1697
1698    /// Called when an order is filled.
1699    ///
1700    /// Override this method to implement custom logic when an order is filled.
1701    #[allow(unused_variables)]
1702    fn on_order_filled(&mut self, event: &OrderFilled) {}
1703
1704    /// Called when an applied order fill is partly or fully voided.
1705    #[allow(unused_variables)]
1706    fn on_order_fill_voided(&mut self, event: &OrderFillVoided) {}
1707
1708    /// Called when a position is opened.
1709    ///
1710    /// Override this method to implement custom logic when a position is opened.
1711    #[allow(unused_variables)]
1712    fn on_position_opened(&mut self, event: PositionOpened) {}
1713
1714    /// Called after a position opened, changed, or closed handler runs.
1715    ///
1716    /// Override this method to implement custom logic for all position events.
1717    #[allow(unused_variables)]
1718    fn on_position_event(&mut self, event: PositionEvent) {}
1719
1720    /// Called when a position is changed (quantity or price updated).
1721    ///
1722    /// Override this method to implement custom logic when a position changes.
1723    #[allow(unused_variables)]
1724    fn on_position_changed(&mut self, event: PositionChanged) {}
1725
1726    /// Called when a position is closed.
1727    ///
1728    /// Override this method to implement custom logic when a position is closed.
1729    #[allow(unused_variables)]
1730    fn on_position_closed(&mut self, event: PositionClosed) {}
1731
1732    /// Called when a market exit has been initiated.
1733    ///
1734    /// Override this method to implement custom logic when a market exit begins.
1735    fn on_market_exit(&mut self) {}
1736
1737    /// Called after a market exit has completed.
1738    ///
1739    /// Override this method to implement custom logic after a market exit completes.
1740    fn post_market_exit(&mut self) {}
1741
1742    /// Returns whether the strategy is currently executing a market exit.
1743    ///
1744    /// Strategies can check this to avoid submitting new orders during exit.
1745    fn is_exiting(&self) -> bool
1746    where
1747        Self: StrategyNative,
1748    {
1749        StrategyNative::strategy_core(self).is_exiting
1750    }
1751
1752    /// Initiates an iterative market exit for the strategy.
1753    ///
1754    /// Cancels open, in-flight, emulated, and active-local orders and closes open positions.
1755    /// Waits for orders to resolve and positions to close, up to `market_exit_max_attempts`.
1756    /// The strategy remains running after the exit completes.
1757    ///
1758    /// The `on_market_exit` hook is called when the exit process begins.
1759    /// The `post_market_exit` hook is called when the exit process completes.
1760    ///
1761    /// Uses `market_exit_time_in_force` and `market_exit_reduce_only` from
1762    /// the strategy config for closing market orders.
1763    ///
1764    /// # Errors
1765    ///
1766    /// Returns an error if the market exit cannot be initiated.
1767    fn market_exit(&mut self) -> anyhow::Result<()>
1768    where
1769        Self: StrategyNative,
1770    {
1771        let core = StrategyNative::strategy_core_mut(self);
1772        let strategy_id = registered_strategy_id(core)?;
1773
1774        if core.actor.state() != ComponentState::Running {
1775            log::warn!("{strategy_id} Cannot market exit: strategy is not running");
1776            return Ok(());
1777        }
1778
1779        if core.is_exiting {
1780            log::warn!("{strategy_id} Market exit called when already in progress");
1781            return Ok(());
1782        }
1783
1784        core.is_exiting = true;
1785        core.market_exit_attempts = 0;
1786        let time_in_force = core.config.market_exit_time_in_force;
1787        let reduce_only = core.config.market_exit_reduce_only;
1788
1789        log::info!("{strategy_id} Initiating market exit...");
1790
1791        self.on_market_exit();
1792
1793        let core = StrategyNative::strategy_core_mut(self);
1794        let cache = core.cache_ref();
1795
1796        let mut instruments: AHashSet<InstrumentId> = AHashSet::new();
1797
1798        for client_order_id in cache
1799            .iter_client_order_ids_open(None, None, Some(&strategy_id), None)
1800            .chain(cache.iter_client_order_ids_inflight(None, None, Some(&strategy_id), None))
1801            .chain(cache.iter_client_order_ids_emulated(None, None, Some(&strategy_id), None))
1802            .chain(cache.iter_client_order_ids_active_local(None, None, Some(&strategy_id), None))
1803        {
1804            if let Some(order) = cache.order(&client_order_id) {
1805                instruments.insert(order.instrument_id());
1806            }
1807        }
1808
1809        for position_id in cache.iter_position_open_ids(None, None, Some(&strategy_id), None) {
1810            if let Some(position) = cache.position(&position_id) {
1811                instruments.insert(position.instrument_id);
1812            }
1813        }
1814
1815        let market_exit_tag = core.market_exit_tag;
1816        // Sort so the per-instrument cancel_all_orders/close_all_positions
1817        // cascade fires msgbus commands in a deterministic sequence; the
1818        // upstream dedup is AHash-backed.
1819        let mut instruments: Vec<_> = instruments.into_iter().collect();
1820        instruments.sort();
1821        drop(cache);
1822
1823        for instrument_id in instruments {
1824            if let Err(e) = self.cancel_all_orders(instrument_id, None, None, true, None) {
1825                log::error!("Error canceling orders for {instrument_id}: {e}");
1826            }
1827
1828            if let Err(e) = self.close_all_positions(
1829                instrument_id,
1830                None,
1831                None,
1832                Some(vec![market_exit_tag]),
1833                Some(time_in_force),
1834                Some(reduce_only),
1835                None,
1836                None,
1837            ) {
1838                log::error!("Error closing positions for {instrument_id}: {e}");
1839            }
1840        }
1841
1842        let core = StrategyNative::strategy_core_mut(self);
1843        let interval_ms = core.config.market_exit_interval_ms;
1844        let timer_name = core.market_exit_timer_name;
1845
1846        log::info!("{strategy_id} Setting market exit timer at {interval_ms}ms intervals");
1847
1848        let Ok(interval_ns) = DurationNanos::try_from_millis(interval_ms) else {
1849            core.is_exiting = false;
1850            core.market_exit_attempts = 0;
1851            anyhow::bail!("Market exit timer interval exceeds the nanosecond range");
1852        };
1853        let result = core.clock_mut().set_timer_ns(
1854            timer_name.as_str(),
1855            interval_ns,
1856            None,
1857            None,
1858            None,
1859            None,
1860            None,
1861        );
1862
1863        if let Err(e) = result {
1864            // Reset exit state on timer failure (caller handles pending_stop)
1865            core.is_exiting = false;
1866            core.market_exit_attempts = 0;
1867            return Err(e);
1868        }
1869
1870        Ok(())
1871    }
1872
1873    /// Checks if the market exit is complete and finalizes if so.
1874    ///
1875    /// This method is called by the market exit timer.
1876    fn check_market_exit(&mut self, _event: TimeEvent)
1877    where
1878        Self: StrategyNative + Component,
1879    {
1880        // Guard against stale timer events after cancel_market_exit
1881        if !self.is_exiting() {
1882            return;
1883        }
1884
1885        let core = StrategyNative::strategy_core_mut(self);
1886        let Some(strategy_id) = core.strategy_id() else {
1887            log::error!("Cannot check market exit: strategy_id is not set");
1888            return;
1889        };
1890
1891        core.market_exit_attempts += 1;
1892        let attempts = core.market_exit_attempts;
1893        let max_attempts = core.config.market_exit_max_attempts;
1894
1895        log::debug!(
1896            "{strategy_id} Market exit check triggered (attempt {attempts}/{max_attempts})"
1897        );
1898
1899        if attempts >= max_attempts {
1900            let cache = core.cache_ref();
1901            let open_orders_count =
1902                cache.orders_open_count(None, None, Some(&strategy_id), None, None);
1903            let inflight_orders_count =
1904                cache.orders_inflight_count(None, None, Some(&strategy_id), None, None);
1905            let emulated_orders_count =
1906                cache.orders_emulated_count(None, None, Some(&strategy_id), None, None);
1907            let local_orders_count =
1908                cache.orders_active_local_count(None, None, Some(&strategy_id), None, None);
1909            let open_positions_count =
1910                cache.positions_open_count(None, None, Some(&strategy_id), None, None);
1911
1912            drop(cache);
1913
1914            log::warn!(
1915                "{strategy_id} Market exit max attempts ({max_attempts}) reached, \
1916                completing with open orders: {open_orders_count}, \
1917                inflight orders: {inflight_orders_count}, \
1918                emulated orders: {emulated_orders_count}, \
1919                active-local orders: {local_orders_count}, \
1920                open positions: {open_positions_count}"
1921            );
1922
1923            self.finalize_market_exit();
1924            return;
1925        }
1926
1927        let cache = core.cache_ref();
1928        let has_open_orders = cache.has_orders_open(None, None, Some(&strategy_id), None, None);
1929        let has_inflight_orders =
1930            cache.has_orders_inflight(None, None, Some(&strategy_id), None, None);
1931        let has_emulated_orders =
1932            cache.has_orders_emulated(None, None, Some(&strategy_id), None, None);
1933        let has_local_orders =
1934            cache.has_orders_active_local(None, None, Some(&strategy_id), None, None);
1935
1936        if has_open_orders || has_inflight_orders || has_emulated_orders || has_local_orders {
1937            return;
1938        }
1939
1940        let mut instrument_ids: Vec<_> = cache
1941            .positions_open(None, None, Some(&strategy_id), None, None)
1942            .iter()
1943            .filter_map(|position| {
1944                // Keep failed conversions pending so close_all_positions reports the error
1945                match closing_qty(core, position.instrument_id, position.quantity) {
1946                    Ok(None) => None,
1947                    Ok(Some(_)) | Err(_) => Some(position.instrument_id),
1948                }
1949            })
1950            .collect();
1951
1952        instrument_ids.sort();
1953        instrument_ids.dedup();
1954
1955        let time_in_force = core.config.market_exit_time_in_force;
1956        let reduce_only = core.config.market_exit_reduce_only;
1957        let market_exit_tag = core.market_exit_tag;
1958        drop(cache);
1959
1960        if instrument_ids.is_empty() {
1961            self.finalize_market_exit();
1962            return;
1963        }
1964
1965        for instrument_id in instrument_ids {
1966            if let Err(e) = self.close_all_positions(
1967                instrument_id,
1968                None,
1969                None,
1970                Some(vec![market_exit_tag]),
1971                Some(time_in_force),
1972                Some(reduce_only),
1973                None,
1974                None,
1975            ) {
1976                log::error!("Error re-submitting close orders for {instrument_id}: {e}");
1977            }
1978        }
1979    }
1980
1981    /// Finalizes the market exit process.
1982    ///
1983    /// Cancels the market exit timer, resets state, calls the `post_market_exit` hook,
1984    /// and stops the strategy if a stop was pending.
1985    fn finalize_market_exit(&mut self)
1986    where
1987        Self: StrategyNative + Component,
1988    {
1989        let (actor_id, should_stop) = {
1990            let core = StrategyNative::strategy_core_mut(self);
1991            let actor_id = core.actor_id();
1992            let should_stop = core.pending_stop;
1993            (actor_id, should_stop)
1994        };
1995
1996        self.cancel_market_exit();
1997
1998        let hook_result = catch_unwind(AssertUnwindSafe(|| {
1999            self.post_market_exit();
2000        }));
2001
2002        if let Err(e) = hook_result {
2003            log::error!("{actor_id} Error in post_market_exit: {e:?}");
2004        }
2005
2006        if should_stop {
2007            log::info!("{actor_id} Market exit complete, stopping strategy");
2008
2009            if let Err(e) = Component::stop(self) {
2010                log::error!("{actor_id} Failed to stop: {e}");
2011            }
2012        }
2013
2014        let core = StrategyNative::strategy_core_mut(self);
2015        debug_assert!(
2016            !(core.pending_stop
2017                && !core.is_exiting
2018                && core.actor.state() == ComponentState::Running),
2019            "INVARIANT: stuck state after finalize_market_exit"
2020        );
2021    }
2022
2023    /// Cancels an active market exit without calling hooks.
2024    ///
2025    /// Used when `stop()` is called during an active market exit to avoid state leaks.
2026    fn cancel_market_exit(&mut self)
2027    where
2028        Self: StrategyNative,
2029    {
2030        let core = StrategyNative::strategy_core_mut(self);
2031        let timer_name = core.market_exit_timer_name;
2032
2033        if core
2034            .clock_mut()
2035            .timer_names()
2036            .contains(&timer_name.as_str())
2037        {
2038            core.clock_mut().cancel_timer(timer_name.as_str());
2039        }
2040
2041        core.is_exiting = false;
2042        core.pending_stop = false;
2043        core.market_exit_attempts = 0;
2044    }
2045
2046    /// Stops the strategy with optional managed stop behavior.
2047    ///
2048    /// If `manage_stop` is enabled in the config, the strategy will first complete
2049    /// any active market exit (or initiate one) before stopping. If `manage_stop`
2050    /// is disabled, the strategy stops immediately, cleaning up any active market
2051    /// exit state.
2052    ///
2053    /// # Returns
2054    ///
2055    /// Returns `true` if the strategy should proceed with stopping, `false` if
2056    /// the stop is being deferred until market exit completes.
2057    fn stop(&mut self) -> bool
2058    where
2059        Self: StrategyNative,
2060    {
2061        let (manage_stop, is_exiting, should_initiate_exit) = {
2062            let core = StrategyNative::strategy_core_mut(self);
2063            let actor_id = core.actor_id();
2064            let manage_stop = core.config.manage_stop;
2065            let state = core.actor.state();
2066            let pending_stop = core.pending_stop;
2067            let is_exiting = core.is_exiting;
2068
2069            if manage_stop {
2070                if state != ComponentState::Running {
2071                    return true; // Proceed with stop
2072                }
2073
2074                if pending_stop {
2075                    return false; // Already waiting for market exit
2076                }
2077
2078                core.pending_stop = true;
2079                let should_initiate_exit = !is_exiting;
2080
2081                if should_initiate_exit {
2082                    log::info!("{actor_id} Initiating market exit before stop");
2083                }
2084
2085                (manage_stop, is_exiting, should_initiate_exit)
2086            } else {
2087                (manage_stop, is_exiting, false)
2088            }
2089        };
2090
2091        if manage_stop {
2092            if should_initiate_exit && let Err(e) = self.market_exit() {
2093                log::warn!("Market exit failed during stop: {e}, proceeding with stop");
2094                StrategyNative::strategy_core_mut(self).pending_stop = false;
2095                return true;
2096            }
2097            debug_assert!(
2098                self.is_exiting(),
2099                "INVARIANT: deferring stop but not exiting"
2100            );
2101            return false; // Defer stop until market exit completes
2102        }
2103
2104        // manage_stop is false - clean up any active market exit
2105        if is_exiting {
2106            self.cancel_market_exit();
2107        }
2108
2109        true // Proceed with stop
2110    }
2111
2112    /// Denies an order by generating an `OrderDenied` event.
2113    ///
2114    /// This method creates an `OrderDenied` event, applies it to the order,
2115    /// and updates the cache.
2116    fn deny_order(&mut self, order: &OrderAny, reason: Ustr)
2117    where
2118        Self: StrategyNative,
2119    {
2120        let core = StrategyNative::strategy_core_mut(self);
2121        let Some(trader_id) = core.trader_id() else {
2122            log::error!(
2123                "Cannot deny order {}: trader_id is not set",
2124                order.client_order_id()
2125            );
2126            return;
2127        };
2128        let Some(strategy_id) = core.strategy_id() else {
2129            log::error!(
2130                "Cannot deny order {}: strategy_id is not set",
2131                order.client_order_id()
2132            );
2133            return;
2134        };
2135        let ts_now = core.clock_mut().timestamp_ns();
2136
2137        let event = OrderDenied::new(
2138            trader_id,
2139            strategy_id,
2140            order.instrument_id(),
2141            order.client_order_id(),
2142            reason,
2143            UUID4::new(),
2144            ts_now,
2145            ts_now,
2146        );
2147
2148        log::warn!(
2149            "{strategy_id} Order {} denied: {reason}",
2150            order.client_order_id()
2151        );
2152
2153        let publish_initialized = {
2154            let cache_rc = core.cache_rc();
2155            let mut cache = cache_rc.borrow_mut();
2156            if cache.order_exists(&order.client_order_id()) {
2157                false
2158            } else {
2159                match cache.add_order(order.clone(), None, None, true) {
2160                    Ok(()) => true,
2161                    Err(e) => {
2162                        log::warn!("Failed to add denied order to cache: {e}");
2163                        false
2164                    }
2165                }
2166            }
2167        };
2168
2169        if publish_initialized {
2170            publish_order_initialized(order);
2171        }
2172
2173        let event = OrderEventAny::Denied(event);
2174        let applied = {
2175            let cache_rc = core.cache_rc();
2176            let mut cache = cache_rc.borrow_mut();
2177            if let Err(e) = cache.update_order(&event) {
2178                log::warn!("Failed to apply OrderDenied event: {e}");
2179                false
2180            } else {
2181                true
2182            }
2183        };
2184
2185        if applied {
2186            let topic = format!("events.order.{strategy_id}");
2187            msgbus::publish_order_event(topic.into(), &event);
2188        }
2189    }
2190
2191    /// Denies all orders in an order list.
2192    ///
2193    /// This method denies each non-closed order in the list.
2194    fn deny_order_list(&mut self, orders: &[OrderAny], reason: Ustr)
2195    where
2196        Self: StrategyNative,
2197    {
2198        for order in orders {
2199            if !order.is_closed() {
2200                self.deny_order(order, reason);
2201            }
2202        }
2203    }
2204
2205    // -- GTD EXPIRY MANAGEMENT -------------------------------------------------------------------
2206
2207    /// Sets a GTD expiry timer for an order.
2208    ///
2209    /// Creates a timer that will automatically cancel the order when it expires.
2210    ///
2211    /// # Errors
2212    ///
2213    /// Returns an error if timer creation fails.
2214    fn set_gtd_expiry(&mut self, order: &OrderAny) -> anyhow::Result<()>
2215    where
2216        Self: StrategyNative,
2217    {
2218        let core = StrategyNative::strategy_core_mut(self);
2219
2220        if !core.config.manage_gtd_expiry || order.time_in_force() != TimeInForce::Gtd {
2221            return Ok(());
2222        }
2223
2224        let Some(expire_time) = order.expire_time() else {
2225            return Ok(());
2226        };
2227
2228        let client_order_id = order.client_order_id();
2229        let timer_name = format!("GTD-EXPIRY:{client_order_id}");
2230
2231        let current_time_ns = {
2232            let clock = core.clock_mut();
2233            clock.timestamp_ns()
2234        };
2235
2236        if current_time_ns >= expire_time.as_u64() {
2237            log::info!("GTD order {client_order_id} already expired, canceling immediately");
2238            return self.cancel_order(order.client_order_id(), None, None);
2239        }
2240
2241        {
2242            let mut clock = core.clock_mut();
2243            clock.set_time_alert_ns(&timer_name, expire_time, None, None)?;
2244        }
2245
2246        core.gtd_timers
2247            .insert(client_order_id, Ustr::from(&timer_name));
2248
2249        log::debug!("Set GTD expiry timer for {client_order_id} at {expire_time}");
2250        Ok(())
2251    }
2252
2253    /// Cancels a GTD expiry timer for an order.
2254    fn cancel_gtd_expiry(&mut self, client_order_id: &ClientOrderId)
2255    where
2256        Self: StrategyNative,
2257    {
2258        let core = StrategyNative::strategy_core_mut(self);
2259
2260        if let Some(timer_name) = core.gtd_timers.remove(client_order_id) {
2261            core.clock_mut().cancel_timer(timer_name.as_str());
2262            log::debug!("Canceled GTD expiry timer for {client_order_id}");
2263        }
2264    }
2265
2266    /// Checks if a GTD expiry timer exists for an order.
2267    fn has_gtd_expiry_timer(&mut self, client_order_id: &ClientOrderId) -> bool
2268    where
2269        Self: StrategyNative,
2270    {
2271        let core = StrategyNative::strategy_core_mut(self);
2272        core.gtd_timers.contains_key(client_order_id)
2273    }
2274
2275    /// Handles GTD order expiry by canceling the order.
2276    ///
2277    /// This method is called when a GTD expiry timer fires.
2278    fn expire_gtd_order(&mut self, event: TimeEvent)
2279    where
2280        Self: StrategyNative,
2281    {
2282        let timer_name = event.name;
2283        let Some(client_order_id) = timer_name
2284            .strip_prefix("GTD-EXPIRY:")
2285            .and_then(|value| ClientOrderId::new_checked(value).ok())
2286        else {
2287            log::error!("Invalid GTD timer name format: {timer_name}");
2288            return;
2289        };
2290
2291        let core = StrategyNative::strategy_core_mut(self);
2292        if core.gtd_timers.get(&client_order_id) != Some(&timer_name) {
2293            return;
2294        }
2295        core.gtd_timers.remove(&client_order_id);
2296
2297        let order = core.cache_ref().order(&client_order_id).map(|o| o.clone());
2298        let Some(order) = order else {
2299            log::warn!("GTD order {client_order_id} not found in cache");
2300            return;
2301        };
2302
2303        log::info!("GTD order {client_order_id} expired");
2304
2305        if let Err(e) = self.cancel_order(order.client_order_id(), None, None) {
2306            log::error!("Failed to cancel expired GTD order {client_order_id}: {e}");
2307        }
2308    }
2309
2310    /// Reactivates GTD timers for open orders on strategy start.
2311    ///
2312    /// Queries the cache for all open GTD orders and creates timers for those
2313    /// that haven't expired yet. Orders that have already expired are canceled immediately.
2314    fn reactivate_gtd_timers(&mut self)
2315    where
2316        Self: StrategyNative,
2317    {
2318        let core = StrategyNative::strategy_core_mut(self);
2319        let Some(strategy_id) = core.strategy_id() else {
2320            log::error!("Cannot reactivate GTD timers: strategy_id is not set");
2321            return;
2322        };
2323        let current_time_ns = core.clock_mut().timestamp_ns();
2324
2325        let gtd_orders: Vec<OrderAny> = core
2326            .cache_ref()
2327            .orders_open(None, None, Some(&strategy_id), None, None)
2328            .into_iter()
2329            .filter(|o| o.time_in_force() == TimeInForce::Gtd)
2330            .map(|o| o.clone())
2331            .collect();
2332
2333        for order in gtd_orders {
2334            let Some(expire_time) = order.expire_time() else {
2335                continue;
2336            };
2337
2338            let expire_time_ns = expire_time.as_u64();
2339            let client_order_id = order.client_order_id();
2340
2341            if current_time_ns >= expire_time_ns {
2342                log::info!("GTD order {client_order_id} already expired, canceling immediately");
2343                if let Err(e) = self.cancel_order(order.client_order_id(), None, None) {
2344                    log::error!("Failed to cancel expired GTD order {client_order_id}: {e}");
2345                }
2346            } else if let Err(e) = self.set_gtd_expiry(&order) {
2347                log::error!("Failed to set GTD expiry timer for {client_order_id}: {e}");
2348            }
2349        }
2350    }
2351}
2352
2353/// Routes a time event through framework-managed strategy handlers.
2354///
2355/// Runtime hosts call this before the user-facing [`DataActor::on_time_event`] callback so custom
2356/// [`Strategy::on_time_event`] implementations cannot replace managed GTD expiry or market exit
2357/// routing.
2358pub fn route_time_event<T>(strategy: &mut T, event: &TimeEvent)
2359where
2360    T: Strategy + StrategyNative + Component + ?Sized,
2361{
2362    let (gtd_order_id, is_market_exit) = {
2363        let core = StrategyNative::strategy_core(strategy);
2364        let gtd_order_id = event
2365            .name
2366            .strip_prefix("GTD-EXPIRY:")
2367            .and_then(|value| ClientOrderId::new_checked(value).ok())
2368            .filter(|client_order_id| core.gtd_timers.get(client_order_id) == Some(&event.name));
2369        let is_market_exit = event.name == core.market_exit_timer_name;
2370        (gtd_order_id, is_market_exit)
2371    };
2372
2373    if gtd_order_id.is_none() && !is_market_exit {
2374        return;
2375    }
2376
2377    let core = StrategyNative::strategy_core_mut(strategy);
2378    if core.managed_time_event_last_id == Some(event.event_id) {
2379        return;
2380    }
2381    core.managed_time_event_last_id = Some(event.event_id);
2382
2383    if gtd_order_id.is_some() {
2384        strategy.expire_gtd_order(event.clone());
2385    } else {
2386        strategy.check_market_exit(event.clone());
2387    }
2388}
2389
2390pub(super) fn submit_order_native<T>(
2391    strategy: &mut T,
2392    order: &OrderAny,
2393    position_id: Option<PositionId>,
2394    client_id: Option<ClientId>,
2395    params: Option<Params>,
2396) -> anyhow::Result<()>
2397where
2398    T: Strategy + StrategyNative + ?Sized,
2399{
2400    let core = StrategyNative::strategy_core_mut(strategy);
2401
2402    let trader_id = registered_trader_id(core)?;
2403    let strategy_id = registered_strategy_id(core)?;
2404    let ts_init = core.clock_mut().timestamp_ns();
2405
2406    if order.status() != OrderStatus::Initialized {
2407        anyhow::bail!(
2408            "Order denied: invalid status for {}, expected INITIALIZED",
2409            order.client_order_id()
2410        );
2411    }
2412
2413    let market_exit_tag = core.market_exit_tag;
2414    let is_market_exit_order = order
2415        .tags()
2416        .is_some_and(|tags| tags.contains(&market_exit_tag));
2417    let should_deny_for_market_exit =
2418        core.is_exiting && !order.is_reduce_only() && !is_market_exit_order;
2419
2420    let params = params.filter(|params| !params.is_empty());
2421
2422    {
2423        let cache_rc = core.cache_rc();
2424        let mut cache = cache_rc.try_borrow_mut().map_err(|_| {
2425            anyhow::anyhow!(
2426                "Cannot submit order {}: cache is currently borrowed",
2427                order.client_order_id()
2428            )
2429        })?;
2430
2431        if cache
2432            .order(&order.client_order_id())
2433            .is_some_and(|cached_order| !cached_order.is_active_local())
2434        {
2435            anyhow::bail!("Order denied: duplicate {}", order.client_order_id());
2436        }
2437
2438        if !should_deny_for_market_exit {
2439            cache.add_order(order.clone(), position_id, client_id, true)?;
2440        }
2441    }
2442
2443    if should_deny_for_market_exit {
2444        strategy.deny_order(order, Ustr::from("MARKET_EXIT_IN_PROGRESS"));
2445        return Ok(());
2446    }
2447
2448    publish_order_initialized(order);
2449
2450    let command = SubmitOrder::new(
2451        trader_id,
2452        client_id,
2453        strategy_id,
2454        order.instrument_id(),
2455        order.client_order_id(),
2456        order.init_event().clone(),
2457        order.exec_algorithm_id(),
2458        position_id,
2459        params,
2460        UUID4::new(),
2461        ts_init,
2462        None, // correlation_id
2463    );
2464
2465    if order.emulation_trigger().is_some() {
2466        send_emulator_command(TradingCommand::SubmitOrder(command));
2467    } else if let Some(exec_algorithm_id) = order.exec_algorithm_id() {
2468        send_algo_command(command, exec_algorithm_id);
2469    } else {
2470        send_risk_command(TradingCommand::SubmitOrder(command));
2471    }
2472
2473    strategy.set_gtd_expiry(order)?;
2474    Ok(())
2475}
2476
2477fn restore_gtd_timer<T: Strategy + StrategyNative + ?Sized>(
2478    strategy: &mut T,
2479    event: &OrderEventAny,
2480) {
2481    match event {
2482        OrderEventAny::CancelDenied(_) | OrderEventAny::CancelRejected(_) => {}
2483        OrderEventAny::FillVoided(event) if event.is_reopened => {}
2484        _ => return,
2485    }
2486
2487    let client_order_id = event.client_order_id();
2488    let order = StrategyNative::strategy_core_mut(strategy)
2489        .cache_ref()
2490        .order(&client_order_id)
2491        .map(|order| order.clone());
2492
2493    let Some(order) = order else {
2494        return;
2495    };
2496
2497    match event {
2498        OrderEventAny::CancelDenied(_) | OrderEventAny::CancelRejected(_) => {
2499            if !(order.is_open() || order.is_inflight()) || order.has_pending_cancel_command() {
2500                return;
2501            }
2502        }
2503        OrderEventAny::FillVoided(event) if event.is_reopened => {
2504            if !order.is_open() {
2505                return;
2506            }
2507        }
2508        _ => return,
2509    }
2510
2511    if strategy.has_gtd_expiry_timer(&client_order_id) {
2512        return;
2513    }
2514
2515    let now = StrategyNative::strategy_core_mut(strategy)
2516        .clock_mut()
2517        .timestamp_ns();
2518
2519    if matches!(event, OrderEventAny::CancelDenied(_))
2520        && order.expire_time().is_some_and(|expiry| expiry <= now)
2521    {
2522        log::warn!("GTD cancel denied for expired order {client_order_id}; order remains open");
2523        return;
2524    }
2525
2526    if let Err(e) = strategy.set_gtd_expiry(&order) {
2527        log::error!("Failed to restore GTD expiry for {client_order_id}: {e}");
2528    }
2529}
2530
2531fn publish_order_initialized(order: &OrderAny) {
2532    let topic = msgbus::switchboard::get_event_order_topic(order.strategy_id());
2533    let event = OrderEventAny::Initialized(order.init_event().clone());
2534    msgbus::publish_order_event(topic, &event);
2535}
2536
2537fn send_emulator_command(command: TradingCommand) {
2538    log_cmd_send(&command);
2539    let endpoint = MessagingSwitchboard::order_emulator_execute();
2540    msgbus::send_trading_command(endpoint, command);
2541}
2542
2543fn send_algo_command(command: SubmitOrder, exec_algorithm_id: ExecAlgorithmId) {
2544    let id = command.strategy_id;
2545    log::info!("{id} {CMD}{SEND} {command}");
2546
2547    let endpoint = format!("{exec_algorithm_id}.execute");
2548    msgbus::send_any(endpoint.into(), &TradingCommand::SubmitOrder(command));
2549}
2550
2551fn send_risk_command(command: TradingCommand) {
2552    log_cmd_send(&command);
2553    let endpoint = MessagingSwitchboard::risk_engine_queue_execute();
2554    msgbus::send_trading_command(endpoint, command);
2555}
2556
2557fn send_exec_command(command: TradingCommand) {
2558    log_cmd_send(&command);
2559    let endpoint = MessagingSwitchboard::exec_engine_queue_execute();
2560    msgbus::send_trading_command(endpoint, command);
2561}
2562
2563fn log_cmd_send(command: &TradingCommand) {
2564    if let Some(id) = command.strategy_id() {
2565        log::info!("{id} {CMD}{SEND} {command}");
2566    } else {
2567        log::info!("{CMD}{SEND} {command}");
2568    }
2569}
2570
2571fn closing_qty(
2572    core: &StrategyCore,
2573    instrument_id: InstrumentId,
2574    quantity: Quantity,
2575) -> anyhow::Result<Option<Quantity>> {
2576    let cache = core.cache_ref();
2577
2578    let instrument = cache.instrument(&instrument_id).ok_or_else(|| {
2579        anyhow::anyhow!("Cannot close position: instrument {instrument_id} is not cached")
2580    })?;
2581
2582    let Some(rounded) = quantity.checked_floor_to_increment(instrument.size_increment()) else {
2583        anyhow::bail!("close quantity cannot be aligned to the size increment");
2584    };
2585
2586    if rounded.is_zero() {
2587        log::warn!(
2588            "Cannot close {instrument_id} position: quantity {quantity} is below the size increment"
2589        );
2590        return Ok(None);
2591    }
2592
2593    Ok(Some(rounded))
2594}
2595
2596fn registered_trader_id(core: &StrategyCore) -> anyhow::Result<TraderId> {
2597    core.trader_id()
2598        .ok_or_else(|| anyhow::anyhow!("Strategy not registered: trader_id is not set"))
2599}
2600
2601fn registered_strategy_id(core: &StrategyCore) -> anyhow::Result<StrategyId> {
2602    core.strategy_id()
2603        .ok_or_else(|| anyhow::anyhow!("Strategy not registered: strategy_id is not set"))
2604}
2605
2606// Trailing stops carry a venue-maintained trigger that a modify can reset, matching the risk
2607// engine's modify projection.
2608fn has_trigger_price(order_type: OrderType) -> bool {
2609    STOP_ORDER_TYPES.contains(&order_type)
2610        || matches!(
2611            order_type,
2612            OrderType::TrailingStopMarket | OrderType::TrailingStopLimit
2613        )
2614}
2615
2616fn required_account_id(order: &OrderAny, operation: &str) -> anyhow::Result<AccountId> {
2617    order.account_id().ok_or_else(|| {
2618        anyhow::anyhow!(
2619            "Cannot generate {operation} event for {}: account_id is not set",
2620            order.client_order_id()
2621        )
2622    })
2623}
2624
2625#[cfg(test)]
2626mod tests {
2627    use std::{cell::RefCell, rc::Rc, sync::Mutex};
2628
2629    use nautilus_common::{
2630        actor::{
2631            DataActor,
2632            registry::{deregister_actor, try_get_actor_unchecked},
2633        },
2634        cache::{Cache, ORDER_NOT_FOUND},
2635        clock::{Clock, VirtualClock},
2636        component::{Component, deregister_component, register_component_actor},
2637        enums::ComponentState,
2638        logging::{
2639            arm_shutdown_on_error, disarm_shutdown_on_error, init_logging,
2640            take_shutdown_on_error_trigger,
2641        },
2642        msgbus::{
2643            self, MessagingSwitchboard, TypedHandler, TypedIntoHandler,
2644            stubs::{
2645                TypedIntoMessageSavingHandler, TypedMessageSavingHandler, get_any_saving_handler,
2646                get_typed_into_message_saving_handler, get_typed_message_saving_handler,
2647            },
2648        },
2649        timer::{TimeEvent, TimeEventCallback},
2650    };
2651    use nautilus_core::{DurationNanos, UnixNanos};
2652    use nautilus_execution::{engine::ExecutionEngine, order_emulator::emulator::OrderEmulator};
2653    use nautilus_model::{
2654        data::QuoteTick,
2655        enums::{
2656            ContingencyType, LiquiditySide, OmsType, OrderSide, OrderStatus, OrderType,
2657            PositionAdjustmentType, PositionSide, TrailingOffsetType, TriggerType,
2658        },
2659        events::{
2660            OrderAccepted, OrderCanceled, OrderFilled, OrderRejected, PositionAdjusted,
2661            order::spec::{
2662                OrderAcceptedSpec, OrderCancelDeniedSpec, OrderCancelRejectedSpec,
2663                OrderCanceledSpec, OrderEmulatedSpec, OrderExpiredSpec, OrderFillVoidedSpec,
2664                OrderFilledSpec, OrderModifyRejectedSpec, OrderRejectedSpec, OrderReleasedSpec,
2665                OrderUpdatedSpec,
2666            },
2667        },
2668        identifiers::{
2669            AccountId, ActorId, ClientOrderId, InstrumentId, OrderListId, PositionId, StrategyId,
2670            TradeId, TraderId, VenueOrderId,
2671        },
2672        instruments::{InstrumentAny, stubs::currency_pair_btcusdt},
2673        orderbook::own::OwnOrderBook,
2674        orders::{LimitOrder, MarketOrder, OrderTestBuilder, stubs::TestOrderEventStubs},
2675        stubs::TestDefault,
2676        types::{Currency, Money, Price},
2677    };
2678    use nautilus_portfolio::portfolio::Portfolio;
2679    use rstest::rstest;
2680    use rust_decimal::Decimal;
2681    use serde_json::Value;
2682
2683    use super::*;
2684    use crate::{cache_database::FailNthAddOrderDatabase, nautilus_strategy};
2685
2686    #[derive(Debug)]
2687    struct TestStrategy {
2688        core: StrategyCore,
2689        start_calls: usize,
2690        on_order_rejected_called: bool,
2691        on_order_event_called: bool,
2692        on_order_accepted_called: bool,
2693        on_order_canceled_called: bool,
2694        on_order_filled_called: bool,
2695        on_order_fill_voided_called: bool,
2696        on_order_expired_called: bool,
2697        order_event_timeline: Rc<RefCell<Vec<&'static str>>>,
2698        on_position_event_called: bool,
2699        on_position_opened_called: bool,
2700        on_position_changed_called: bool,
2701        on_position_closed_called: bool,
2702    }
2703
2704    #[derive(Debug)]
2705    struct ModifyRejectionStrategy {
2706        core: StrategyCore,
2707        rejections: Vec<(OrderModifyRejected, OrderAny)>,
2708    }
2709
2710    #[derive(Debug)]
2711    struct CoreFreeStrategy {
2712        started: bool,
2713    }
2714
2715    #[derive(Debug)]
2716    struct InitializedModifyStrategy {
2717        core: StrategyCore,
2718        modified_quantity: Quantity,
2719    }
2720
2721    #[derive(Debug)]
2722    struct TimerOverrideStrategy {
2723        core: StrategyCore,
2724        gtd_expiries: usize,
2725        market_exit_checks: usize,
2726    }
2727
2728    impl DataActor for ModifyRejectionStrategy {}
2729
2730    nautilus_strategy!(ModifyRejectionStrategy, {
2731        fn on_order_modify_rejected(&mut self, event: OrderModifyRejected) {
2732            let order = self.cache().order(&event.client_order_id).unwrap();
2733            self.rejections.push((event, order));
2734        }
2735    });
2736
2737    impl DataActor for CoreFreeStrategy {
2738        fn on_start(&mut self) -> anyhow::Result<()> {
2739            self.started = true;
2740            Ok(())
2741        }
2742    }
2743
2744    impl Strategy for CoreFreeStrategy {}
2745
2746    impl DataActor for InitializedModifyStrategy {}
2747
2748    nautilus_strategy!(InitializedModifyStrategy, {
2749        fn on_order_initialized(&mut self, event: OrderInitialized) {
2750            self.modify_order(
2751                event.client_order_id,
2752                Some(self.modified_quantity),
2753                None,
2754                None,
2755                None,
2756                None,
2757            )
2758            .unwrap();
2759        }
2760    });
2761
2762    impl DataActor for TimerOverrideStrategy {
2763        fn on_time_event(&mut self, event: &TimeEvent) -> anyhow::Result<()> {
2764            Strategy::on_time_event(self, event)
2765        }
2766    }
2767
2768    nautilus_strategy!(TimerOverrideStrategy, {
2769        fn check_market_exit(&mut self, _event: TimeEvent) {
2770            self.market_exit_checks += 1;
2771        }
2772
2773        fn expire_gtd_order(&mut self, _event: TimeEvent) {
2774            self.gtd_expiries += 1;
2775        }
2776    });
2777
2778    impl TestStrategy {
2779        fn new(config: StrategyConfig) -> Self {
2780            Self {
2781                core: StrategyCore::new(config),
2782                start_calls: 0,
2783                on_order_rejected_called: false,
2784                on_order_event_called: false,
2785                on_order_accepted_called: false,
2786                on_order_canceled_called: false,
2787                on_order_filled_called: false,
2788                on_order_fill_voided_called: false,
2789                on_order_expired_called: false,
2790                order_event_timeline: Rc::new(RefCell::new(Vec::new())),
2791                on_position_event_called: false,
2792                on_position_opened_called: false,
2793                on_position_changed_called: false,
2794                on_position_closed_called: false,
2795            }
2796        }
2797    }
2798
2799    impl DataActor for TestStrategy {
2800        fn on_start(&mut self) -> anyhow::Result<()> {
2801            self.start_calls += 1;
2802            Ok(())
2803        }
2804    }
2805
2806    nautilus_strategy!(TestStrategy, {
2807        fn on_order_canceled(&mut self, _event: &OrderCanceled) {
2808            self.on_order_canceled_called = true;
2809        }
2810
2811        fn on_order_filled(&mut self, _event: &OrderFilled) {
2812            self.on_order_filled_called = true;
2813            self.order_event_timeline.borrow_mut().push("specific");
2814        }
2815
2816        fn on_order_fill_voided(&mut self, _event: &OrderFillVoided) {
2817            self.on_order_fill_voided_called = true;
2818        }
2819
2820        fn on_order_rejected(&mut self, _event: OrderRejected) {
2821            self.on_order_rejected_called = true;
2822        }
2823
2824        fn on_order_event(&mut self, _event: OrderEventAny) {
2825            self.on_order_event_called = true;
2826            self.order_event_timeline.borrow_mut().push("aggregate");
2827        }
2828
2829        fn on_order_accepted(&mut self, _event: OrderAccepted) {
2830            self.on_order_accepted_called = true;
2831        }
2832
2833        fn on_order_expired(&mut self, _event: OrderExpired) {
2834            self.on_order_expired_called = true;
2835        }
2836
2837        fn on_position_opened(&mut self, _event: PositionOpened) {
2838            self.on_position_opened_called = true;
2839        }
2840
2841        fn on_position_event(&mut self, _event: PositionEvent) {
2842            self.on_position_event_called = true;
2843        }
2844
2845        fn on_position_changed(&mut self, _event: PositionChanged) {
2846            self.on_position_changed_called = true;
2847        }
2848
2849        fn on_position_closed(&mut self, _event: PositionClosed) {
2850            self.on_position_closed_called = true;
2851        }
2852    });
2853
2854    fn create_test_strategy() -> TestStrategy {
2855        let config = StrategyConfig {
2856            strategy_id: Some(StrategyId::from("TEST-001")),
2857            order_id_tag: Some("001".to_string()),
2858            ..Default::default()
2859        };
2860        TestStrategy::new(config)
2861    }
2862
2863    fn create_gtd_managed_strategy() -> TestStrategy {
2864        TestStrategy::new(StrategyConfig {
2865            strategy_id: Some(StrategyId::from("TEST-001")),
2866            order_id_tag: Some("001".to_string()),
2867            manage_gtd_expiry: true,
2868            ..Default::default()
2869        })
2870    }
2871
2872    fn register_strategy(strategy: &mut TestStrategy) {
2873        let _clock = register_strategy_with_clock(strategy);
2874    }
2875
2876    fn register_strategy_with_clock(strategy: &mut TestStrategy) -> Rc<RefCell<VirtualClock>> {
2877        let trader_id = TraderId::from("TRADER-001");
2878        let clock = Rc::new(RefCell::new(VirtualClock::new()));
2879        let cache = Rc::new(RefCell::new(Cache::default()));
2880        let portfolio = Rc::new(RefCell::new(Portfolio::new(
2881            clock.clone(),
2882            cache.clone(),
2883            None,
2884        )));
2885
2886        strategy
2887            .core
2888            .register(trader_id, clock.clone(), cache, portfolio)
2889            .unwrap();
2890        strategy.initialize().unwrap();
2891        clock
2892    }
2893
2894    fn register_gtd_strategy(strategy: &mut TestStrategy) -> Rc<RefCell<VirtualClock>> {
2895        let clock = register_strategy_with_clock(strategy);
2896        clock
2897            .borrow_mut()
2898            .register_default_handler(TimeEventCallback::from(|_event: TimeEvent| {}));
2899        clock
2900    }
2901
2902    fn start_strategy(strategy: &mut TestStrategy) {
2903        strategy.start().unwrap();
2904    }
2905
2906    fn stop_strategy(strategy: &mut TestStrategy) {
2907        Component::stop(strategy).unwrap();
2908    }
2909
2910    fn make_filled(client_order_id: ClientOrderId) -> OrderEventAny {
2911        OrderEventAny::Filled(
2912            OrderFilledSpec::builder()
2913                .trader_id(TraderId::from("TRADER-001"))
2914                .strategy_id(StrategyId::from("TEST-001"))
2915                .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2916                .client_order_id(client_order_id)
2917                .venue_order_id(VenueOrderId::test_default())
2918                .account_id(AccountId::from("ACC-001"))
2919                .trade_id(TradeId::test_default())
2920                .last_qty(Quantity::default())
2921                .last_px(Price::default())
2922                .currency(Currency::from("USD"))
2923                .liquidity_side(LiquiditySide::Taker)
2924                .event_id(UUID4::default())
2925                .build(),
2926        )
2927    }
2928
2929    fn make_fill_voided(client_order_id: ClientOrderId, is_reopened: bool) -> OrderEventAny {
2930        OrderEventAny::FillVoided(
2931            OrderFillVoidedSpec::builder()
2932                .trader_id(TraderId::from("TRADER-001"))
2933                .strategy_id(StrategyId::from("TEST-001"))
2934                .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2935                .client_order_id(client_order_id)
2936                .venue_order_id(VenueOrderId::test_default())
2937                .account_id(AccountId::from("ACC-001"))
2938                .is_reopened(is_reopened)
2939                .build(),
2940        )
2941    }
2942
2943    fn make_terminal_fill_voided(client_order_id: ClientOrderId) -> OrderEventAny {
2944        make_fill_voided(client_order_id, false)
2945    }
2946
2947    fn make_canceled(client_order_id: ClientOrderId) -> OrderEventAny {
2948        OrderEventAny::Canceled(
2949            OrderCanceledSpec::builder()
2950                .trader_id(TraderId::from("TRADER-001"))
2951                .strategy_id(StrategyId::from("TEST-001"))
2952                .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2953                .client_order_id(client_order_id)
2954                .account_id(AccountId::from("ACC-001"))
2955                .event_id(UUID4::default())
2956                .build(),
2957        )
2958    }
2959
2960    fn make_rejected(client_order_id: ClientOrderId) -> OrderEventAny {
2961        OrderEventAny::Rejected(
2962            OrderRejectedSpec::builder()
2963                .trader_id(TraderId::from("TRADER-001"))
2964                .strategy_id(StrategyId::from("TEST-001"))
2965                .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2966                .client_order_id(client_order_id)
2967                .account_id(AccountId::from("ACC-001"))
2968                .reason("Test rejection".into())
2969                .event_id(UUID4::default())
2970                .build(),
2971        )
2972    }
2973
2974    fn make_cancel_denied(client_order_id: ClientOrderId) -> OrderEventAny {
2975        OrderEventAny::CancelDenied(
2976            OrderCancelDeniedSpec::builder()
2977                .trader_id(TraderId::from("TRADER-001"))
2978                .strategy_id(StrategyId::from("TEST-001"))
2979                .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2980                .client_order_id(client_order_id)
2981                .reason("Test denial".into())
2982                .venue_order_id(VenueOrderId::from(client_order_id.as_str()))
2983                .account_id(AccountId::from("ACC-001"))
2984                .build(),
2985        )
2986    }
2987
2988    fn make_cancel_rejected(client_order_id: ClientOrderId) -> OrderEventAny {
2989        OrderEventAny::CancelRejected(
2990            OrderCancelRejectedSpec::builder()
2991                .trader_id(TraderId::from("TRADER-001"))
2992                .strategy_id(StrategyId::from("TEST-001"))
2993                .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2994                .client_order_id(client_order_id)
2995                .reason("Test rejection".into())
2996                .venue_order_id(VenueOrderId::from(client_order_id.as_str()))
2997                .account_id(AccountId::from("ACC-001"))
2998                .build(),
2999        )
3000    }
3001
3002    fn make_expired(client_order_id: ClientOrderId) -> OrderEventAny {
3003        OrderEventAny::Expired(
3004            OrderExpiredSpec::builder()
3005                .trader_id(TraderId::from("TRADER-001"))
3006                .strategy_id(StrategyId::from("TEST-001"))
3007                .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
3008                .client_order_id(client_order_id)
3009                .account_id(AccountId::from("ACC-001"))
3010                .event_id(UUID4::default())
3011                .build(),
3012        )
3013    }
3014
3015    fn make_accepted(client_order_id: ClientOrderId) -> OrderEventAny {
3016        OrderEventAny::Accepted(
3017            OrderAcceptedSpec::builder()
3018                .trader_id(TraderId::from("TRADER-001"))
3019                .strategy_id(StrategyId::from("TEST-001"))
3020                .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
3021                .client_order_id(client_order_id)
3022                .venue_order_id(VenueOrderId::test_default())
3023                .account_id(AccountId::from("ACC-001"))
3024                .event_id(UUID4::default())
3025                .build(),
3026        )
3027    }
3028
3029    fn make_accepted_market_order(client_order_id: &str) -> OrderAny {
3030        let mut order = OrderAny::Market(MarketOrder::new(
3031            TraderId::from("TRADER-001"),
3032            StrategyId::from("TEST-001"),
3033            InstrumentId::from("BTCUSDT.BINANCE"),
3034            ClientOrderId::from(client_order_id),
3035            OrderSide::Buy,
3036            Quantity::from(100_000),
3037            TimeInForce::Gtc,
3038            UUID4::new(),
3039            UnixNanos::default(),
3040            false,
3041            false,
3042            None,
3043            None,
3044            None,
3045            None,
3046            None,
3047            None,
3048            None,
3049            None,
3050        ));
3051        let account_id = AccountId::from("ACC-001");
3052        order
3053            .apply(TestOrderEventStubs::submitted(&order, account_id))
3054            .unwrap();
3055        order
3056            .apply(TestOrderEventStubs::accepted(
3057                &order,
3058                account_id,
3059                // Derived per order: a venue order ID has a single owning client order, so
3060                // batch tests holding two accepted orders cannot share the default.
3061                VenueOrderId::from(client_order_id),
3062            ))
3063            .unwrap();
3064        order
3065    }
3066
3067    fn make_accepted_limit_order(client_order_id: &str) -> OrderAny {
3068        let mut order = OrderAny::Limit(LimitOrder::new(
3069            TraderId::from("TRADER-001"),
3070            StrategyId::from("TEST-001"),
3071            InstrumentId::from("BTCUSDT.BINANCE"),
3072            ClientOrderId::from(client_order_id),
3073            OrderSide::Buy,
3074            Quantity::from("1.0"),
3075            Price::from("50000.0"),
3076            TimeInForce::Gtc,
3077            None,
3078            false,
3079            false,
3080            false,
3081            None,
3082            None,
3083            None,
3084            None,
3085            None,
3086            None,
3087            None,
3088            None,
3089            None,
3090            None,
3091            None,
3092            UUID4::new(),
3093            UnixNanos::default(),
3094        ));
3095        let account_id = AccountId::from("ACC-001");
3096        order
3097            .apply(TestOrderEventStubs::submitted(&order, account_id))
3098            .unwrap();
3099        order
3100            .apply(TestOrderEventStubs::accepted(
3101                &order,
3102                account_id,
3103                // Derived per order, as above.
3104                VenueOrderId::from(client_order_id),
3105            ))
3106            .unwrap();
3107        order
3108    }
3109
3110    fn make_submitted_gtd_limit_order(client_order_id: &str, expire_time: UnixNanos) -> OrderAny {
3111        let mut order = OrderTestBuilder::new(OrderType::Limit)
3112            .trader_id(TraderId::from("TRADER-001"))
3113            .strategy_id(StrategyId::from("TEST-001"))
3114            .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
3115            .client_order_id(ClientOrderId::from(client_order_id))
3116            .side(OrderSide::Buy)
3117            .quantity(Quantity::from("1.0"))
3118            .price(Price::from("50000.0"))
3119            .time_in_force(TimeInForce::Gtd)
3120            .expire_time(expire_time)
3121            .build();
3122        let account_id = AccountId::from("ACC-001");
3123        order
3124            .apply(TestOrderEventStubs::submitted(&order, account_id))
3125            .unwrap();
3126        order
3127    }
3128
3129    fn make_accepted_gtd_limit_order(client_order_id: &str, expire_time: UnixNanos) -> OrderAny {
3130        let mut order = make_submitted_gtd_limit_order(client_order_id, expire_time);
3131        let account_id = AccountId::from("ACC-001");
3132        order
3133            .apply(TestOrderEventStubs::accepted(
3134                &order,
3135                account_id,
3136                VenueOrderId::from(client_order_id),
3137            ))
3138            .unwrap();
3139        order
3140    }
3141
3142    fn make_initialized_market_order(client_order_id: &str) -> OrderAny {
3143        OrderAny::Market(MarketOrder::new(
3144            TraderId::from("TRADER-001"),
3145            StrategyId::from("TEST-001"),
3146            InstrumentId::from("BTCUSDT.BINANCE"),
3147            ClientOrderId::from(client_order_id),
3148            OrderSide::Buy,
3149            Quantity::from(100_000),
3150            TimeInForce::Gtc,
3151            UUID4::new(),
3152            UnixNanos::default(),
3153            false,
3154            false,
3155            None,
3156            None,
3157            None,
3158            None,
3159            None,
3160            None,
3161            None,
3162            None,
3163        ))
3164    }
3165
3166    fn make_initialized_trailing_stop_order(
3167        order_type: OrderType,
3168        client_order_id: &str,
3169    ) -> OrderAny {
3170        let mut builder = OrderTestBuilder::new(order_type);
3171        builder
3172            .trader_id(TraderId::from("TRADER-001"))
3173            .strategy_id(StrategyId::from("TEST-001"))
3174            .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
3175            .client_order_id(ClientOrderId::from(client_order_id))
3176            .side(OrderSide::Sell)
3177            .quantity(Quantity::from(100_000))
3178            .trigger_price(Price::from("49000.0"))
3179            .trailing_offset(Decimal::from(100))
3180            .trailing_offset_type(TrailingOffsetType::Price);
3181
3182        if order_type == OrderType::TrailingStopLimit {
3183            builder
3184                .price(Price::from("48900.0"))
3185                .limit_offset(Decimal::from(100));
3186        }
3187
3188        builder.build()
3189    }
3190
3191    fn make_initialized_algorithm_order(client_order_id: &str) -> OrderAny {
3192        OrderAny::Market(MarketOrder::new(
3193            TraderId::from("TRADER-001"),
3194            StrategyId::from("TEST-001"),
3195            InstrumentId::from("BTCUSDT.BINANCE"),
3196            ClientOrderId::from(client_order_id),
3197            OrderSide::Buy,
3198            Quantity::from(100_000),
3199            TimeInForce::Gtc,
3200            UUID4::new(),
3201            UnixNanos::default(),
3202            false,
3203            false,
3204            None,
3205            None,
3206            None,
3207            None,
3208            Some(ExecAlgorithmId::from("TWAP")),
3209            None,
3210            Some(ClientOrderId::from(client_order_id)),
3211            None,
3212        ))
3213    }
3214
3215    fn add_order_to_cache(strategy: &TestStrategy, order: &OrderAny) {
3216        let cache_rc = strategy.core.cache_rc();
3217        let mut cache = cache_rc.borrow_mut();
3218        cache.add_order(order.clone(), None, None, true).unwrap();
3219    }
3220
3221    fn make_submit_command(order: &OrderAny) -> SubmitOrder {
3222        SubmitOrder::new(
3223            order.trader_id(),
3224            None,
3225            order.strategy_id(),
3226            order.instrument_id(),
3227            order.client_order_id(),
3228            order.init_event().clone(),
3229            order.exec_algorithm_id(),
3230            None,
3231            None,
3232            UUID4::new(),
3233            UnixNanos::default(),
3234            None, // correlation_id
3235        )
3236    }
3237
3238    fn add_order_to_cache_and_own_book(strategy: &TestStrategy, order: &OrderAny) {
3239        let cache_rc = strategy.core.cache_rc();
3240        let mut cache = cache_rc.borrow_mut();
3241        cache.add_order(order.clone(), None, None, true).unwrap();
3242        cache
3243            .add_own_order_book(OwnOrderBook::new(order.instrument_id()))
3244            .unwrap();
3245        cache.update_own_order_book(order);
3246    }
3247
3248    fn make_position_opened() -> PositionEvent {
3249        PositionEvent::PositionOpened(PositionOpened {
3250            trader_id: TraderId::from("TRADER-001"),
3251            strategy_id: StrategyId::from("TEST-001"),
3252            instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
3253            position_id: PositionId::test_default(),
3254            account_id: AccountId::from("ACC-001"),
3255            opening_order_id: ClientOrderId::from("O-001"),
3256            entry: OrderSide::Buy,
3257            side: PositionSide::Long,
3258            signed_qty: 1.into(),
3259            quantity: Quantity::default(),
3260            last_qty: Quantity::default(),
3261            last_px: Price::default(),
3262            currency: Currency::from("USD"),
3263            avg_px_open: 0.into(),
3264            realized_pnl: None,
3265            event_id: UUID4::default(),
3266            ts_event: UnixNanos::default(),
3267            ts_init: UnixNanos::default(),
3268        })
3269    }
3270
3271    fn make_position_changed() -> PositionEvent {
3272        let currency = Currency::from("USD");
3273        PositionEvent::PositionChanged(PositionChanged {
3274            trader_id: TraderId::from("TRADER-001"),
3275            strategy_id: StrategyId::from("TEST-001"),
3276            instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
3277            position_id: PositionId::test_default(),
3278            account_id: AccountId::from("ACC-001"),
3279            opening_order_id: ClientOrderId::from("O-001"),
3280            entry: OrderSide::Buy,
3281            side: PositionSide::Long,
3282            signed_qty: 2.into(),
3283            quantity: Quantity::default(),
3284            peak_quantity: Quantity::default(),
3285            last_qty: Quantity::default(),
3286            last_px: Price::default(),
3287            currency,
3288            avg_px_open: 0.into(),
3289            avg_px_close: None,
3290            realized_return: 0.0,
3291            realized_pnl: None,
3292            unrealized_pnl: Money::zero(currency),
3293            event_id: UUID4::default(),
3294            ts_opened: UnixNanos::default(),
3295            ts_event: UnixNanos::default(),
3296            ts_init: UnixNanos::default(),
3297        })
3298    }
3299
3300    fn make_position_closed() -> PositionEvent {
3301        let currency = Currency::from("USD");
3302        PositionEvent::PositionClosed(PositionClosed {
3303            trader_id: TraderId::from("TRADER-001"),
3304            strategy_id: StrategyId::from("TEST-001"),
3305            instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
3306            position_id: PositionId::test_default(),
3307            account_id: AccountId::from("ACC-001"),
3308            opening_order_id: ClientOrderId::from("O-001"),
3309            closing_order_id: Some(ClientOrderId::from("O-002")),
3310            entry: OrderSide::Buy,
3311            side: PositionSide::Flat,
3312            signed_qty: 0.into(),
3313            quantity: Quantity::default(),
3314            peak_quantity: Quantity::default(),
3315            last_qty: Quantity::default(),
3316            last_px: Price::default(),
3317            currency,
3318            avg_px_open: 0.into(),
3319            avg_px_close: None,
3320            realized_return: 0.0,
3321            realized_pnl: None,
3322            unrealized_pnl: Money::zero(currency),
3323            duration: DurationNanos::default(),
3324            event_id: UUID4::default(),
3325            ts_opened: UnixNanos::default(),
3326            ts_closed: None,
3327            ts_event: UnixNanos::default(),
3328            ts_init: UnixNanos::default(),
3329        })
3330    }
3331
3332    fn make_position_adjusted() -> PositionEvent {
3333        PositionEvent::PositionAdjusted(PositionAdjusted {
3334            trader_id: TraderId::from("TRADER-001"),
3335            strategy_id: StrategyId::from("TEST-001"),
3336            instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
3337            position_id: PositionId::test_default(),
3338            account_id: AccountId::from("ACC-001"),
3339            adjustment_type: PositionAdjustmentType::Funding,
3340            quantity_change: None,
3341            pnl_change: None,
3342            reason: None,
3343            event_id: UUID4::default(),
3344            ts_event: UnixNanos::default(),
3345            ts_init: UnixNanos::default(),
3346        })
3347    }
3348
3349    #[rstest]
3350    fn test_strategy_creation() {
3351        let strategy = create_test_strategy();
3352        assert_eq!(strategy.strategy_id(), Some(StrategyId::from("TEST-001")));
3353        assert!(!strategy.on_order_rejected_called);
3354        assert!(!strategy.on_position_opened_called);
3355    }
3356
3357    #[rstest]
3358    fn test_strategy_registration() {
3359        let mut strategy = create_test_strategy();
3360        register_strategy(&mut strategy);
3361
3362        assert!(strategy.is_registered());
3363        let _ = strategy.order().generate_client_order_id();
3364        let _ = strategy.portfolio().is_initialized();
3365    }
3366
3367    #[rstest]
3368    fn test_set_external_order_instrument_ids_replaces_claims_atomically() {
3369        let mut strategy = create_test_strategy();
3370        register_strategy(&mut strategy);
3371        let cache = strategy.core.cache_rc();
3372        let strategy_id = StrategyId::from("TEST-001");
3373        let other_strategy_id = StrategyId::from("OTHER-001");
3374        let audusd = InstrumentId::from("AUDUSD.SIM");
3375        let eurusd = InstrumentId::from("EURUSD.SIM");
3376        let gbpusd = InstrumentId::from("GBPUSD.SIM");
3377
3378        strategy
3379            .set_external_order_instrument_ids(vec![audusd, eurusd])
3380            .unwrap();
3381        cache
3382            .borrow_mut()
3383            .set_external_order_claims(other_strategy_id, &[gbpusd])
3384            .unwrap();
3385
3386        let result = strategy.set_external_order_instrument_ids(vec![eurusd, gbpusd]);
3387
3388        assert_eq!(
3389            result.unwrap_err().to_string(),
3390            "External order claim for GBPUSD.SIM already exists for OTHER-001"
3391        );
3392        assert_eq!(
3393            cache.borrow().external_order_claim(&audusd),
3394            Some(strategy_id)
3395        );
3396        assert_eq!(
3397            cache.borrow().external_order_claim(&eurusd),
3398            Some(strategy_id)
3399        );
3400        assert_eq!(
3401            cache.borrow().external_order_claim(&gbpusd),
3402            Some(other_strategy_id)
3403        );
3404        assert_eq!(
3405            strategy.core.config.external_order_instrument_ids,
3406            Some(vec![audusd, eurusd])
3407        );
3408
3409        strategy
3410            .set_external_order_instrument_ids(vec![eurusd])
3411            .unwrap();
3412
3413        assert_eq!(cache.borrow().external_order_claim(&audusd), None);
3414        assert_eq!(
3415            cache.borrow().external_order_claim(&eurusd),
3416            Some(strategy_id)
3417        );
3418        assert_eq!(
3419            cache.borrow().external_order_claim(&gbpusd),
3420            Some(other_strategy_id)
3421        );
3422        assert_eq!(
3423            strategy.core.config.external_order_instrument_ids,
3424            Some(vec![eurusd])
3425        );
3426    }
3427
3428    #[rstest]
3429    fn test_set_external_order_instrument_ids_rejects_unregistered_strategy() {
3430        let mut strategy = create_test_strategy();
3431
3432        let error = strategy
3433            .set_external_order_instrument_ids(vec![InstrumentId::from("AUDUSD.SIM")])
3434            .unwrap_err();
3435
3436        assert_eq!(
3437            error.to_string(),
3438            "Strategy TEST-001 is not registered with a trader"
3439        );
3440        assert!(strategy.core.config.external_order_instrument_ids.is_none());
3441    }
3442
3443    #[rstest]
3444    fn test_strategy_native_methods_are_available_on_strategy_type() {
3445        let mut strategy = create_test_strategy();
3446        register_strategy(&mut strategy);
3447
3448        drop(strategy.order_factory());
3449
3450        assert!(Rc::ptr_eq(
3451            &strategy.order_factory_rc(),
3452            strategy.core.order_factory.as_ref().unwrap()
3453        ));
3454        assert!(Rc::ptr_eq(
3455            &strategy.portfolio_rc(),
3456            strategy.core.portfolio.as_ref().unwrap()
3457        ));
3458    }
3459
3460    #[rstest]
3461    #[case(false, false, false, Some(log::Level::Warn))]
3462    #[case(false, false, true, Some(log::Level::Warn))]
3463    #[case(false, true, false, Some(log::Level::Warn))]
3464    #[case(false, true, true, Some(log::Level::Warn))]
3465    #[case(true, false, false, None)]
3466    #[case(true, false, true, Some(log::Level::Info))]
3467    #[case(true, true, false, Some(log::Level::Warn))]
3468    #[case(true, true, true, Some(log::Level::Warn))]
3469    fn test_handle_order_event_logs_and_dispatches_rejection(
3470        #[case] due_post_only: bool,
3471        #[case] log_rejected_due_post_only_as_warning: bool,
3472        #[case] log_events: bool,
3473        #[case] expected_level: Option<log::Level>,
3474    ) {
3475        static LOG_CAPTURE: OrderEventLogCapture = OrderEventLogCapture(Mutex::new(Vec::new()));
3476
3477        let mut strategy = create_test_strategy();
3478        strategy.core.config.log_rejected_due_post_only_as_warning =
3479            log_rejected_due_post_only_as_warning;
3480        strategy.core.actor.config.log_events = log_events;
3481        register_strategy(&mut strategy);
3482        start_strategy(&mut strategy);
3483
3484        let event = OrderEventAny::Rejected(
3485            OrderRejectedSpec::builder()
3486                .strategy_id(StrategyId::from("TEST-001"))
3487                .client_order_id(ClientOrderId::from("O-001"))
3488                .due_post_only(due_post_only)
3489                .build(),
3490        );
3491
3492        log::set_logger(&LOG_CAPTURE).unwrap();
3493        log::set_max_level(log::LevelFilter::Info);
3494        let expected_records: Vec<_> = expected_level
3495            .map(|level| (level, format!("TEST-001 {RECV}{EVT} {event}")))
3496            .into_iter()
3497            .collect();
3498
3499        strategy.handle_order_event(event);
3500
3501        assert_eq!(*LOG_CAPTURE.0.lock().unwrap(), expected_records);
3502        assert!(strategy.on_order_rejected_called);
3503        assert!(strategy.on_order_event_called);
3504    }
3505
3506    struct OrderEventLogCapture(Mutex<Vec<(log::Level, String)>>);
3507
3508    impl log::Log for OrderEventLogCapture {
3509        fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
3510            metadata.target() == "nautilus_trading::strategy"
3511        }
3512
3513        fn log(&self, record: &log::Record<'_>) {
3514            if self.enabled(record.metadata()) {
3515                self.0
3516                    .lock()
3517                    .unwrap()
3518                    .push((record.level(), record.args().to_string()));
3519            }
3520        }
3521
3522        fn flush(&self) {}
3523    }
3524
3525    #[rstest]
3526    fn test_dispatch_manager_actions_routes_every_action() {
3527        let mut strategy = create_test_strategy();
3528        register_strategy(&mut strategy);
3529        let strategy_id = StrategyId::from("TEST-001");
3530        let instrument_id = InstrumentId::from("BTCUSDT.BINANCE");
3531        let initialized_order = make_initialized_market_order("O-MANAGER-INIT");
3532        let emulator_order = OrderTestBuilder::new(OrderType::StopMarket)
3533            .trader_id(TraderId::from("TRADER-001"))
3534            .strategy_id(strategy_id)
3535            .instrument_id(instrument_id)
3536            .client_order_id(ClientOrderId::from("O-MANAGER-EMULATOR"))
3537            .side(OrderSide::Buy)
3538            .trigger_price(Price::from("51000.0"))
3539            .quantity(Quantity::from(100_000))
3540            .emulation_trigger(TriggerType::BidAsk)
3541            .build();
3542        let risk_order = make_initialized_market_order("O-MANAGER-RISK");
3543        let algorithm_order = make_initialized_algorithm_order("O-MANAGER-ALGORITHM");
3544        let cancel_order = OrderTestBuilder::new(OrderType::Limit)
3545            .trader_id(TraderId::from("TRADER-001"))
3546            .strategy_id(strategy_id)
3547            .instrument_id(instrument_id)
3548            .client_order_id(ClientOrderId::from("O-MANAGER-CANCEL"))
3549            .side(OrderSide::Buy)
3550            .price(Price::from("50000.0"))
3551            .quantity(Quantity::from(100_000))
3552            .submit(true)
3553            .build();
3554        let missing_cancel_order = OrderTestBuilder::new(OrderType::Limit)
3555            .trader_id(TraderId::from("TRADER-001"))
3556            .strategy_id(strategy_id)
3557            .instrument_id(instrument_id)
3558            .client_order_id(ClientOrderId::from("O-MANAGER-MISSING-CANCEL"))
3559            .side(OrderSide::Buy)
3560            .price(Price::from("50000.0"))
3561            .quantity(Quantity::from(100_000))
3562            .submit(true)
3563            .build();
3564        let modify_order = OrderTestBuilder::new(OrderType::Limit)
3565            .trader_id(TraderId::from("TRADER-001"))
3566            .strategy_id(strategy_id)
3567            .instrument_id(instrument_id)
3568            .client_order_id(ClientOrderId::from("O-MANAGER-MODIFY"))
3569            .side(OrderSide::Buy)
3570            .price(Price::from("50000.0"))
3571            .quantity(Quantity::from(100_000))
3572            .submit(true)
3573            .build();
3574        add_order_to_cache(&strategy, &cancel_order);
3575        add_order_to_cache(&strategy, &modify_order);
3576
3577        let (order_handler, order_events) =
3578            get_typed_message_saving_handler(Some(Ustr::from("manager-action-order-events")));
3579        let order_topic = format!("events.order.{strategy_id}");
3580        msgbus::subscribe_order_events(order_topic.clone().into(), order_handler.clone(), None);
3581        let (emulator_handler, emulator_messages): (
3582            _,
3583            TypedIntoMessageSavingHandler<TradingCommand>,
3584        ) = get_typed_into_message_saving_handler(Some(Ustr::from("OrderEmulator.execute")));
3585        msgbus::register_trading_command_endpoint(
3586            MessagingSwitchboard::order_emulator_execute(),
3587            emulator_handler,
3588        );
3589        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
3590            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
3591        msgbus::register_trading_command_endpoint(
3592            MessagingSwitchboard::risk_engine_queue_execute(),
3593            risk_handler,
3594        );
3595        let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
3596            get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
3597        msgbus::register_trading_command_endpoint(
3598            MessagingSwitchboard::exec_engine_queue_execute(),
3599            exec_handler,
3600        );
3601        let (algorithm_handler, algorithm_messages) =
3602            get_any_saving_handler::<TradingCommand>(Some(Ustr::from("TWAP.execute")));
3603        msgbus::register_any("TWAP.execute".into(), algorithm_handler);
3604
3605        strategy.dispatch_manager_actions(vec![
3606            OrderManagerAction::CancelLocal(missing_cancel_order),
3607            OrderManagerAction::PublishInitialized(OrderEventAny::Initialized(
3608                initialized_order.init_event().clone(),
3609            )),
3610            OrderManagerAction::SubmitToEmulator(make_submit_command(&emulator_order)),
3611            OrderManagerAction::SubmitToRisk(make_submit_command(&risk_order)),
3612            OrderManagerAction::SubmitToAlgorithm {
3613                command: make_submit_command(&algorithm_order),
3614                exec_algorithm_id: ExecAlgorithmId::from("TWAP"),
3615            },
3616            OrderManagerAction::CancelLocal(cancel_order.clone()),
3617            OrderManagerAction::ModifyLocalQuantity {
3618                order: modify_order.clone(),
3619                quantity: Quantity::from(50_000),
3620            },
3621            OrderManagerAction::ModifyLocalQuantity {
3622                order: modify_order.clone(),
3623                quantity: Quantity::from(100_000),
3624            },
3625        ]);
3626        msgbus::unsubscribe_order_events(order_topic.into(), &order_handler);
3627
3628        let order_events = order_events.get_messages();
3629        assert_eq!(order_events.len(), 4);
3630        assert!(matches!(
3631            &order_events[0],
3632            OrderEventAny::Initialized(event)
3633                if event.client_order_id == initialized_order.client_order_id()
3634        ));
3635        assert!(matches!(
3636            &order_events[1],
3637            OrderEventAny::PendingCancel(event)
3638                if event.client_order_id == cancel_order.client_order_id()
3639        ));
3640        assert!(matches!(
3641            &order_events[2],
3642            OrderEventAny::PendingUpdate(event)
3643                if event.client_order_id == modify_order.client_order_id()
3644        ));
3645        assert!(matches!(
3646            &order_events[3],
3647            OrderEventAny::PendingUpdate(event)
3648                if event.client_order_id == modify_order.client_order_id()
3649        ));
3650
3651        let pending_ids: Vec<_> = order_events
3652            .iter()
3653            .filter_map(|event| match event {
3654                OrderEventAny::PendingUpdate(event) => event.causation_id,
3655                _ => None,
3656            })
3657            .collect();
3658
3659        let dispatched_ids: Vec<_> = risk_messages
3660            .get_messages()
3661            .iter()
3662            .filter_map(|command| match command {
3663                TradingCommand::ModifyOrder(command) => Some(command.command_id),
3664                _ => None,
3665            })
3666            .collect();
3667
3668        assert_eq!(pending_ids, dispatched_ids);
3669        assert!(matches!(
3670            emulator_messages.get_messages().as_slice(),
3671            [TradingCommand::SubmitOrder(command)]
3672                if command.client_order_id == emulator_order.client_order_id()
3673        ));
3674        assert!(matches!(
3675            risk_messages.get_messages().as_slice(),
3676            [
3677                TradingCommand::SubmitOrder(submit),
3678                TradingCommand::ModifyOrder(first_modify),
3679                TradingCommand::ModifyOrder(second_modify),
3680            ]
3681
3682                if submit.client_order_id == risk_order.client_order_id()
3683                    && first_modify.client_order_id == modify_order.client_order_id()
3684                    && first_modify.quantity == Some(Quantity::from(50_000))
3685                    && second_modify.client_order_id == modify_order.client_order_id()
3686                    && second_modify.quantity == Some(Quantity::from(100_000))
3687        ));
3688        assert!(matches!(
3689            algorithm_messages.get_messages().as_slice(),
3690            [TradingCommand::SubmitOrder(command)]
3691                if command.client_order_id == algorithm_order.client_order_id()
3692        ));
3693        assert!(matches!(
3694            exec_messages.get_messages().as_slice(),
3695            [TradingCommand::CancelOrder(command)]
3696                if command.client_order_id == cancel_order.client_order_id()
3697        ));
3698    }
3699
3700    #[rstest]
3701    #[case::disabled(false)]
3702    #[case::enabled(true)]
3703    fn test_contingent_manager_dispatch_precedes_user_handlers_and_is_idempotent(
3704        #[case] manage_contingent_orders: bool,
3705    ) {
3706        let mut strategy = TestStrategy::new(StrategyConfig {
3707            strategy_id: Some(StrategyId::from("TEST-001")),
3708            order_id_tag: Some("001".to_string()),
3709            manage_contingent_orders,
3710            ..Default::default()
3711        });
3712        register_strategy(&mut strategy);
3713        start_strategy(&mut strategy);
3714        let timeline = strategy.order_event_timeline.clone();
3715        let endpoint_timeline = timeline.clone();
3716        let exec_handler = TypedIntoHandler::from(move |command: TradingCommand| {
3717            assert!(matches!(command, TradingCommand::CancelOrder(_)));
3718            endpoint_timeline.borrow_mut().push("endpoint");
3719        });
3720        msgbus::register_trading_command_endpoint(
3721            MessagingSwitchboard::exec_engine_queue_execute(),
3722            exec_handler,
3723        );
3724        let instrument_id = InstrumentId::from("BTCUSDT.BINANCE");
3725        let parent_id = ClientOrderId::from("O-CONTINGENT-PARENT");
3726        let sibling_id = ClientOrderId::from("O-CONTINGENT-SIBLING");
3727        let parent = OrderTestBuilder::new(OrderType::Limit)
3728            .trader_id(TraderId::from("TRADER-001"))
3729            .strategy_id(StrategyId::from("TEST-001"))
3730            .instrument_id(instrument_id)
3731            .client_order_id(parent_id)
3732            .side(OrderSide::Buy)
3733            .price(Price::from("50000.0"))
3734            .quantity(Quantity::from(100_000))
3735            .contingency_type(ContingencyType::Oco)
3736            .linked_order_ids(vec![parent_id, sibling_id])
3737            .submit(true)
3738            .build();
3739        let sibling = OrderTestBuilder::new(OrderType::Limit)
3740            .trader_id(TraderId::from("TRADER-001"))
3741            .strategy_id(StrategyId::from("TEST-001"))
3742            .instrument_id(instrument_id)
3743            .client_order_id(sibling_id)
3744            .side(OrderSide::Sell)
3745            .price(Price::from("51000.0"))
3746            .quantity(Quantity::from(100_000))
3747            .submit(true)
3748            .build();
3749        add_order_to_cache(&strategy, &parent);
3750        add_order_to_cache(&strategy, &sibling);
3751        let event = OrderEventAny::Filled(
3752            OrderFilledSpec::builder()
3753                .trader_id(parent.trader_id())
3754                .strategy_id(parent.strategy_id())
3755                .instrument_id(parent.instrument_id())
3756                .client_order_id(parent.client_order_id())
3757                .venue_order_id(VenueOrderId::from("V-CONTINGENT-PARENT"))
3758                .account_id(AccountId::from("ACCOUNT-001"))
3759                .trade_id(TradeId::from("T-CONTINGENT-PARENT"))
3760                .order_side(parent.order_side())
3761                .order_type(parent.order_type())
3762                .last_qty(parent.quantity())
3763                .last_px(Price::from("50000.0"))
3764                .liquidity_side(LiquiditySide::Taker)
3765                .build(),
3766        );
3767        strategy
3768            .core
3769            .cache_rc()
3770            .borrow_mut()
3771            .update_order(&event)
3772            .unwrap();
3773
3774        strategy.handle_order_event(event.clone());
3775        strategy.handle_order_event(event);
3776
3777        let timeline = timeline.borrow();
3778        let sibling_status = strategy
3779            .core
3780            .cache_ref()
3781            .order(&sibling_id)
3782            .unwrap()
3783            .status();
3784
3785        if manage_contingent_orders {
3786            assert_eq!(
3787                timeline.as_slice(),
3788                ["endpoint", "specific", "aggregate", "specific", "aggregate"]
3789            );
3790            assert_eq!(sibling_status, OrderStatus::PendingCancel);
3791        } else {
3792            assert_eq!(
3793                timeline.as_slice(),
3794                ["specific", "aggregate", "specific", "aggregate"]
3795            );
3796            assert_eq!(sibling_status, OrderStatus::Submitted);
3797        }
3798    }
3799
3800    #[rstest]
3801    fn test_handle_order_fill_voided_dispatches_to_specific_handler() {
3802        let mut strategy = create_test_strategy();
3803        register_strategy(&mut strategy);
3804        start_strategy(&mut strategy);
3805
3806        strategy.handle_order_event(make_fill_voided(ClientOrderId::from("O-001"), false));
3807
3808        assert!(strategy.on_order_fill_voided_called);
3809        assert!(strategy.on_order_event_called);
3810    }
3811
3812    #[rstest]
3813    #[case::opened(make_position_opened())]
3814    #[case::changed(make_position_changed())]
3815    #[case::closed(make_position_closed())]
3816    fn test_handle_position_event_dispatches_to_handler(#[case] event: PositionEvent) {
3817        let mut strategy = create_test_strategy();
3818        register_strategy(&mut strategy);
3819        start_strategy(&mut strategy);
3820
3821        let expected_opened = matches!(event, PositionEvent::PositionOpened(_));
3822        let expected_changed = matches!(event, PositionEvent::PositionChanged(_));
3823        let expected_closed = matches!(event, PositionEvent::PositionClosed(_));
3824
3825        strategy.handle_position_event(event);
3826
3827        assert_eq!(strategy.on_position_opened_called, expected_opened);
3828        assert_eq!(strategy.on_position_changed_called, expected_changed);
3829        assert_eq!(strategy.on_position_closed_called, expected_closed);
3830        assert!(strategy.on_position_event_called);
3831    }
3832
3833    #[rstest]
3834    fn test_handle_position_event_skips_dispatch_when_stopped() {
3835        let mut strategy = create_test_strategy();
3836        register_strategy(&mut strategy);
3837        start_strategy(&mut strategy);
3838        stop_strategy(&mut strategy);
3839        assert_eq!(strategy.state(), ComponentState::Stopped);
3840
3841        strategy.handle_position_event(make_position_opened());
3842
3843        assert!(!strategy.on_position_event_called);
3844        assert!(!strategy.on_position_opened_called);
3845    }
3846
3847    #[rstest]
3848    fn test_handle_position_event_skips_dispatch_for_adjusted() {
3849        let mut strategy = create_test_strategy();
3850        register_strategy(&mut strategy);
3851        start_strategy(&mut strategy);
3852
3853        strategy.handle_position_event(make_position_adjusted());
3854
3855        assert!(!strategy.on_position_event_called);
3856        assert!(!strategy.on_position_opened_called);
3857        assert!(!strategy.on_position_changed_called);
3858        assert!(!strategy.on_position_closed_called);
3859    }
3860
3861    #[rstest]
3862    fn test_strategy_default_handlers_do_not_panic() {
3863        let mut strategy = create_test_strategy();
3864
3865        strategy.on_order_initialized(OrderInitialized::default());
3866        strategy.on_order_event(OrderEventAny::Accepted(OrderAccepted::default()));
3867        strategy.on_order_denied(OrderDenied::default());
3868        strategy.on_order_emulated(OrderEmulated::default());
3869        strategy.on_order_released(OrderReleased::default());
3870        strategy.on_order_submitted(OrderSubmitted::default());
3871        strategy.on_order_rejected(OrderRejected::default());
3872        strategy.on_order_canceled(&OrderCanceled::default());
3873        strategy.on_order_expired(OrderExpired::default());
3874        strategy.on_order_triggered(OrderTriggered::default());
3875        strategy.on_order_pending_update(OrderPendingUpdate::default());
3876        strategy.on_order_pending_cancel(OrderPendingCancel::default());
3877        strategy.on_order_modify_rejected(OrderModifyRejected::default());
3878        strategy.on_order_cancel_rejected(OrderCancelRejected::default());
3879        strategy.on_order_updated(OrderUpdated::default());
3880        strategy.on_order_filled(&OrderFilledSpec::builder().build());
3881        strategy.on_order_fill_voided(&OrderFillVoidedSpec::builder().build());
3882        strategy.on_position_event(make_position_opened());
3883    }
3884
3885    #[rstest]
3886    #[case::submitted(OrderStatus::Submitted, false)]
3887    #[case::accepted(OrderStatus::Accepted, false)]
3888    #[case::accepted_exiting(OrderStatus::Accepted, true)]
3889    #[case::canceled(OrderStatus::Canceled, false)]
3890    fn test_submit_order_rejects_retained_initialized_order_with_non_local_cached_order(
3891        #[case] status: OrderStatus,
3892        #[case] exiting: bool,
3893    ) {
3894        let mut strategy = create_test_strategy();
3895        register_strategy(&mut strategy);
3896        let order = make_initialized_market_order("O-RETAINED-ACCEPTED");
3897        let client_order_id = order.client_order_id();
3898        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
3899            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
3900        msgbus::register_trading_command_endpoint(
3901            MessagingSwitchboard::risk_engine_queue_execute(),
3902            risk_handler,
3903        );
3904        let topic = format!("events.order.{}", order.strategy_id());
3905        let (event_handler, event_messages): (_, TypedMessageSavingHandler<OrderEventAny>) =
3906            get_typed_message_saving_handler(Some(Ustr::from("events.order.retained")));
3907        msgbus::subscribe_order_events(topic.clone().into(), event_handler.clone(), None);
3908
3909        strategy
3910            .submit_order(order.clone(), None, None, None)
3911            .unwrap();
3912        let cache_rc = strategy.core.cache_rc();
3913        let submitted = TestOrderEventStubs::submitted(&order, AccountId::from("ACC-001"));
3914        cache_rc.borrow_mut().update_order(&submitted).unwrap();
3915        if status != OrderStatus::Submitted {
3916            cache_rc
3917                .borrow_mut()
3918                .update_order(&make_accepted(client_order_id))
3919                .unwrap();
3920        }
3921
3922        if status == OrderStatus::Canceled {
3923            cache_rc
3924                .borrow_mut()
3925                .update_order(&make_canceled(client_order_id))
3926                .unwrap();
3927        }
3928
3929        let before = cache_rc.borrow().order(&client_order_id).unwrap().clone();
3930        let snapshot = serde_json::to_value(&before).unwrap();
3931
3932        strategy.core.is_exiting = exiting;
3933        let result = strategy.submit_order(order.clone(), None, None, None);
3934
3935        msgbus::unsubscribe_order_events(topic.into(), &event_handler);
3936        let cache = cache_rc.borrow();
3937        let cached = cache.order(&client_order_id).unwrap();
3938        assert_eq!(
3939            result.unwrap_err().to_string(),
3940            format!("Order denied: duplicate {client_order_id}")
3941        );
3942        assert_eq!(risk_messages.get_messages().len(), 1);
3943        assert_eq!(
3944            event_messages.get_messages(),
3945            vec![OrderEventAny::Initialized(order.init_event().clone())]
3946        );
3947        assert_eq!(cached.status(), status);
3948        assert_eq!(cached.events(), before.events());
3949        assert_eq!(serde_json::to_value(&*cached).unwrap(), snapshot);
3950    }
3951
3952    #[rstest]
3953    #[case::initialized(OrderStatus::Initialized)]
3954    #[case::emulated(OrderStatus::Emulated)]
3955    #[case::released(OrderStatus::Released)]
3956    fn test_submit_order_replaces_active_local_cached_order(#[case] status: OrderStatus) {
3957        let mut strategy = create_test_strategy();
3958        register_strategy(&mut strategy);
3959        let client_order_id = ClientOrderId::from("O-LOCAL-REPLACE");
3960        let mut local = OrderTestBuilder::new(OrderType::StopMarket)
3961            .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
3962            .strategy_id(StrategyId::from("TEST-001"))
3963            .client_order_id(client_order_id)
3964            .side(OrderSide::Sell)
3965            .quantity(Quantity::from("1.25"))
3966            .trigger_price(Price::from("51000.0"))
3967            .emulation_trigger(TriggerType::BidAsk)
3968            .build();
3969
3970        if status == OrderStatus::Emulated {
3971            let event = OrderEmulatedSpec::builder()
3972                .trader_id(local.trader_id())
3973                .strategy_id(local.strategy_id())
3974                .instrument_id(local.instrument_id())
3975                .client_order_id(client_order_id)
3976                .build();
3977            local.apply(OrderEventAny::Emulated(event)).unwrap();
3978        } else if status == OrderStatus::Released {
3979            let event = OrderReleasedSpec::builder()
3980                .trader_id(local.trader_id())
3981                .strategy_id(local.strategy_id())
3982                .instrument_id(local.instrument_id())
3983                .client_order_id(client_order_id)
3984                .released_price(Price::from("51001.0"))
3985                .build();
3986            local.apply(OrderEventAny::Released(event)).unwrap();
3987        }
3988
3989        let cache_rc = strategy.core.cache_rc();
3990        cache_rc
3991            .borrow_mut()
3992            .add_order(local.clone(), None, None, false)
3993            .unwrap();
3994        let order = OrderTestBuilder::new(OrderType::Market)
3995            .instrument_id(local.instrument_id())
3996            .strategy_id(local.strategy_id())
3997            .client_order_id(client_order_id)
3998            .side(OrderSide::Buy)
3999            .quantity(Quantity::from("2.50"))
4000            .build();
4001        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4002            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4003        msgbus::register_trading_command_endpoint(
4004            MessagingSwitchboard::risk_engine_queue_execute(),
4005            risk_handler,
4006        );
4007
4008        strategy
4009            .submit_order(order.clone(), None, None, None)
4010            .unwrap();
4011
4012        let cache = cache_rc.borrow();
4013        let cached = cache.order(&client_order_id).unwrap();
4014        assert_eq!(local.status(), status);
4015        assert_eq!(cached.status(), OrderStatus::Initialized);
4016        assert_eq!(cached.events(), order.events());
4017        assert_eq!(
4018            serde_json::to_value(&*cached).unwrap(),
4019            serde_json::to_value(&order).unwrap()
4020        );
4021        let messages = risk_messages.get_messages();
4022        assert_eq!(messages.len(), 1);
4023
4024        let TradingCommand::SubmitOrder(command) = &messages[0] else {
4025            panic!("Expected SubmitOrder");
4026        };
4027
4028        assert_eq!(command.order_init, *order.init_event());
4029    }
4030
4031    #[rstest]
4032    fn test_submit_order_publishes_order_initialized_after_cache_insert_before_send() {
4033        let mut strategy = create_test_strategy();
4034        register_strategy(&mut strategy);
4035
4036        let order = make_initialized_market_order("O-20250208-INIT-001");
4037        let client_order_id = order.client_order_id();
4038        let cache_rc = strategy.core.cache_rc();
4039        let old_instrument_id = InstrumentId::from("ETHUSDT.KRAKEN");
4040        let old_order = OrderTestBuilder::new(OrderType::Market)
4041            .instrument_id(old_instrument_id)
4042            .client_order_id(client_order_id)
4043            .strategy_id(order.strategy_id())
4044            .side(OrderSide::Buy)
4045            .quantity(Quantity::from(1))
4046            .build();
4047        cache_rc
4048            .borrow_mut()
4049            .add_order(old_order, None, None, false)
4050            .unwrap();
4051        let new_instrument_id = order.instrument_id();
4052        let timeline = Rc::new(RefCell::new(Vec::new()));
4053        let event_messages = Rc::new(RefCell::new(Vec::new()));
4054
4055        let event_handler = {
4056            let event_messages = event_messages.clone();
4057            let timeline = timeline.clone();
4058            TypedHandler::from_with_id("events.order.initialized", move |event: &OrderEventAny| {
4059                assert!(cache_rc.borrow().order_exists(&client_order_id));
4060                assert!(
4061                    cache_rc
4062                        .borrow()
4063                        .orders(None, Some(&old_instrument_id), None, None, None)
4064                        .is_empty()
4065                );
4066                assert_eq!(
4067                    cache_rc
4068                        .borrow()
4069                        .orders(None, Some(&new_instrument_id), None, None, None)
4070                        .len(),
4071                    1,
4072                );
4073                assert!(matches!(event, OrderEventAny::Initialized(_)));
4074                event_messages.borrow_mut().push(event.clone());
4075                timeline.borrow_mut().push("init");
4076            })
4077        };
4078        let risk_handler = {
4079            let timeline = timeline.clone();
4080            TypedIntoHandler::from_with_id(
4081                "RiskEngine.queue_execute",
4082                move |command: TradingCommand| {
4083                    assert!(matches!(command, TradingCommand::SubmitOrder(_)));
4084                    timeline.borrow_mut().push("command");
4085                },
4086            )
4087        };
4088        msgbus::register_trading_command_endpoint(
4089            MessagingSwitchboard::risk_engine_queue_execute(),
4090            risk_handler,
4091        );
4092
4093        let topic = format!("events.order.{}", order.strategy_id());
4094        msgbus::subscribe_order_events(topic.clone().into(), event_handler.clone(), None);
4095
4096        strategy
4097            .submit_order(order.clone(), None, None, None)
4098            .unwrap();
4099
4100        msgbus::unsubscribe_order_events(topic.into(), &event_handler);
4101
4102        let event_messages = event_messages.borrow();
4103        assert_eq!(event_messages.len(), 1);
4104        assert_eq!(
4105            event_messages[0],
4106            OrderEventAny::Initialized(order.init_event().clone())
4107        );
4108        assert_eq!(timeline.borrow().as_slice(), &["init", "command"]);
4109    }
4110
4111    #[rstest]
4112    #[case::single("single", "1.234570", "0.00000211 BTC", "0.000001", Some("1.234567"))]
4113    #[case::all("all", "1.234570", "0.00000211 BTC", "0.000001", Some("1.234567"))]
4114    #[case::single_dust("single", "0.000001", "0.00000011 BTC", "0.000001", None)]
4115    #[case::all_dust("all", "0.000001", "0.00000011 BTC", "0.000001", None)]
4116    #[case::retry("retry", "1.234570", "0.00000211 BTC", "0.000001", Some("1.234567"))]
4117    #[case::retry_dust("retry", "0.000001", "0.00000011 BTC", "0.000001", None)]
4118    #[case::single_increment("single", "1.00", "0.001 BTC", "0.50", Some("0.50"))]
4119    #[case::all_increment("all", "1.00", "0.001 BTC", "0.50", Some("0.50"))]
4120    #[case::retry_increment("retry", "1.00", "0.001 BTC", "0.50", Some("0.50"))]
4121    #[case::single_increment_dust("single", "0.50", "0.001 BTC", "0.50", None)]
4122    #[case::all_increment_dust("all", "0.50", "0.001 BTC", "0.50", None)]
4123    #[case::retry_increment_dust("retry", "0.50", "0.001 BTC", "0.50", None)]
4124    #[case::single_increment_same_precision("single", "1.00", "0.25 BTC", "0.50", Some("0.50"))]
4125    fn test_close_positions_retains_fee_dust(
4126        #[case] operation: &str,
4127        #[case] qty: &str,
4128        #[case] fee: &str,
4129        #[case] increment: &str,
4130        #[case] expected_qty: Option<&str>,
4131    ) {
4132        let mut strategy = create_test_strategy();
4133        register_strategy(&mut strategy);
4134        let mut pair = currency_pair_btcusdt();
4135        pair.size_increment = Quantity::from(increment);
4136        pair.size_precision = pair.size_increment.precision;
4137        let instrument = InstrumentAny::CurrencyPair(pair);
4138        let fill = OrderFilledSpec::builder()
4139            .strategy_id(strategy.core.strategy_id().unwrap())
4140            .instrument_id(instrument.id())
4141            .position_id(PositionId::from("P-CLOSE-DUST"))
4142            .last_qty(Quantity::from(qty))
4143            .last_px(Price::from("10000.00"))
4144            .commission(Money::from_str_exact(fee).unwrap())
4145            .build();
4146        let position = Position::new(&instrument, fill);
4147        let cache = strategy.core.cache_rc();
4148        cache
4149            .borrow_mut()
4150            .add_instrument(instrument.clone())
4151            .unwrap();
4152        cache
4153            .borrow_mut()
4154            .add_position(&position, OmsType::Hedging)
4155            .unwrap();
4156        let (handler, messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4157            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4158        msgbus::register_trading_command_endpoint(
4159            MessagingSwitchboard::risk_engine_queue_execute(),
4160            handler,
4161        );
4162
4163        match operation {
4164            "all" => strategy
4165                .close_all_positions(instrument.id(), None, None, None, None, None, None, None)
4166                .unwrap(),
4167            "retry" => {
4168                strategy.core.is_exiting = true;
4169
4170                let event = TimeEvent::new(
4171                    strategy.core.market_exit_timer_name,
4172                    UUID4::new(),
4173                    UnixNanos::default(),
4174                    UnixNanos::default(),
4175                );
4176                strategy.check_market_exit(event);
4177            }
4178            _ => strategy
4179                .close_position(&position, None, None, None, None, None, None)
4180                .unwrap(),
4181        }
4182
4183        assert_eq!(
4184            strategy.core.is_exiting,
4185            operation == "retry" && expected_qty.is_some()
4186        );
4187        let messages = messages.get_messages();
4188
4189        if let Some(expected) = expected_qty {
4190            assert_eq!(messages.len(), 1);
4191
4192            let TradingCommand::SubmitOrder(command) = &messages[0] else {
4193                panic!("expected close order")
4194            };
4195
4196            let cache = cache.borrow();
4197            let order = cache.order(&command.client_order_id).unwrap();
4198            assert_eq!(order.quantity(), Quantity::from(expected));
4199            assert_eq!(order.order_side(), OrderSide::Sell);
4200            assert!(order.is_reduce_only());
4201            assert_eq!(command.position_id, Some(position.id));
4202        } else {
4203            assert!(messages.is_empty());
4204        }
4205
4206        assert_eq!(
4207            cache.borrow().position(&position.id).unwrap().signed_qty,
4208            position.signed_qty
4209        );
4210    }
4211
4212    #[rstest]
4213    fn test_market_exit_retry_sorts_and_deduplicates_instruments() {
4214        let mut strategy = create_test_strategy();
4215        register_strategy(&mut strategy);
4216        let cache = strategy.core.cache_rc();
4217
4218        for index in (0..12).rev() {
4219            let mut pair = currency_pair_btcusdt();
4220            pair.id = InstrumentId::from(format!("BTCUSDT.VENUE{index:02}"));
4221            let instrument = InstrumentAny::CurrencyPair(pair);
4222            cache
4223                .borrow_mut()
4224                .add_instrument(instrument.clone())
4225                .unwrap();
4226            let fill = OrderFilledSpec::builder()
4227                .strategy_id(strategy.core.strategy_id().unwrap())
4228                .instrument_id(instrument.id())
4229                .client_order_id(ClientOrderId::from(format!("O-SORT-{index:02}")))
4230                .position_id(PositionId::from(format!("P-SORT-{:02}", 11 - index)))
4231                .last_qty(Quantity::from(1))
4232                .last_px(Price::from("10000.00"))
4233                .currency(instrument.quote_currency())
4234                .commission(Money::zero(instrument.quote_currency()))
4235                .build();
4236            let position = Position::new(&instrument, fill.clone());
4237            cache
4238                .borrow_mut()
4239                .add_position(&position, OmsType::Hedging)
4240                .unwrap();
4241
4242            if index == 0 {
4243                let mut duplicate = fill;
4244                duplicate.position_id = Some(PositionId::from("Z-SORT-DUP"));
4245                duplicate.client_order_id = ClientOrderId::from("O-SORT-DUP");
4246                let position = Position::new(&instrument, duplicate);
4247                cache
4248                    .borrow_mut()
4249                    .add_position(&position, OmsType::Hedging)
4250                    .unwrap();
4251            }
4252        }
4253
4254        let (handler, messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4255            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4256        msgbus::register_trading_command_endpoint(
4257            MessagingSwitchboard::risk_engine_queue_execute(),
4258            handler,
4259        );
4260        strategy.core.is_exiting = true;
4261
4262        let event = TimeEvent::new(
4263            strategy.core.market_exit_timer_name,
4264            UUID4::new(),
4265            UnixNanos::default(),
4266            UnixNanos::default(),
4267        );
4268
4269        strategy.check_market_exit(event);
4270
4271        let commands: Vec<_> = messages
4272            .get_messages()
4273            .iter()
4274            .map(|message| {
4275                let TradingCommand::SubmitOrder(command) = message else {
4276                    panic!("expected close command")
4277                };
4278
4279                (command.instrument_id, command.position_id.unwrap())
4280            })
4281            .collect();
4282
4283        let mut expected = vec![
4284            (
4285                InstrumentId::from("BTCUSDT.VENUE00"),
4286                PositionId::from("P-SORT-11"),
4287            ),
4288            (
4289                InstrumentId::from("BTCUSDT.VENUE00"),
4290                PositionId::from("Z-SORT-DUP"),
4291            ),
4292        ];
4293        expected.extend((1..12).map(|index| {
4294            (
4295                InstrumentId::from(format!("BTCUSDT.VENUE{index:02}")),
4296                PositionId::from(format!("P-SORT-{:02}", 11 - index)),
4297            )
4298        }));
4299
4300        assert_eq!(commands, expected);
4301    }
4302
4303    #[rstest]
4304    fn test_submit_order_routes_emulated_order_to_order_emulator() {
4305        let mut strategy = create_test_strategy();
4306        register_strategy(&mut strategy);
4307        let (emulator_handler, emulator_messages): (
4308            _,
4309            TypedIntoMessageSavingHandler<TradingCommand>,
4310        ) = get_typed_into_message_saving_handler(Some(Ustr::from("OrderEmulator.execute")));
4311        msgbus::register_trading_command_endpoint(
4312            MessagingSwitchboard::order_emulator_execute(),
4313            emulator_handler,
4314        );
4315        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4316            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4317        msgbus::register_trading_command_endpoint(
4318            MessagingSwitchboard::risk_engine_queue_execute(),
4319            risk_handler,
4320        );
4321        let order = OrderTestBuilder::new(OrderType::StopMarket)
4322            .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
4323            .client_order_id(ClientOrderId::from("O-20250208-EMULATED-001"))
4324            .side(OrderSide::Buy)
4325            .trigger_price(Price::from("51000.0"))
4326            .quantity(Quantity::from(100_000))
4327            .emulation_trigger(TriggerType::BidAsk)
4328            .build();
4329        let client_order_id = order.client_order_id();
4330
4331        strategy.submit_order(order, None, None, None).unwrap();
4332
4333        let emulator_messages = emulator_messages.get_messages();
4334        assert_eq!(emulator_messages.len(), 1);
4335        assert!(matches!(
4336            emulator_messages.first(),
4337            Some(TradingCommand::SubmitOrder(command))
4338                if command.client_order_id == client_order_id
4339        ));
4340        assert!(risk_messages.get_messages().is_empty());
4341    }
4342
4343    #[rstest]
4344    fn test_submit_order_errors_when_strategy_not_registered() {
4345        let mut strategy = create_test_strategy();
4346        let order = make_initialized_market_order("O-20250208-UNREGISTERED-001");
4347
4348        let err = strategy
4349            .submit_order(order, None, None, None)
4350            .unwrap_err()
4351            .to_string();
4352
4353        assert_eq!(err, "Strategy not registered: trader_id is not set");
4354    }
4355
4356    #[rstest]
4357    fn test_submit_order_uses_stored_strategy_id_when_actor_id_diverges() {
4358        let mut strategy = create_test_strategy();
4359        register_strategy(&mut strategy);
4360        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4361            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4362        msgbus::register_trading_command_endpoint(
4363            MessagingSwitchboard::risk_engine_queue_execute(),
4364            risk_handler,
4365        );
4366
4367        // A hyphenless actor ID has no strategy ID form, so any re-parse of it panics
4368        strategy.core.actor.actor_id = ActorId::from("Strategy");
4369        let order = make_initialized_market_order("O-20250208-DIVERGED-001");
4370
4371        strategy.submit_order(order, None, None, None).unwrap();
4372
4373        let risk_messages = risk_messages.get_messages();
4374        assert_eq!(risk_messages.len(), 1);
4375        let Some(TradingCommand::SubmitOrder(command)) = risk_messages.first() else {
4376            panic!("Expected a SubmitOrder command, was {risk_messages:?}");
4377        };
4378        assert_eq!(command.strategy_id, StrategyId::from("TEST-001"));
4379        assert_eq!(
4380            command.client_order_id,
4381            ClientOrderId::from("O-20250208-DIVERGED-001")
4382        );
4383    }
4384
4385    #[rstest]
4386    fn test_required_account_id_errors_when_missing_for_strategy_event() {
4387        let order = make_initialized_market_order("O-20250208-NO-ACCOUNT-001");
4388
4389        let err = required_account_id(&order, "pending cancel")
4390            .unwrap_err()
4391            .to_string();
4392
4393        assert_eq!(
4394            err,
4395            "Cannot generate pending cancel event for O-20250208-NO-ACCOUNT-001: \
4396             account_id is not set"
4397        );
4398    }
4399
4400    #[rstest]
4401    fn test_submit_order_rejects_non_initialized_without_events() {
4402        let mut strategy = create_test_strategy();
4403        register_strategy(&mut strategy);
4404
4405        let order = make_accepted_market_order("O-20250208-ACCEPTED-001");
4406        let topic = format!("events.order.{}", order.strategy_id());
4407        let (event_handler, event_messages): (_, TypedMessageSavingHandler<OrderEventAny>) =
4408            get_typed_message_saving_handler(Some(Ustr::from("events.order.invalid")));
4409
4410        msgbus::subscribe_order_events(topic.clone().into(), event_handler.clone(), None);
4411        let result = strategy.submit_order(order, None, None, None);
4412
4413        msgbus::unsubscribe_order_events(topic.into(), &event_handler);
4414
4415        assert!(result.is_err());
4416        assert!(
4417            result
4418                .unwrap_err()
4419                .to_string()
4420                .contains("expected INITIALIZED")
4421        );
4422        assert!(event_messages.get_messages().is_empty());
4423    }
4424
4425    #[rstest]
4426    fn test_submit_order_returns_error_when_cache_already_borrowed() {
4427        let mut strategy = create_test_strategy();
4428        register_strategy(&mut strategy);
4429
4430        let order = make_initialized_market_order("O-20250208-BORROWED-001");
4431        let cache_rc = strategy.core.cache_rc();
4432        let _cache = cache_rc.borrow();
4433
4434        let result = catch_unwind(AssertUnwindSafe(|| {
4435            strategy.submit_order(order, None, None, None)
4436        }));
4437
4438        let err = result
4439            .expect("submit_order should not panic")
4440            .unwrap_err()
4441            .to_string();
4442
4443        assert_eq!(
4444            err,
4445            "Cannot submit order O-20250208-BORROWED-001: cache is currently borrowed"
4446        );
4447    }
4448
4449    #[rstest]
4450    fn test_submit_order_list_publishes_order_initialized_after_cache_insert_before_send() {
4451        let mut strategy = create_test_strategy();
4452        register_strategy(&mut strategy);
4453
4454        let order_list_id = OrderListId::from("OL-20250208-LIST-INIT");
4455        let mut orders = vec![
4456            make_initialized_market_order("O-20250208-LIST-INIT-001"),
4457            make_initialized_market_order("O-20250208-LIST-INIT-002"),
4458        ];
4459
4460        for order in &mut orders {
4461            order.set_order_list_id(order_list_id);
4462        }
4463
4464        let client_order_id1 = orders[0].client_order_id();
4465        let client_order_id2 = orders[1].client_order_id();
4466        let cache_rc = strategy.core.cache_rc();
4467        let timeline = Rc::new(RefCell::new(Vec::new()));
4468        let event_messages = Rc::new(RefCell::new(Vec::new()));
4469
4470        let event_handler = {
4471            let event_messages = event_messages.clone();
4472            let timeline = timeline.clone();
4473            TypedHandler::from_with_id(
4474                "events.order.list_initialized",
4475                move |event: &OrderEventAny| {
4476                    match event {
4477                        OrderEventAny::Initialized(e) if e.client_order_id == client_order_id1 => {
4478                            let cache = cache_rc.borrow();
4479                            assert!(cache.order_exists(&client_order_id1));
4480                            assert!(cache.order_exists(&client_order_id2));
4481                            assert!(cache.order_list_exists(&order_list_id));
4482                            let order_list = cache.order_list(&order_list_id).unwrap();
4483                            assert_eq!(
4484                                order_list.client_order_ids.as_slice(),
4485                                &[client_order_id1, client_order_id2]
4486                            );
4487                            timeline.borrow_mut().push("init1");
4488                        }
4489                        OrderEventAny::Initialized(e) if e.client_order_id == client_order_id2 => {
4490                            assert!(cache_rc.borrow().order_exists(&client_order_id2));
4491                            timeline.borrow_mut().push("init2");
4492                        }
4493                        _ => panic!("unexpected order event {event:?}"),
4494                    }
4495                    event_messages.borrow_mut().push(event.clone());
4496                },
4497            )
4498        };
4499        let risk_handler = {
4500            let timeline = timeline.clone();
4501            TypedIntoHandler::from_with_id(
4502                "RiskEngine.queue_execute",
4503                move |command: TradingCommand| {
4504                    assert!(matches!(command, TradingCommand::SubmitOrderList(_)));
4505                    timeline.borrow_mut().push("command");
4506                },
4507            )
4508        };
4509        msgbus::register_trading_command_endpoint(
4510            MessagingSwitchboard::risk_engine_queue_execute(),
4511            risk_handler,
4512        );
4513
4514        let topic = format!("events.order.{}", orders[0].strategy_id());
4515        msgbus::subscribe_order_events(topic.clone().into(), event_handler.clone(), None);
4516
4517        strategy
4518            .submit_order_list(orders.clone(), None, None, None)
4519            .unwrap();
4520
4521        msgbus::unsubscribe_order_events(topic.into(), &event_handler);
4522
4523        let event_messages = event_messages.borrow();
4524        assert_eq!(event_messages.len(), 2);
4525        assert_eq!(
4526            event_messages[0],
4527            OrderEventAny::Initialized(orders[0].init_event().clone())
4528        );
4529        assert_eq!(
4530            event_messages[1],
4531            OrderEventAny::Initialized(orders[1].init_event().clone())
4532        );
4533        assert_eq!(timeline.borrow().as_slice(), &["init1", "init2", "command"]);
4534    }
4535
4536    #[rstest]
4537    fn test_submission_persistence_failure_does_not_block_dispatch(
4538        #[values(false, true)] fail_position_index: bool,
4539        #[values(false, true)] list: bool,
4540    ) {
4541        let mut strategy = create_test_strategy();
4542        register_strategy(&mut strategy);
4543        let (database, control) = FailNthAddOrderDatabase::create();
4544        let cache_rc = strategy.core.cache_rc();
4545        cache_rc.borrow_mut().set_database(Box::new(database));
4546
4547        let order_count = if list { 2 } else { 1 };
4548        if fail_position_index {
4549            control.set_fail_index_order_position_on(Some(order_count));
4550        } else {
4551            control.set_fail_add_order_on(Some(order_count));
4552        }
4553
4554        let position_id = PositionId::from("P-LIST-RETRY");
4555        let client_id = ClientId::from("EXEC-LIST-RETRY");
4556        let order_list_id = OrderListId::from("OL-LIST-RETRY");
4557        let mut orders = vec![
4558            make_initialized_market_order("O-LIST-RETRY-001"),
4559            make_initialized_market_order("O-LIST-RETRY-002"),
4560        ];
4561
4562        orders.truncate(order_count);
4563        if list {
4564            for order in &mut orders {
4565                order.set_order_list_id(order_list_id);
4566            }
4567        }
4568
4569        let topic = format!("events.order.{}", orders[0].strategy_id());
4570        let (event_handler, event_messages): (_, TypedMessageSavingHandler<OrderEventAny>) =
4571            get_typed_message_saving_handler(Some(Ustr::from("events.order.list_retry")));
4572        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4573            get_typed_into_message_saving_handler(Some(Ustr::from("risk.list_retry")));
4574        msgbus::subscribe_order_events(topic.clone().into(), event_handler.clone(), None);
4575        msgbus::register_trading_command_endpoint(
4576            MessagingSwitchboard::risk_engine_queue_execute(),
4577            risk_handler,
4578        );
4579
4580        if list {
4581            strategy
4582                .submit_order_list(orders.clone(), Some(position_id), Some(client_id), None)
4583                .unwrap();
4584        } else {
4585            strategy
4586                .submit_order(orders[0].clone(), Some(position_id), Some(client_id), None)
4587                .unwrap();
4588        }
4589
4590        msgbus::unsubscribe_order_events(topic.into(), &event_handler);
4591
4592        let cache = cache_rc.borrow();
4593        assert_eq!(control.calls(), (order_count, order_count));
4594        assert_eq!(
4595            cache.order_lists(None, None, None, None).len(),
4596            usize::from(list)
4597        );
4598
4599        if list {
4600            assert_eq!(
4601                cache.order_list(&order_list_id).unwrap().client_order_ids,
4602                orders
4603                    .iter()
4604                    .map(Order::client_order_id)
4605                    .collect::<Vec<_>>()
4606            );
4607        }
4608
4609        for order in &orders {
4610            let cached = cache.order(&order.client_order_id()).unwrap();
4611            assert_eq!(
4612                serde_json::to_value(&*cached).unwrap(),
4613                serde_json::to_value(order).unwrap()
4614            );
4615            assert_eq!(
4616                cache.position_id(&order.client_order_id()),
4617                Some(&position_id)
4618            );
4619            assert_eq!(cache.client_id(&order.client_order_id()), Some(&client_id));
4620        }
4621
4622        assert_eq!(cache.orders_for_position(&position_id).len(), order_count);
4623        assert_eq!(
4624            event_messages.get_messages(),
4625            orders
4626                .iter()
4627                .map(|order| OrderEventAny::Initialized(order.init_event().clone()))
4628                .collect::<Vec<_>>()
4629        );
4630        let commands = risk_messages.get_messages();
4631        assert_eq!(commands.len(), 1);
4632
4633        match (&commands[0], list) {
4634            (TradingCommand::SubmitOrderList(command), true) => {
4635                assert_eq!(command.order_list.id, order_list_id);
4636                assert_eq!(command.position_id, Some(position_id));
4637                assert_eq!(command.client_id, Some(client_id));
4638                assert_eq!(
4639                    command.order_inits,
4640                    orders
4641                        .iter()
4642                        .map(|order| order.init_event().clone())
4643                        .collect::<Vec<_>>()
4644                );
4645            }
4646            (TradingCommand::SubmitOrder(command), false) => {
4647                assert_eq!(command.position_id, Some(position_id));
4648                assert_eq!(command.client_id, Some(client_id));
4649                assert_eq!(command.order_init, orders[0].init_event().clone());
4650            }
4651            _ => panic!("unexpected submission command"),
4652        }
4653    }
4654
4655    #[rstest]
4656    fn test_submit_order_list_returns_error_when_cache_already_borrowed() {
4657        let mut strategy = create_test_strategy();
4658        register_strategy(&mut strategy);
4659
4660        let order_list_id = OrderListId::from("OL-20250208-BORROWED");
4661        let mut orders = vec![
4662            make_initialized_market_order("O-20250208-LIST-BORROWED-001"),
4663            make_initialized_market_order("O-20250208-LIST-BORROWED-002"),
4664        ];
4665
4666        for order in &mut orders {
4667            order.set_order_list_id(order_list_id);
4668        }
4669
4670        let cache_rc = strategy.core.cache_rc();
4671        let _cache = cache_rc.borrow();
4672
4673        let result = catch_unwind(AssertUnwindSafe(|| {
4674            strategy.submit_order_list(orders, None, None, None)
4675        }));
4676
4677        let err = result
4678            .expect("submit_order_list should not panic")
4679            .unwrap_err()
4680            .to_string();
4681
4682        assert_eq!(
4683            err,
4684            "Cannot submit order list OL-20250208-BORROWED: cache is currently borrowed"
4685        );
4686    }
4687
4688    #[rstest]
4689    fn test_submit_order_list_create_list_branch_publishes_init_after_cache_insert() {
4690        let mut strategy = create_test_strategy();
4691        register_strategy(&mut strategy);
4692
4693        let orders = vec![
4694            make_initialized_market_order("O-20250208-LIST-CREATE-001"),
4695            make_initialized_market_order("O-20250208-LIST-CREATE-002"),
4696        ];
4697
4698        let client_order_id1 = orders[0].client_order_id();
4699        let client_order_id2 = orders[1].client_order_id();
4700        let cache_rc = strategy.core.cache_rc();
4701        let timeline = Rc::new(RefCell::new(Vec::new()));
4702        let event_messages = Rc::new(RefCell::new(Vec::new()));
4703
4704        let event_handler = {
4705            let event_messages = event_messages.clone();
4706            let timeline = timeline.clone();
4707            TypedHandler::from_with_id(
4708                "events.order.list_create_initialized",
4709                move |event: &OrderEventAny| {
4710                    match event {
4711                        OrderEventAny::Initialized(e) if e.client_order_id == client_order_id1 => {
4712                            let cache = cache_rc.borrow();
4713                            let cached_order1 = cache.order(&client_order_id1).unwrap();
4714                            let cached_order2 = cache.order(&client_order_id2).unwrap();
4715                            let order_list_id = cached_order1.order_list_id().unwrap();
4716                            assert_eq!(cached_order2.order_list_id(), Some(order_list_id));
4717                            assert_eq!(e.order_list_id, Some(order_list_id));
4718                            assert!(cache.order_list_exists(&order_list_id));
4719                            let order_list = cache.order_list(&order_list_id).unwrap();
4720                            assert_eq!(
4721                                order_list.client_order_ids.as_slice(),
4722                                &[client_order_id1, client_order_id2]
4723                            );
4724                            timeline.borrow_mut().push("init1");
4725                        }
4726                        OrderEventAny::Initialized(e) if e.client_order_id == client_order_id2 => {
4727                            let cache = cache_rc.borrow();
4728                            let cached_order = cache.order(&client_order_id2).unwrap();
4729                            assert_eq!(e.order_list_id, cached_order.order_list_id());
4730                            timeline.borrow_mut().push("init2");
4731                        }
4732                        _ => panic!("unexpected order event {event:?}"),
4733                    }
4734                    event_messages.borrow_mut().push(event.clone());
4735                },
4736            )
4737        };
4738        let risk_handler = {
4739            let timeline = timeline.clone();
4740            TypedIntoHandler::from_with_id(
4741                "RiskEngine.queue_execute",
4742                move |command: TradingCommand| {
4743                    let TradingCommand::SubmitOrderList(command) = command else {
4744                        panic!("expected SubmitOrderList command");
4745                    };
4746                    assert!(
4747                        command
4748                            .order_inits
4749                            .iter()
4750                            .all(|init| init.order_list_id == Some(command.order_list.id))
4751                    );
4752                    timeline.borrow_mut().push("command");
4753                },
4754            )
4755        };
4756        msgbus::register_trading_command_endpoint(
4757            MessagingSwitchboard::risk_engine_queue_execute(),
4758            risk_handler,
4759        );
4760
4761        let topic = format!("events.order.{}", orders[0].strategy_id());
4762        msgbus::subscribe_order_events(topic.clone().into(), event_handler.clone(), None);
4763
4764        strategy
4765            .submit_order_list(orders, None, None, None)
4766            .unwrap();
4767
4768        msgbus::unsubscribe_order_events(topic.into(), &event_handler);
4769
4770        let cache = strategy.cache();
4771        let cached_order1 = cache.order(&client_order_id1).unwrap();
4772        let cached_order2 = cache.order(&client_order_id2).unwrap();
4773        let order_list_id = cached_order1.order_list_id().unwrap();
4774        assert_eq!(cached_order2.order_list_id(), Some(order_list_id));
4775
4776        let event_messages = event_messages.borrow();
4777        assert_eq!(event_messages.len(), 2);
4778        let OrderEventAny::Initialized(init1) = &event_messages[0] else {
4779            panic!("expected first OrderInitialized event");
4780        };
4781        let OrderEventAny::Initialized(init2) = &event_messages[1] else {
4782            panic!("expected second OrderInitialized event");
4783        };
4784        assert_eq!(init1.order_list_id, Some(order_list_id));
4785        assert_eq!(init2.order_list_id, Some(order_list_id));
4786        assert_eq!(timeline.borrow().as_slice(), &["init1", "init2", "command"]);
4787
4788        let order_list = cache.order_list(&order_list_id).unwrap();
4789        assert_eq!(
4790            order_list.client_order_ids.as_slice(),
4791            &[client_order_id1, client_order_id2]
4792        );
4793    }
4794
4795    #[rstest]
4796    fn test_submit_order_list_routes_optional_params_to_risk() {
4797        let mut strategy = create_test_strategy();
4798        register_strategy(&mut strategy);
4799
4800        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4801            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4802        msgbus::register_trading_command_endpoint(
4803            MessagingSwitchboard::risk_engine_queue_execute(),
4804            risk_handler,
4805        );
4806
4807        let no_params_orders = vec![
4808            make_initialized_market_order("O-20250208-LIST-001"),
4809            make_initialized_market_order("O-20250208-LIST-002"),
4810        ];
4811        strategy
4812            .submit_order_list(no_params_orders, None, None, None)
4813            .unwrap();
4814
4815        let params = Params::new(
4816            [(
4817                "routing_hint".to_string(),
4818                Value::String("prefer_batch".to_string()),
4819            )]
4820            .into(),
4821        );
4822        let param_orders = vec![
4823            make_initialized_market_order("O-20250208-LIST-003"),
4824            make_initialized_market_order("O-20250208-LIST-004"),
4825        ];
4826        strategy
4827            .submit_order_list(param_orders, None, None, Some(params.clone()))
4828            .unwrap();
4829
4830        let risk_messages = risk_messages.get_messages();
4831        assert_eq!(risk_messages.len(), 2);
4832        let Some(TradingCommand::SubmitOrderList(no_params_command)) = risk_messages.first() else {
4833            panic!("expected SubmitOrderList command");
4834        };
4835        let Some(TradingCommand::SubmitOrderList(param_command)) = risk_messages.get(1) else {
4836            panic!("expected SubmitOrderList command");
4837        };
4838        assert!(no_params_command.params.is_none());
4839        assert_eq!(param_command.params.as_ref(), Some(&params));
4840    }
4841
4842    #[rstest]
4843    fn test_modify_order_routes_non_emulated_orders_to_risk() {
4844        let mut strategy = create_test_strategy();
4845        register_strategy(&mut strategy);
4846
4847        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4848            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4849        msgbus::register_trading_command_endpoint(
4850            MessagingSwitchboard::risk_engine_queue_execute(),
4851            risk_handler,
4852        );
4853
4854        let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4855            get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
4856        msgbus::register_trading_command_endpoint(
4857            MessagingSwitchboard::exec_engine_queue_execute(),
4858            exec_handler,
4859        );
4860
4861        let order = OrderAny::Market(MarketOrder::new(
4862            TraderId::from("TRADER-001"),
4863            StrategyId::from("TEST-001"),
4864            InstrumentId::from("BTCUSDT.BINANCE"),
4865            ClientOrderId::from("O-20250208-0003"),
4866            OrderSide::Buy,
4867            Quantity::from(100_000),
4868            TimeInForce::Gtc,
4869            UUID4::new(),
4870            UnixNanos::default(),
4871            false,
4872            false,
4873            None,
4874            None,
4875            None,
4876            None,
4877            None,
4878            None,
4879            None,
4880            None,
4881        ));
4882        add_order_to_cache(&strategy, &order);
4883
4884        strategy
4885            .modify_order(
4886                order.client_order_id(),
4887                Some(Quantity::from(200_000)),
4888                None,
4889                None,
4890                None,
4891                None,
4892            )
4893            .unwrap();
4894
4895        let risk_messages = risk_messages.get_messages();
4896        let exec_messages = exec_messages.get_messages();
4897
4898        assert_eq!(risk_messages.len(), 1);
4899        assert!(matches!(
4900            risk_messages.first(),
4901            Some(TradingCommand::ModifyOrder(_))
4902        ));
4903        assert!(exec_messages.is_empty());
4904    }
4905
4906    #[rstest]
4907    #[case::trailing_stop_market(OrderType::TrailingStopMarket)]
4908    #[case::trailing_stop_limit(OrderType::TrailingStopLimit)]
4909    fn test_modify_order_changes_trailing_stop_trigger(#[case] order_type: OrderType) {
4910        let mut strategy = create_test_strategy();
4911        register_strategy(&mut strategy);
4912
4913        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4914            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4915        msgbus::register_trading_command_endpoint(
4916            MessagingSwitchboard::risk_engine_queue_execute(),
4917            risk_handler,
4918        );
4919
4920        let order = make_initialized_trailing_stop_order(order_type, "O-TRAILING-MODIFY");
4921        add_order_to_cache(&strategy, &order);
4922
4923        strategy
4924            .modify_order(
4925                order.client_order_id(),
4926                None,
4927                None,
4928                Some(Price::from("49500.0")),
4929                None,
4930                None,
4931            )
4932            .unwrap();
4933
4934        let risk_messages = risk_messages.get_messages();
4935
4936        let [TradingCommand::ModifyOrder(command)] = risk_messages.as_slice() else {
4937            panic!("expected one ModifyOrder command, was {risk_messages:?}");
4938        };
4939
4940        assert_eq!(command.client_order_id, order.client_order_id());
4941        assert_eq!(command.quantity, None);
4942        assert_eq!(command.price, None);
4943        assert_eq!(command.trigger_price, Some(Price::from("49500.0")));
4944    }
4945
4946    #[rstest]
4947    #[case::trailing_stop_market(OrderType::TrailingStopMarket)]
4948    #[case::trailing_stop_limit(OrderType::TrailingStopLimit)]
4949    fn test_modify_orders_changes_trailing_stop_trigger(#[case] order_type: OrderType) {
4950        let mut strategy = create_test_strategy();
4951        register_strategy(&mut strategy);
4952
4953        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4954            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4955        msgbus::register_trading_command_endpoint(
4956            MessagingSwitchboard::risk_engine_queue_execute(),
4957            risk_handler,
4958        );
4959
4960        let mut order = make_initialized_trailing_stop_order(order_type, "O-TRAILING-BATCH-MODIFY");
4961        let account_id = AccountId::from("ACC-001");
4962        order
4963            .apply(TestOrderEventStubs::submitted(&order, account_id))
4964            .unwrap();
4965        order
4966            .apply(TestOrderEventStubs::accepted(
4967                &order,
4968                account_id,
4969                VenueOrderId::from("V-TRAILING-BATCH-MODIFY"),
4970            ))
4971            .unwrap();
4972        add_order_to_cache(&strategy, &order);
4973
4974        strategy
4975            .modify_orders(
4976                vec![(
4977                    order.client_order_id(),
4978                    None,
4979                    None,
4980                    Some(Price::from("49500.0")),
4981                )],
4982                None,
4983                None,
4984            )
4985            .unwrap();
4986
4987        let risk_messages = risk_messages.get_messages();
4988
4989        let [TradingCommand::ModifyOrders(command)] = risk_messages.as_slice() else {
4990            panic!("expected one BatchModifyOrders command, was {risk_messages:?}");
4991        };
4992
4993        let [modify] = command.modifies.as_slice() else {
4994            panic!("expected one ModifyOrder, was {:?}", command.modifies);
4995        };
4996
4997        assert_eq!(modify.client_order_id, order.client_order_id());
4998        assert_eq!(
4999            modify.venue_order_id,
5000            Some(VenueOrderId::from("V-TRAILING-BATCH-MODIFY"))
5001        );
5002        assert_eq!(modify.quantity, None);
5003        assert_eq!(modify.price, None);
5004        assert_eq!(modify.trigger_price, Some(Price::from("49500.0")));
5005    }
5006
5007    #[rstest]
5008    #[case::market(OrderType::Market)]
5009    #[case::limit(OrderType::Limit)]
5010    #[case::market_to_limit(OrderType::MarketToLimit)]
5011    fn test_modify_order_rejects_trigger_for_order_without_trigger(#[case] order_type: OrderType) {
5012        let mut strategy = create_test_strategy();
5013        register_strategy(&mut strategy);
5014
5015        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
5016            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
5017        msgbus::register_trading_command_endpoint(
5018            MessagingSwitchboard::risk_engine_queue_execute(),
5019            risk_handler,
5020        );
5021
5022        let mut builder = OrderTestBuilder::new(order_type);
5023        builder
5024            .trader_id(TraderId::from("TRADER-001"))
5025            .strategy_id(StrategyId::from("TEST-001"))
5026            .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
5027            .client_order_id(ClientOrderId::from("O-NO-TRIGGER-MODIFY"))
5028            .quantity(Quantity::from(100_000));
5029
5030        if order_type == OrderType::Limit {
5031            builder.price(Price::from("50000.0"));
5032        }
5033
5034        let order = builder.build();
5035        add_order_to_cache(&strategy, &order);
5036
5037        let result = strategy.modify_order(
5038            order.client_order_id(),
5039            None,
5040            None,
5041            Some(Price::from("49500.0")),
5042            None,
5043            None,
5044        );
5045
5046        assert_eq!(
5047            result.unwrap_err().to_string(),
5048            format!("{order_type} orders do not have a STOP trigger price")
5049        );
5050        assert!(risk_messages.get_messages().is_empty());
5051    }
5052
5053    #[rstest]
5054    fn test_modify_order_routes_active_local_algorithm_order_to_algorithm() {
5055        let mut strategy = create_test_strategy();
5056        register_strategy(&mut strategy);
5057
5058        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
5059            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
5060        msgbus::register_trading_command_endpoint(
5061            MessagingSwitchboard::risk_engine_queue_execute(),
5062            risk_handler,
5063        );
5064        let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
5065            get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
5066        msgbus::register_trading_command_endpoint(
5067            MessagingSwitchboard::exec_engine_queue_execute(),
5068            exec_handler,
5069        );
5070        let (algo_handler, algo_messages) =
5071            get_any_saving_handler::<TradingCommand>(Some(Ustr::from("TWAP.execute")));
5072        msgbus::register_any("TWAP.execute".into(), algo_handler);
5073
5074        let order = make_initialized_algorithm_order("O-20250208-ALGO-MODIFY-001");
5075        add_order_to_cache(&strategy, &order);
5076
5077        strategy
5078            .modify_order(
5079                order.client_order_id(),
5080                Some(Quantity::from(200_000)),
5081                None,
5082                None,
5083                None,
5084                None,
5085            )
5086            .unwrap();
5087
5088        let algo_messages = algo_messages.get_messages();
5089        assert_eq!(algo_messages.len(), 1);
5090        assert!(matches!(
5091            algo_messages.first(),
5092            Some(TradingCommand::ModifyOrder(command))
5093                if command.client_order_id == order.client_order_id()
5094        ));
5095        assert!(risk_messages.get_messages().is_empty());
5096        assert!(exec_messages.get_messages().is_empty());
5097    }
5098
5099    #[rstest]
5100    fn test_modify_order_routes_accepted_algorithm_order_to_risk() {
5101        let mut strategy = create_test_strategy();
5102        register_strategy(&mut strategy);
5103
5104        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
5105            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
5106        msgbus::register_trading_command_endpoint(
5107            MessagingSwitchboard::risk_engine_queue_execute(),
5108            risk_handler,
5109        );
5110        let (algo_handler, algo_messages) =
5111            get_any_saving_handler::<TradingCommand>(Some(Ustr::from("TWAP.execute")));
5112        msgbus::register_any("TWAP.execute".into(), algo_handler);
5113
5114        let mut order = make_initialized_algorithm_order("O-20250208-ALGO-MODIFY-002");
5115        let account_id = AccountId::from("ACC-001");
5116        order
5117            .apply(TestOrderEventStubs::submitted(&order, account_id))
5118            .unwrap();
5119        order
5120            .apply(TestOrderEventStubs::accepted(
5121                &order,
5122                account_id,
5123                VenueOrderId::from("O-20250208-ALGO-MODIFY-002"),
5124            ))
5125            .unwrap();
5126        add_order_to_cache(&strategy, &order);
5127
5128        strategy
5129            .modify_order(
5130                order.client_order_id(),
5131                Some(Quantity::from(200_000)),
5132                None,
5133                None,
5134                None,
5135                None,
5136            )
5137            .unwrap();
5138
5139        let risk_messages = risk_messages.get_messages();
5140        assert_eq!(risk_messages.len(), 1);
5141        assert!(matches!(
5142            risk_messages.first(),
5143            Some(TradingCommand::ModifyOrder(command))
5144                if command.client_order_id == order.client_order_id()
5145        ));
5146        assert!(algo_messages.get_messages().is_empty());
5147        assert_eq!(
5148            strategy
5149                .cache()
5150                .order(&order.client_order_id())
5151                .unwrap()
5152                .status(),
5153            OrderStatus::PendingUpdate
5154        );
5155    }
5156
5157    #[rstest]
5158    fn test_modify_order_routes_emulated_order_to_order_emulator() {
5159        let mut strategy = create_test_strategy();
5160        register_strategy(&mut strategy);
5161
5162        let (emulator_handler, emulator_messages): (
5163            _,
5164            TypedIntoMessageSavingHandler<TradingCommand>,
5165        ) = get_typed_into_message_saving_handler(Some(Ustr::from("OrderEmulator.execute")));
5166        msgbus::register_trading_command_endpoint(
5167            MessagingSwitchboard::order_emulator_execute(),
5168            emulator_handler,
5169        );
5170        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
5171            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
5172        msgbus::register_trading_command_endpoint(
5173            MessagingSwitchboard::risk_engine_queue_execute(),
5174            risk_handler,
5175        );
5176        let mut order = OrderTestBuilder::new(OrderType::StopMarket)
5177            .trader_id(TraderId::from("TRADER-001"))
5178            .strategy_id(StrategyId::from("TEST-001"))
5179            .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
5180            .client_order_id(ClientOrderId::from("O-20250208-EMULATED-MODIFY-001"))
5181            .side(OrderSide::Buy)
5182            .trigger_price(Price::from("51000.0"))
5183            .quantity(Quantity::from(100_000))
5184            .emulation_trigger(TriggerType::BidAsk)
5185            .build();
5186        order
5187            .apply(OrderEventAny::Emulated(
5188                OrderEmulatedSpec::builder()
5189                    .trader_id(order.trader_id())
5190                    .strategy_id(order.strategy_id())
5191                    .instrument_id(order.instrument_id())
5192                    .client_order_id(order.client_order_id())
5193                    .build(),
5194            ))
5195            .unwrap();
5196        add_order_to_cache(&strategy, &order);
5197
5198        strategy
5199            .modify_order(
5200                order.client_order_id(),
5201                None,
5202                None,
5203                Some(Price::from("52000.0")),
5204                None,
5205                None,
5206            )
5207            .unwrap();
5208
5209        let emulator_messages = emulator_messages.get_messages();
5210        assert_eq!(emulator_messages.len(), 1);
5211        assert!(matches!(
5212            emulator_messages.first(),
5213            Some(TradingCommand::ModifyOrder(command))
5214                if command.client_order_id == order.client_order_id()
5215        ));
5216        assert!(risk_messages.get_messages().is_empty());
5217    }
5218
5219    #[rstest]
5220    fn test_modify_order_routes_initialized_trigger_order_to_emulator_reentrantly() {
5221        let strategy_id = StrategyId::from("REENTRANT-001");
5222        let modified_quantity = Quantity::from(200_000);
5223        let mut strategy = InitializedModifyStrategy {
5224            core: StrategyCore::new(StrategyConfig {
5225                strategy_id: Some(strategy_id),
5226                ..Default::default()
5227            }),
5228            modified_quantity,
5229        };
5230        let trader_id = TraderId::from("TRADER-001");
5231        let clock = Rc::new(RefCell::new(VirtualClock::new()));
5232        let cache = Rc::new(RefCell::new(Cache::default()));
5233        let portfolio = Rc::new(RefCell::new(Portfolio::new(
5234            clock.clone(),
5235            cache.clone(),
5236            None,
5237        )));
5238        strategy
5239            .core
5240            .register(trader_id, clock, cache, portfolio)
5241            .unwrap();
5242        strategy.initialize().unwrap();
5243        strategy.start().unwrap();
5244
5245        let actor_id = strategy.actor_id().inner();
5246        register_component_actor(strategy);
5247        let order_handler = TypedHandler::from(move |event: &OrderEventAny| {
5248            let mut strategy =
5249                try_get_actor_unchecked::<InitializedModifyStrategy>(&actor_id).unwrap();
5250            strategy.handle_order_event(event.clone());
5251        });
5252        let topic = format!("events.order.{strategy_id}");
5253        msgbus::subscribe_order_events(topic.clone().into(), order_handler.clone(), None);
5254
5255        let (emulator_handler, emulator_messages): (
5256            _,
5257            TypedIntoMessageSavingHandler<TradingCommand>,
5258        ) = get_typed_into_message_saving_handler(Some(Ustr::from("OrderEmulator.execute")));
5259        msgbus::register_trading_command_endpoint(
5260            MessagingSwitchboard::order_emulator_execute(),
5261            emulator_handler,
5262        );
5263        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
5264            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
5265        msgbus::register_trading_command_endpoint(
5266            MessagingSwitchboard::risk_engine_queue_execute(),
5267            risk_handler,
5268        );
5269        let (algo_handler, algo_messages) =
5270            get_any_saving_handler::<TradingCommand>(Some(Ustr::from("TWAP.execute")));
5271        msgbus::register_any("TWAP.execute".into(), algo_handler);
5272
5273        let order = OrderTestBuilder::new(OrderType::StopMarket)
5274            .trader_id(trader_id)
5275            .strategy_id(strategy_id)
5276            .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
5277            .client_order_id(ClientOrderId::from("O-REENTRANT-001"))
5278            .side(OrderSide::Buy)
5279            .trigger_price(Price::from("51000.0"))
5280            .quantity(Quantity::from(100_000))
5281            .emulation_trigger(TriggerType::BidAsk)
5282            .build();
5283        let client_order_id = order.client_order_id();
5284
5285        let mut strategy = try_get_actor_unchecked::<InitializedModifyStrategy>(&actor_id).unwrap();
5286        strategy.submit_order(order, None, None, None).unwrap();
5287        drop(strategy);
5288
5289        msgbus::unsubscribe_order_events(topic.into(), &order_handler);
5290        deregister_component(&strategy_id.inner());
5291        deregister_actor(&actor_id);
5292
5293        let emulator_messages = emulator_messages.get_messages();
5294        assert_eq!(emulator_messages.len(), 2);
5295        assert!(matches!(
5296            emulator_messages.first(),
5297            Some(TradingCommand::ModifyOrder(command))
5298                if command.client_order_id == client_order_id
5299                    && command.quantity == Some(modified_quantity)
5300        ));
5301        assert!(
5302            risk_messages
5303                .get_messages()
5304                .iter()
5305                .all(|command| !matches!(command, TradingCommand::ModifyOrder(_)))
5306        );
5307        assert!(
5308            algo_messages
5309                .get_messages()
5310                .iter()
5311                .all(|command| !matches!(command, TradingCommand::ModifyOrder(_)))
5312        );
5313        assert!(matches!(
5314            emulator_messages.get(1),
5315            Some(TradingCommand::SubmitOrder(command))
5316                if command.client_order_id == client_order_id
5317        ));
5318    }
5319
5320    #[rstest]
5321    fn test_modify_order_prefers_emulator_over_algorithm_for_emulated_algorithm_order() {
5322        let mut strategy = create_test_strategy();
5323        register_strategy(&mut strategy);
5324
5325        let (emulator_handler, emulator_messages): (
5326            _,
5327            TypedIntoMessageSavingHandler<TradingCommand>,
5328        ) = get_typed_into_message_saving_handler(Some(Ustr::from("OrderEmulator.execute")));
5329        msgbus::register_trading_command_endpoint(
5330            MessagingSwitchboard::order_emulator_execute(),
5331            emulator_handler,
5332        );
5333        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
5334            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
5335        msgbus::register_trading_command_endpoint(
5336            MessagingSwitchboard::risk_engine_queue_execute(),
5337            risk_handler,
5338        );
5339        let (algo_handler, algo_messages) =
5340            get_any_saving_handler::<TradingCommand>(Some(Ustr::from("TWAP.execute")));
5341        msgbus::register_any("TWAP.execute".into(), algo_handler);
5342
5343        let mut order = OrderTestBuilder::new(OrderType::StopMarket)
5344            .trader_id(TraderId::from("TRADER-001"))
5345            .strategy_id(StrategyId::from("TEST-001"))
5346            .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
5347            .client_order_id(ClientOrderId::from("O-20250208-EMULATED-ALGO-MODIFY-001"))
5348            .side(OrderSide::Buy)
5349            .trigger_price(Price::from("51000.0"))
5350            .quantity(Quantity::from(100_000))
5351            .emulation_trigger(TriggerType::BidAsk)
5352            .exec_algorithm_id(ExecAlgorithmId::from("TWAP"))
5353            .exec_spawn_id(ClientOrderId::from("O-20250208-EMULATED-ALGO-MODIFY-001"))
5354            .build();
5355        order
5356            .apply(OrderEventAny::Emulated(
5357                OrderEmulatedSpec::builder()
5358                    .trader_id(order.trader_id())
5359                    .strategy_id(order.strategy_id())
5360                    .instrument_id(order.instrument_id())
5361                    .client_order_id(order.client_order_id())
5362                    .build(),
5363            ))
5364            .unwrap();
5365
5366        // An `Emulated` order satisfies both `is_emulated` and `is_active_local`, so this
5367        // order matches the emulator branch and the algorithm branch at once.
5368        assert!(order.is_emulated());
5369        assert!(order.is_active_local());
5370        assert!(order.exec_algorithm_id().is_some());
5371
5372        add_order_to_cache(&strategy, &order);
5373
5374        strategy
5375            .modify_order(
5376                order.client_order_id(),
5377                None,
5378                None,
5379                Some(Price::from("52000.0")),
5380                None,
5381                None,
5382            )
5383            .unwrap();
5384
5385        let emulator_messages = emulator_messages.get_messages();
5386        assert_eq!(emulator_messages.len(), 1);
5387        assert!(matches!(
5388            emulator_messages.first(),
5389            Some(TradingCommand::ModifyOrder(command))
5390                if command.client_order_id == order.client_order_id()
5391        ));
5392        assert!(algo_messages.get_messages().is_empty());
5393        assert!(risk_messages.get_messages().is_empty());
5394    }
5395
5396    #[rstest]
5397    fn test_modify_order_marks_order_pending_update_locally_before_send() {
5398        let mut strategy = create_test_strategy();
5399        register_strategy(&mut strategy);
5400
5401        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
5402            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
5403        msgbus::register_trading_command_endpoint(
5404            MessagingSwitchboard::risk_engine_queue_execute(),
5405            risk_handler,
5406        );
5407
5408        let (event_handler, event_messages): (_, TypedMessageSavingHandler<OrderEventAny>) =
5409            get_typed_message_saving_handler(Some(Ustr::from("events.order.pending_update")));
5410        let order = make_accepted_limit_order("O-20250208-UPDATE-001");
5411        let topic = format!("events.order.{}", order.strategy_id());
5412        msgbus::subscribe_order_events(topic.clone().into(), event_handler.clone(), None);
5413        add_order_to_cache(&strategy, &order);
5414
5415        strategy
5416            .modify_order(
5417                order.client_order_id(),
5418                None,
5419                Some(Price::from("51000.0")),
5420                None,
5421                None,
5422                None,
5423            )
5424            .unwrap();
5425
5426        msgbus::unsubscribe_order_events(topic.into(), &event_handler);
5427
5428        let cache = strategy.cache();
5429        let cached_order = cache.order(&order.client_order_id()).unwrap();
5430        assert_eq!(cached_order.status(), OrderStatus::PendingUpdate);
5431
5432        let risk_messages = risk_messages.get_messages();
5433        assert_eq!(risk_messages.len(), 1);
5434        assert!(matches!(
5435            risk_messages.first(),
5436            Some(TradingCommand::ModifyOrder(_))
5437        ));
5438
5439        let event_messages = event_messages.get_messages();
5440        assert_eq!(event_messages.len(), 1);
5441        assert!(matches!(
5442            event_messages.first(),
5443            Some(OrderEventAny::PendingUpdate(_))
5444        ));
5445    }
5446
5447    #[rstest]
5448    fn test_late_modify_rejection_reaches_running_strategy_before_acceptance() {
5449        let mut strategy = ModifyRejectionStrategy {
5450            core: StrategyCore::new(StrategyConfig {
5451                strategy_id: Some(StrategyId::from("TEST-001")),
5452                ..Default::default()
5453            }),
5454            rejections: Vec::new(),
5455        };
5456
5457        let clock = Rc::new(RefCell::new(VirtualClock::new()));
5458        let cache = Rc::new(RefCell::new(Cache::default()));
5459
5460        let portfolio = Rc::new(RefCell::new(Portfolio::new(
5461            clock.clone(),
5462            cache.clone(),
5463            None,
5464        )));
5465        strategy
5466            .core
5467            .register(
5468                TraderId::from("TRADER-001"),
5469                clock.clone(),
5470                cache.clone(),
5471                portfolio,
5472            )
5473            .unwrap();
5474        strategy.initialize().unwrap();
5475        strategy.start().unwrap();
5476        let mut engine = ExecutionEngine::new(clock, cache.clone(), None);
5477        let order = OrderTestBuilder::new(OrderType::Limit)
5478            .trader_id(TraderId::from("TRADER-001"))
5479            .strategy_id(StrategyId::from("TEST-001"))
5480            .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
5481            .client_order_id(ClientOrderId::from("O-LATE-MODIFY-001"))
5482            .side(OrderSide::Buy)
5483            .quantity(Quantity::from(10))
5484            .price(Price::from("50000.00"))
5485            .build();
5486        cache
5487            .borrow_mut()
5488            .add_order(order.clone(), None, None, false)
5489            .unwrap();
5490        let account_id = AccountId::from("ACC-001");
5491        engine.process(&TestOrderEventStubs::submitted(&order, account_id));
5492        assert_eq!(
5493            cache
5494                .borrow()
5495                .order(&order.client_order_id())
5496                .unwrap()
5497                .status(),
5498            OrderStatus::Submitted
5499        );
5500
5501        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
5502            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
5503        msgbus::register_trading_command_endpoint(
5504            MessagingSwitchboard::risk_engine_queue_execute(),
5505            risk_handler,
5506        );
5507
5508        for (quantity, price) in [(30, "52000.00"), (20, "51000.00")] {
5509            strategy
5510                .modify_order(
5511                    order.client_order_id(),
5512                    Some(Quantity::from(quantity)),
5513                    Some(Price::from(price)),
5514                    None,
5515                    None,
5516                    None,
5517                )
5518                .unwrap();
5519        }
5520
5521        let commands = risk_messages.get_messages();
5522        assert_eq!(commands.len(), 2);
5523
5524        let causes: Vec<_> = commands
5525            .iter()
5526            .map(|command| {
5527                let TradingCommand::ModifyOrder(command) = command else {
5528                    panic!("expected modify command");
5529                };
5530
5531                command.command_id
5532            })
5533            .collect();
5534
5535        for (command, quantity, price) in [
5536            (&commands[0], 30, "52000.00"),
5537            (&commands[1], 20, "51000.00"),
5538        ] {
5539            let TradingCommand::ModifyOrder(command) = command else {
5540                panic!("Expected ModifyOrder command");
5541            };
5542
5543            assert_eq!(command.client_order_id, order.client_order_id());
5544            assert_eq!(command.venue_order_id, None);
5545            assert_eq!(command.quantity, Some(Quantity::from(quantity)));
5546            assert_eq!(command.price, Some(Price::from(price)));
5547        }
5548
5549        assert_eq!(
5550            cache
5551                .borrow()
5552                .order(&order.client_order_id())
5553                .unwrap()
5554                .status(),
5555            OrderStatus::PendingUpdate
5556        );
5557        let strategy = Rc::new(RefCell::new(strategy));
5558
5559        let handler = TypedHandler::from({
5560            let strategy = strategy.clone();
5561            move |event: &OrderEventAny| strategy.borrow_mut().handle_order_event(event.clone())
5562        });
5563
5564        let topic = msgbus::switchboard::get_event_order_topic(order.strategy_id());
5565        msgbus::subscribe_order_events(topic.into(), handler.clone(), None);
5566        let mut updated = OrderUpdatedSpec::builder()
5567            .trader_id(order.trader_id())
5568            .strategy_id(order.strategy_id())
5569            .instrument_id(order.instrument_id())
5570            .client_order_id(order.client_order_id())
5571            .venue_order_id(VenueOrderId::from("V-AMENDED-001"))
5572            .account_id(account_id)
5573            .quantity(Quantity::from(20))
5574            .price(Price::from("51000.00"))
5575            .build();
5576        updated.causation_id = Some(causes[1]);
5577        engine.process(&OrderEventAny::Updated(updated));
5578        assert_eq!(
5579            cache
5580                .borrow()
5581                .order(&order.client_order_id())
5582                .unwrap()
5583                .status(),
5584            OrderStatus::PendingUpdate
5585        );
5586        let mut rejection = OrderModifyRejectedSpec::builder()
5587            .trader_id(order.trader_id())
5588            .strategy_id(order.strategy_id())
5589            .instrument_id(order.instrument_id())
5590            .client_order_id(order.client_order_id())
5591            .account_id(account_id)
5592            .reason(Ustr::from("First amend rejected"))
5593            .event_id(UUID4::new())
5594            .ts_event(UnixNanos::from(123))
5595            .ts_init(UnixNanos::from(456))
5596            .build();
5597        rejection.causation_id = Some(causes[0]);
5598        engine.process(&OrderEventAny::ModifyRejected(rejection));
5599        msgbus::unsubscribe_order_events(topic.into(), &handler);
5600
5601        let strategy = strategy.borrow();
5602        assert_eq!(strategy.state(), ComponentState::Running);
5603        assert_eq!(strategy.rejections.len(), 1);
5604        let (received, cached) = &strategy.rejections[0];
5605        assert_eq!(*received, rejection);
5606        assert_eq!(cached.status(), OrderStatus::Submitted);
5607        assert_eq!(cached.previous_status(), Some(OrderStatus::Submitted));
5608        assert_eq!(cached.quantity(), Quantity::from(20));
5609        assert_eq!(cached.price(), Some(Price::from("51000.00")));
5610        assert_eq!(cached.filled_qty(), Quantity::from(0));
5611        assert_eq!(cached.leaves_qty(), Quantity::from(20));
5612        assert_eq!(
5613            cached.venue_order_id(),
5614            Some(VenueOrderId::from("V-AMENDED-001"))
5615        );
5616        assert_eq!(cached.events().len(), 6);
5617
5618        for (event, cause) in cached.events()[2..4].iter().zip(causes) {
5619            let OrderEventAny::PendingUpdate(event) = event else {
5620                panic!("expected command marker");
5621            };
5622
5623            assert_eq!(event.causation_id, Some(cause));
5624        }
5625
5626        assert_eq!(
5627            cached.last_event(),
5628            &OrderEventAny::ModifyRejected(rejection)
5629        );
5630    }
5631
5632    #[rstest]
5633    fn test_modify_orders_marks_orders_pending_update_locally_before_send() {
5634        let mut strategy = create_test_strategy();
5635        register_strategy(&mut strategy);
5636
5637        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
5638            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
5639        msgbus::register_trading_command_endpoint(
5640            MessagingSwitchboard::risk_engine_queue_execute(),
5641            risk_handler,
5642        );
5643
5644        let (event_handler, event_messages): (_, TypedMessageSavingHandler<OrderEventAny>) =
5645            get_typed_message_saving_handler(Some(Ustr::from("events.order.batch_pending_update")));
5646        let order1 = make_accepted_limit_order("O-20250208-BATCH-UPDATE-001");
5647        let order2 = make_accepted_limit_order("O-20250208-BATCH-UPDATE-002");
5648        let topic = format!("events.order.{}", order1.strategy_id());
5649        msgbus::subscribe_order_events(topic.clone().into(), event_handler.clone(), None);
5650        add_order_to_cache(&strategy, &order1);
5651        add_order_to_cache(&strategy, &order2);
5652
5653        strategy
5654            .modify_orders(
5655                vec![
5656                    (
5657                        order1.client_order_id(),
5658                        None,
5659                        Some(Price::from("51000.0")),
5660                        None,
5661                    ),
5662                    (
5663                        order2.client_order_id(),
5664                        Some(Quantity::from("2.0")),
5665                        None,
5666                        None,
5667                    ),
5668                ],
5669                None,
5670                None,
5671            )
5672            .unwrap();
5673
5674        msgbus::unsubscribe_order_events(topic.into(), &event_handler);
5675
5676        let cache = strategy.cache();
5677        let cached_order1 = cache.order(&order1.client_order_id()).unwrap();
5678        let cached_order2 = cache.order(&order2.client_order_id()).unwrap();
5679        assert_eq!(cached_order1.status(), OrderStatus::PendingUpdate);
5680        assert_eq!(cached_order2.status(), OrderStatus::PendingUpdate);
5681
5682        let risk_messages = risk_messages.get_messages();
5683        assert_eq!(risk_messages.len(), 1);
5684        let Some(TradingCommand::ModifyOrders(command)) = risk_messages.first() else {
5685            panic!("expected BatchModifyOrders command");
5686        };
5687        assert_eq!(command.modifies.len(), 2);
5688        assert_eq!(
5689            command
5690                .modifies
5691                .iter()
5692                .map(|modify| modify.client_order_id)
5693                .collect::<Vec<_>>(),
5694            vec![order1.client_order_id(), order2.client_order_id()]
5695        );
5696
5697        let event_messages = event_messages.get_messages();
5698        assert_eq!(event_messages.len(), 2);
5699        assert!(
5700            event_messages
5701                .iter()
5702                .all(|event| matches!(event, OrderEventAny::PendingUpdate(_)))
5703        );
5704    }
5705
5706    #[rstest]
5707    fn test_cancel_order_marks_order_pending_cancel_locally_before_send() {
5708        let mut strategy = create_test_strategy();
5709        register_strategy(&mut strategy);
5710
5711        let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
5712            get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
5713        msgbus::register_trading_command_endpoint(
5714            MessagingSwitchboard::exec_engine_queue_execute(),
5715            exec_handler,
5716        );
5717
5718        let (event_handler, event_messages): (_, TypedMessageSavingHandler<OrderEventAny>) =
5719            get_typed_message_saving_handler(Some(Ustr::from("events.order.pending_cancel")));
5720        let order = make_accepted_market_order("O-20250208-CANCEL-001");
5721        let topic = format!("events.order.{}", order.strategy_id());
5722        msgbus::subscribe_order_events(topic.clone().into(), event_handler.clone(), None);
5723        add_order_to_cache(&strategy, &order);
5724
5725        strategy
5726            .cancel_order(order.client_order_id(), None, None)
5727            .unwrap();
5728
5729        msgbus::unsubscribe_order_events(topic.into(), &event_handler);
5730
5731        let cache = strategy.cache();
5732        let cached_order = cache.order(&order.client_order_id()).unwrap();
5733        assert_eq!(cached_order.status(), OrderStatus::PendingCancel);
5734        let cache = strategy.core.cache_ref();
5735        assert!(cache.is_order_pending_cancel_local(&order.client_order_id()));
5736
5737        let exec_messages = exec_messages.get_messages();
5738        assert_eq!(exec_messages.len(), 1);
5739        assert!(matches!(
5740            exec_messages.first(),
5741            Some(TradingCommand::CancelOrder(_))
5742        ));
5743
5744        let event_messages = event_messages.get_messages();
5745        assert_eq!(event_messages.len(), 1);
5746        assert!(matches!(
5747            event_messages.first(),
5748            Some(OrderEventAny::PendingCancel(_))
5749        ));
5750    }
5751
5752    #[rstest]
5753    fn test_cancel_all_orders_strategy_only_sends_only_caller_strategy_cancels() {
5754        let mut strategy = create_test_strategy();
5755        register_strategy(&mut strategy);
5756
5757        let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
5758            get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
5759        msgbus::register_trading_command_endpoint(
5760            MessagingSwitchboard::exec_engine_queue_execute(),
5761            exec_handler,
5762        );
5763
5764        let order = make_accepted_market_order("O-20250208-CANCEL-ALL-001");
5765        let mut sibling_order = OrderTestBuilder::new(OrderType::Market)
5766            .trader_id(TraderId::from("TRADER-001"))
5767            .strategy_id(StrategyId::from("SIBLING-001"))
5768            .instrument_id(order.instrument_id())
5769            .client_order_id(ClientOrderId::from("O-20250208-CANCEL-ALL-002"))
5770            .side(OrderSide::Buy)
5771            .quantity(Quantity::from(100_000))
5772            .build();
5773        let account_id = AccountId::from("ACC-001");
5774        sibling_order
5775            .apply(TestOrderEventStubs::submitted(&sibling_order, account_id))
5776            .unwrap();
5777        sibling_order
5778            .apply(TestOrderEventStubs::accepted(
5779                &sibling_order,
5780                account_id,
5781                VenueOrderId::from("2"),
5782            ))
5783            .unwrap();
5784        add_order_to_cache(&strategy, &order);
5785        add_order_to_cache(&strategy, &sibling_order);
5786        strategy.core.cache_rc().borrow_mut().build_index();
5787
5788        strategy
5789            .cancel_all_orders(order.instrument_id(), None, None, true, None)
5790            .unwrap();
5791
5792        let messages = exec_messages.get_messages();
5793        let cache = strategy.cache();
5794        let cached_order = cache.order(&order.client_order_id()).unwrap();
5795        let cached_sibling = cache.order(&sibling_order.client_order_id()).unwrap();
5796        assert_eq!(messages.len(), 1);
5797        assert!(matches!(
5798            messages.first(),
5799            Some(TradingCommand::CancelOrder(command))
5800                if command.client_order_id == order.client_order_id()
5801        ));
5802        assert_eq!(cached_order.status(), OrderStatus::PendingCancel);
5803        assert_eq!(cached_sibling.status(), OrderStatus::Accepted);
5804    }
5805
5806    #[rstest]
5807    fn test_cancel_all_orders_strategy_only_deduplicates_emulated_inflight_order() {
5808        let mut strategy = create_test_strategy();
5809        register_strategy(&mut strategy);
5810
5811        let (emulator_handler, emulator_messages): (
5812            _,
5813            TypedIntoMessageSavingHandler<TradingCommand>,
5814        ) = get_typed_into_message_saving_handler(Some(Ustr::from("OrderEmulator.execute")));
5815        msgbus::register_trading_command_endpoint(
5816            MessagingSwitchboard::order_emulator_execute(),
5817            emulator_handler,
5818        );
5819        let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
5820            get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
5821        msgbus::register_trading_command_endpoint(
5822            MessagingSwitchboard::exec_engine_queue_execute(),
5823            exec_handler,
5824        );
5825
5826        let client_order_id = ClientOrderId::from("O-20250208-CANCEL-ALL-EMULATED-001");
5827        let order = OrderTestBuilder::new(OrderType::StopMarket)
5828            .trader_id(TraderId::from("TRADER-001"))
5829            .strategy_id(StrategyId::from("TEST-001"))
5830            .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
5831            .client_order_id(client_order_id)
5832            .side(OrderSide::Buy)
5833            .trigger_price(Price::from("51000.0"))
5834            .quantity(Quantity::from(100_000))
5835            .emulation_trigger(TriggerType::BidAsk)
5836            .exec_algorithm_id(ExecAlgorithmId::from("ALGO-001"))
5837            .exec_spawn_id(client_order_id)
5838            .build();
5839        add_order_to_cache(&strategy, &order);
5840        strategy.core.cache_rc().borrow_mut().build_index();
5841
5842        strategy
5843            .cancel_all_orders(order.instrument_id(), None, None, true, None)
5844            .unwrap();
5845
5846        let emulator_messages = emulator_messages.get_messages();
5847        assert_eq!(emulator_messages.len(), 1);
5848        assert!(matches!(
5849            emulator_messages.first(),
5850            Some(TradingCommand::CancelOrder(command))
5851                if command.client_order_id == order.client_order_id()
5852        ));
5853        assert!(exec_messages.get_messages().is_empty());
5854    }
5855
5856    #[rstest]
5857    fn test_cancel_all_orders_without_strategy_only_sends_cancel_all_command() {
5858        let mut strategy = create_test_strategy();
5859        register_strategy(&mut strategy);
5860
5861        let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
5862            get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
5863        msgbus::register_trading_command_endpoint(
5864            MessagingSwitchboard::exec_engine_queue_execute(),
5865            exec_handler,
5866        );
5867
5868        let instrument_id = InstrumentId::from("BTCUSDT.BINANCE");
5869
5870        strategy
5871            .cancel_all_orders(instrument_id, None, None, false, None)
5872            .unwrap();
5873
5874        let messages = exec_messages.get_messages();
5875        assert_eq!(messages.len(), 1);
5876        let Some(TradingCommand::CancelAllOrders(command)) = messages.first() else {
5877            panic!("Expected a CancelAllOrders command, was {messages:?}");
5878        };
5879        assert_eq!(command.strategy_id, StrategyId::from("TEST-001"));
5880        assert_eq!(command.instrument_id, instrument_id);
5881        assert_eq!(command.client_id, None);
5882        assert_eq!(command.order_side, None);
5883        assert_eq!(command.correlation_id, Some(command.command_id));
5884        assert_eq!(command.causation_id, None);
5885    }
5886
5887    #[rstest]
5888    fn test_cancel_all_orders_without_strategy_only_delegates_local_routing_with_params() {
5889        let mut strategy = create_test_strategy();
5890        register_strategy(&mut strategy);
5891
5892        let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
5893            get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
5894        msgbus::register_trading_command_endpoint(
5895            MessagingSwitchboard::exec_engine_queue_execute(),
5896            exec_handler,
5897        );
5898        let (emulator_handler, emulator_messages): (
5899            _,
5900            TypedIntoMessageSavingHandler<TradingCommand>,
5901        ) = get_typed_into_message_saving_handler(Some(Ustr::from("OrderEmulator.execute")));
5902        msgbus::register_trading_command_endpoint(
5903            MessagingSwitchboard::order_emulator_execute(),
5904            emulator_handler,
5905        );
5906        let exec_algorithm_id = ExecAlgorithmId::from("ALGO-001");
5907        let algorithm_endpoint = format!("{exec_algorithm_id}.execute");
5908        let (algorithm_handler, algorithm_messages) =
5909            get_any_saving_handler::<TradingCommand>(Some(Ustr::from("ALGO-001.execute")));
5910        msgbus::register_any(algorithm_endpoint.into(), algorithm_handler);
5911
5912        let instrument_id = InstrumentId::from("BTCUSDT.BINANCE");
5913        let sibling_strategy_id = StrategyId::from("SIBLING-001");
5914        let selected_client = ClientId::from("CLIENT-001");
5915        let account_id = AccountId::from("ACC-001");
5916
5917        let mut open_order = OrderTestBuilder::new(OrderType::Limit)
5918            .strategy_id(sibling_strategy_id)
5919            .instrument_id(instrument_id)
5920            .client_order_id(ClientOrderId::from("O-BROAD-OPEN-001"))
5921            .side(OrderSide::Buy)
5922            .price(Price::from("49000.0"))
5923            .quantity(Quantity::from(100_000))
5924            .build();
5925        open_order
5926            .apply(TestOrderEventStubs::submitted(&open_order, account_id))
5927            .unwrap();
5928        open_order
5929            .apply(TestOrderEventStubs::accepted(
5930                &open_order,
5931                account_id,
5932                VenueOrderId::from("V-BROAD-OPEN-001"),
5933            ))
5934            .unwrap();
5935        let emulated_order = OrderTestBuilder::new(OrderType::StopMarket)
5936            .strategy_id(sibling_strategy_id)
5937            .instrument_id(instrument_id)
5938            .client_order_id(ClientOrderId::from("O-BROAD-EMULATED-001"))
5939            .side(OrderSide::Buy)
5940            .trigger_price(Price::from("51000.0"))
5941            .quantity(Quantity::from(100_000))
5942            .emulation_trigger(TriggerType::BidAsk)
5943            .build();
5944        let algorithm_order_id = ClientOrderId::from("O-BROAD-ALGO-001");
5945        let algorithm_order = OrderTestBuilder::new(OrderType::Market)
5946            .strategy_id(sibling_strategy_id)
5947            .instrument_id(instrument_id)
5948            .client_order_id(algorithm_order_id)
5949            .side(OrderSide::Buy)
5950            .quantity(Quantity::from(100_000))
5951            .exec_algorithm_id(exec_algorithm_id)
5952            .exec_spawn_id(algorithm_order_id)
5953            .build();
5954
5955        {
5956            let cache_rc = strategy.core.cache_rc();
5957            let mut cache = cache_rc.borrow_mut();
5958            for order in [&open_order, &emulated_order, &algorithm_order] {
5959                cache
5960                    .add_order(order.clone(), None, Some(selected_client), true)
5961                    .unwrap();
5962            }
5963            cache.build_index();
5964        }
5965
5966        let params = Params::new(
5967            [(
5968                "routing_hint".to_string(),
5969                Value::String("broad_cancel".to_string()),
5970            )]
5971            .into(),
5972        );
5973        strategy
5974            .cancel_all_orders(
5975                instrument_id,
5976                Some(OrderSide::Buy),
5977                Some(selected_client),
5978                false,
5979                Some(params.clone()),
5980            )
5981            .unwrap();
5982
5983        let exec_messages = exec_messages.get_messages();
5984        let Some(TradingCommand::CancelAllOrders(exec_command)) = exec_messages.first() else {
5985            panic!("Expected an execution CancelAllOrders command, was {exec_messages:?}");
5986        };
5987
5988        assert_eq!(exec_messages.len(), 1);
5989        assert!(emulator_messages.get_messages().is_empty());
5990        assert!(algorithm_messages.get_messages().is_empty());
5991        assert_eq!(exec_command.client_id, Some(selected_client));
5992        assert_eq!(exec_command.strategy_id, StrategyId::from("TEST-001"));
5993        assert_eq!(exec_command.instrument_id, instrument_id);
5994        assert_eq!(exec_command.order_side, Some(OrderSide::Buy));
5995        assert_eq!(exec_command.params.as_ref(), Some(&params));
5996        assert_eq!(exec_command.correlation_id, Some(exec_command.command_id));
5997        assert_eq!(exec_command.causation_id, None);
5998    }
5999
6000    #[rstest]
6001    fn test_cancel_all_orders_strategy_only_filters_by_client() {
6002        let mut strategy = create_test_strategy();
6003        register_strategy(&mut strategy);
6004
6005        let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6006            get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
6007        msgbus::register_trading_command_endpoint(
6008            MessagingSwitchboard::exec_engine_queue_execute(),
6009            exec_handler,
6010        );
6011
6012        let selected_client = ClientId::from("CLIENT-001");
6013        let other_client = ClientId::from("CLIENT-002");
6014        let selected_order = make_accepted_market_order("O-20250208-CANCEL-CLIENT-001");
6015        let other_order = make_accepted_market_order("O-20250208-CANCEL-CLIENT-002");
6016        let cache_rc = strategy.core.cache_rc();
6017        {
6018            let mut cache = cache_rc.borrow_mut();
6019            cache
6020                .add_order(selected_order.clone(), None, Some(selected_client), true)
6021                .unwrap();
6022            cache
6023                .add_order(other_order.clone(), None, Some(other_client), true)
6024                .unwrap();
6025            cache.build_index();
6026        }
6027
6028        strategy
6029            .cancel_all_orders(
6030                selected_order.instrument_id(),
6031                None,
6032                Some(selected_client),
6033                true,
6034                None,
6035            )
6036            .unwrap();
6037
6038        let messages = exec_messages.get_messages();
6039        let cache = strategy.cache();
6040        let cached_selected = cache.order(&selected_order.client_order_id()).unwrap();
6041        let cached_other = cache.order(&other_order.client_order_id()).unwrap();
6042        assert_eq!(messages.len(), 1);
6043        assert!(matches!(
6044            messages.first(),
6045            Some(TradingCommand::CancelOrder(command))
6046                if command.client_id == Some(selected_client)
6047                    && command.client_order_id == selected_order.client_order_id()
6048        ));
6049        assert_eq!(cached_selected.status(), OrderStatus::PendingCancel);
6050        assert_eq!(cached_other.status(), OrderStatus::Accepted);
6051    }
6052
6053    #[rstest]
6054    fn test_cancel_all_orders_strategy_only_filters_by_side_and_preserves_params() {
6055        let mut strategy = create_test_strategy();
6056        register_strategy(&mut strategy);
6057
6058        let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6059            get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
6060        msgbus::register_trading_command_endpoint(
6061            MessagingSwitchboard::exec_engine_queue_execute(),
6062            exec_handler,
6063        );
6064
6065        let buy_order = make_accepted_market_order("O-20250208-CANCEL-SIDE-001");
6066        let mut sell_order = OrderTestBuilder::new(OrderType::Market)
6067            .trader_id(TraderId::from("TRADER-001"))
6068            .strategy_id(StrategyId::from("TEST-001"))
6069            .instrument_id(buy_order.instrument_id())
6070            .client_order_id(ClientOrderId::from("O-20250208-CANCEL-SIDE-002"))
6071            .side(OrderSide::Sell)
6072            .quantity(Quantity::from(100_000))
6073            .build();
6074        let account_id = AccountId::from("ACC-001");
6075        sell_order
6076            .apply(TestOrderEventStubs::submitted(&sell_order, account_id))
6077            .unwrap();
6078        sell_order
6079            .apply(TestOrderEventStubs::accepted(
6080                &sell_order,
6081                account_id,
6082                VenueOrderId::from("O-20250208-CANCEL-SIDE-002"),
6083            ))
6084            .unwrap();
6085        add_order_to_cache(&strategy, &buy_order);
6086        add_order_to_cache(&strategy, &sell_order);
6087        strategy.core.cache_rc().borrow_mut().build_index();
6088
6089        let params = Params::new(
6090            [(
6091                "routing_hint".to_string(),
6092                Value::String("strategy_only".to_string()),
6093            )]
6094            .into(),
6095        );
6096        strategy
6097            .cancel_all_orders(
6098                buy_order.instrument_id(),
6099                Some(OrderSide::Buy),
6100                None,
6101                true,
6102                Some(params.clone()),
6103            )
6104            .unwrap();
6105
6106        let messages = exec_messages.get_messages();
6107        let Some(TradingCommand::CancelOrder(command)) = messages.first() else {
6108            panic!("Expected a CancelOrder command, was {messages:?}");
6109        };
6110        let cache = strategy.cache();
6111        let cached_buy = cache.order(&buy_order.client_order_id()).unwrap();
6112        let cached_sell = cache.order(&sell_order.client_order_id()).unwrap();
6113        assert_eq!(messages.len(), 1);
6114        assert_eq!(command.trader_id, TraderId::from("TRADER-001"));
6115        assert_eq!(command.client_id, None);
6116        assert_eq!(command.strategy_id, StrategyId::from("TEST-001"));
6117        assert_eq!(command.instrument_id, buy_order.instrument_id());
6118        assert_eq!(command.client_order_id, buy_order.client_order_id());
6119        assert_eq!(command.venue_order_id, buy_order.venue_order_id());
6120        assert_eq!(command.params.as_ref(), Some(&params));
6121        assert_eq!(cached_buy.status(), OrderStatus::PendingCancel);
6122        assert_eq!(cached_sell.status(), OrderStatus::Accepted);
6123    }
6124
6125    #[rstest]
6126    fn test_cancel_all_orders_strategy_only_continues_after_error_and_returns_error() {
6127        let mut strategy = create_test_strategy();
6128        register_strategy(&mut strategy);
6129
6130        let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6131            get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
6132        msgbus::register_trading_command_endpoint(
6133            MessagingSwitchboard::exec_engine_queue_execute(),
6134            exec_handler,
6135        );
6136
6137        let mut failing_order = make_accepted_market_order("O-20250208-CANCEL-ERROR-001");
6138        let OrderAny::Market(order) = &mut failing_order else {
6139            panic!("Expected a MarketOrder");
6140        };
6141        order.account_id = None;
6142        let succeeding_order = make_accepted_market_order("O-20250208-CANCEL-ERROR-002");
6143        add_order_to_cache(&strategy, &failing_order);
6144        add_order_to_cache(&strategy, &succeeding_order);
6145        strategy.core.cache_rc().borrow_mut().build_index();
6146
6147        let error = strategy
6148            .cancel_all_orders(failing_order.instrument_id(), None, None, true, None)
6149            .unwrap_err()
6150            .to_string();
6151
6152        let messages = exec_messages.get_messages();
6153        let cache = strategy.cache();
6154        let cached_failing = cache.order(&failing_order.client_order_id()).unwrap();
6155        let cached_succeeding = cache.order(&succeeding_order.client_order_id()).unwrap();
6156        assert_eq!(
6157            error,
6158            "Cannot generate pending cancel event for O-20250208-CANCEL-ERROR-001: \
6159             account_id is not set"
6160        );
6161        assert_eq!(messages.len(), 1);
6162        assert!(matches!(
6163            messages.first(),
6164            Some(TradingCommand::CancelOrder(command))
6165                if command.client_order_id == succeeding_order.client_order_id()
6166        ));
6167        assert_eq!(cached_failing.status(), OrderStatus::Accepted);
6168        assert_eq!(cached_succeeding.status(), OrderStatus::PendingCancel);
6169    }
6170
6171    #[rstest]
6172    fn test_cancel_all_orders_strategy_only_cancels_order_submitted_in_same_handler() {
6173        let mut strategy = create_test_strategy();
6174        register_strategy(&mut strategy);
6175
6176        let (risk_handler, _risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6177            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
6178        msgbus::register_trading_command_endpoint(
6179            MessagingSwitchboard::risk_engine_queue_execute(),
6180            risk_handler,
6181        );
6182        let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6183            get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
6184        msgbus::register_trading_command_endpoint(
6185            MessagingSwitchboard::exec_engine_queue_execute(),
6186            exec_handler,
6187        );
6188
6189        let order = OrderTestBuilder::new(OrderType::Limit)
6190            .trader_id(TraderId::from("TRADER-001"))
6191            .strategy_id(StrategyId::from("TEST-001"))
6192            .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
6193            .client_order_id(ClientOrderId::from("O-20250208-CANCEL-ALL-LOCAL-001"))
6194            .side(OrderSide::Buy)
6195            .price(Price::from("50000.0"))
6196            .quantity(Quantity::from(100_000))
6197            .build();
6198        let sibling_order = OrderTestBuilder::new(OrderType::Limit)
6199            .trader_id(TraderId::from("TRADER-001"))
6200            .strategy_id(StrategyId::from("SIBLING-001"))
6201            .instrument_id(order.instrument_id())
6202            .client_order_id(ClientOrderId::from("O-20250208-CANCEL-ALL-LOCAL-002"))
6203            .side(OrderSide::Buy)
6204            .price(Price::from("50000.0"))
6205            .quantity(Quantity::from(100_000))
6206            .build();
6207        add_order_to_cache(&strategy, &sibling_order);
6208
6209        // The submit command is queued, so the order is still INITIALIZED when canceled
6210        strategy
6211            .submit_order(order.clone(), None, None, None)
6212            .unwrap();
6213        strategy
6214            .cancel_all_orders(order.instrument_id(), None, None, true, None)
6215            .unwrap();
6216
6217        let messages = exec_messages.get_messages();
6218        assert_eq!(messages.len(), 1);
6219        assert!(matches!(
6220            messages.first(),
6221            Some(TradingCommand::CancelOrder(command))
6222                if command.client_order_id == order.client_order_id()
6223        ));
6224    }
6225
6226    #[rstest]
6227    fn test_cancel_orders_marks_orders_pending_cancel_locally_before_send() {
6228        let mut strategy = create_test_strategy();
6229        register_strategy(&mut strategy);
6230
6231        let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6232            get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
6233        msgbus::register_trading_command_endpoint(
6234            MessagingSwitchboard::exec_engine_queue_execute(),
6235            exec_handler,
6236        );
6237
6238        let (event_handler, event_messages): (_, TypedMessageSavingHandler<OrderEventAny>) =
6239            get_typed_message_saving_handler(Some(Ustr::from("events.order.batch_pending_cancel")));
6240        let order1 = make_accepted_market_order("O-20250208-CANCEL-001");
6241        let order2 = make_accepted_market_order("O-20250208-CANCEL-002");
6242        let topic = format!("events.order.{}", order1.strategy_id());
6243        msgbus::subscribe_order_events(topic.clone().into(), event_handler.clone(), None);
6244        add_order_to_cache(&strategy, &order1);
6245        add_order_to_cache(&strategy, &order2);
6246
6247        strategy
6248            .cancel_orders(
6249                vec![order1.client_order_id(), order2.client_order_id()],
6250                None,
6251                None,
6252            )
6253            .unwrap();
6254
6255        msgbus::unsubscribe_order_events(topic.into(), &event_handler);
6256
6257        let cache = strategy.cache();
6258        let cached_order1 = cache.order(&order1.client_order_id()).unwrap();
6259        let cached_order2 = cache.order(&order2.client_order_id()).unwrap();
6260        assert_eq!(cached_order1.status(), OrderStatus::PendingCancel);
6261        assert_eq!(cached_order2.status(), OrderStatus::PendingCancel);
6262        let cache = strategy.core.cache_ref();
6263        assert!(cache.is_order_pending_cancel_local(&order1.client_order_id()));
6264        assert!(cache.is_order_pending_cancel_local(&order2.client_order_id()));
6265
6266        let exec_messages = exec_messages.get_messages();
6267        assert_eq!(exec_messages.len(), 1);
6268        let Some(TradingCommand::CancelOrders(command)) = exec_messages.first() else {
6269            panic!("expected BatchCancelOrders command");
6270        };
6271        assert_eq!(command.cancels.len(), 2);
6272
6273        let event_messages = event_messages.get_messages();
6274        assert_eq!(event_messages.len(), 2);
6275        assert!(
6276            event_messages
6277                .iter()
6278                .all(|event| matches!(event, OrderEventAny::PendingCancel(_)))
6279        );
6280    }
6281
6282    #[rstest]
6283    fn test_cancel_order_updates_own_book_status_before_send() {
6284        let mut strategy = create_test_strategy();
6285        register_strategy(&mut strategy);
6286
6287        let (exec_handler, _exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6288            get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
6289        msgbus::register_trading_command_endpoint(
6290            MessagingSwitchboard::exec_engine_queue_execute(),
6291            exec_handler,
6292        );
6293
6294        let order = make_accepted_limit_order("O-20250208-CANCEL-OWN-BOOK-001");
6295        add_order_to_cache_and_own_book(&strategy, &order);
6296
6297        strategy
6298            .cancel_order(order.client_order_id(), None, None)
6299            .unwrap();
6300
6301        let mut accepted = AHashSet::new();
6302        accepted.insert(OrderStatus::Accepted);
6303        let mut pending_cancel = AHashSet::new();
6304        pending_cancel.insert(OrderStatus::PendingCancel);
6305
6306        let cache = strategy.cache();
6307        let own_book = cache.own_order_book(&order.instrument_id()).unwrap();
6308        assert!(own_book.bids_as_map(Some(&accepted), None, None).is_empty());
6309        let pending_bids = own_book.bids_as_map(Some(&pending_cancel), None, None);
6310        assert_eq!(pending_bids.values().map(Vec::len).sum::<usize>(), 1);
6311    }
6312
6313    #[rstest]
6314    fn test_cancel_order_returns_error_when_not_in_cache() {
6315        let mut strategy = create_test_strategy();
6316        register_strategy(&mut strategy);
6317
6318        let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6319            get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
6320        msgbus::register_trading_command_endpoint(
6321            MessagingSwitchboard::exec_engine_queue_execute(),
6322            exec_handler,
6323        );
6324
6325        let missing_id = ClientOrderId::from("O-MISSING");
6326        let err = strategy
6327            .cancel_order(missing_id, None, None)
6328            .expect_err("expected cancel_order to fail when order is not in cache");
6329
6330        assert_eq!(
6331            err.to_string(),
6332            format!("Cannot cancel order: {ORDER_NOT_FOUND}: {missing_id}")
6333        );
6334        assert!(exec_messages.get_messages().is_empty());
6335    }
6336
6337    #[rstest]
6338    fn test_modify_order_returns_error_when_not_in_cache() {
6339        let mut strategy = create_test_strategy();
6340        register_strategy(&mut strategy);
6341
6342        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6343            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
6344        msgbus::register_trading_command_endpoint(
6345            MessagingSwitchboard::risk_engine_queue_execute(),
6346            risk_handler,
6347        );
6348
6349        let missing_id = ClientOrderId::from("O-MISSING");
6350        let err = strategy
6351            .modify_order(missing_id, Some(Quantity::from(1)), None, None, None, None)
6352            .expect_err("expected modify_order to fail when order is not in cache");
6353
6354        assert_eq!(
6355            err.to_string(),
6356            format!("Cannot modify order: {ORDER_NOT_FOUND}: {missing_id}")
6357        );
6358        assert!(risk_messages.get_messages().is_empty());
6359    }
6360
6361    #[rstest]
6362    fn test_modify_orders_returns_error_when_any_id_missing() {
6363        let mut strategy = create_test_strategy();
6364        register_strategy(&mut strategy);
6365
6366        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6367            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
6368        msgbus::register_trading_command_endpoint(
6369            MessagingSwitchboard::risk_engine_queue_execute(),
6370            risk_handler,
6371        );
6372
6373        let order = make_accepted_limit_order("O-PRESENT");
6374        add_order_to_cache(&strategy, &order);
6375
6376        let missing_id = ClientOrderId::from("O-MISSING");
6377        let err = strategy
6378            .modify_orders(
6379                vec![
6380                    (
6381                        order.client_order_id(),
6382                        None,
6383                        Some(Price::from("51000.0")),
6384                        None,
6385                    ),
6386                    (missing_id, Some(Quantity::from("2.0")), None, None),
6387                ],
6388                None,
6389                None,
6390            )
6391            .expect_err("expected modify_orders to fail when any id is missing");
6392
6393        assert_eq!(
6394            err.to_string(),
6395            format!("Cannot modify order: {ORDER_NOT_FOUND}: {missing_id}")
6396        );
6397        assert!(risk_messages.get_messages().is_empty());
6398    }
6399
6400    #[rstest]
6401    fn test_cancel_orders_returns_error_when_any_id_missing() {
6402        let mut strategy = create_test_strategy();
6403        register_strategy(&mut strategy);
6404
6405        let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6406            get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
6407        msgbus::register_trading_command_endpoint(
6408            MessagingSwitchboard::exec_engine_queue_execute(),
6409            exec_handler,
6410        );
6411
6412        let order = make_accepted_limit_order("O-PRESENT");
6413        add_order_to_cache(&strategy, &order);
6414
6415        let missing_id = ClientOrderId::from("O-MISSING");
6416        let err = strategy
6417            .cancel_orders(vec![order.client_order_id(), missing_id], None, None)
6418            .expect_err("expected cancel_orders to fail when any id is missing");
6419
6420        assert_eq!(
6421            err.to_string(),
6422            format!("Cannot cancel order: {ORDER_NOT_FOUND}: {missing_id}")
6423        );
6424        assert!(exec_messages.get_messages().is_empty());
6425    }
6426
6427    // -- GTD EXPIRY TESTS ----------------------------------------------------------------------------
6428
6429    #[rstest]
6430    fn test_has_gtd_expiry_timer_when_timer_not_set() {
6431        let mut strategy = create_test_strategy();
6432        let client_order_id = ClientOrderId::from("O-001");
6433
6434        assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
6435    }
6436
6437    #[rstest]
6438    fn test_has_gtd_expiry_timer_when_timer_set() {
6439        let mut strategy = create_test_strategy();
6440        let client_order_id = ClientOrderId::from("O-001");
6441
6442        strategy
6443            .core
6444            .gtd_timers
6445            .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
6446
6447        assert!(strategy.has_gtd_expiry_timer(&client_order_id));
6448    }
6449
6450    #[rstest]
6451    fn test_cancel_gtd_expiry_removes_timer() {
6452        let mut strategy = create_test_strategy();
6453        register_strategy(&mut strategy);
6454
6455        let client_order_id = ClientOrderId::from("O-001");
6456        strategy
6457            .core
6458            .gtd_timers
6459            .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
6460
6461        strategy.cancel_gtd_expiry(&client_order_id);
6462
6463        assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
6464    }
6465
6466    #[rstest]
6467    fn test_cancel_gtd_expiry_when_timer_not_set() {
6468        let mut strategy = create_test_strategy();
6469        register_strategy(&mut strategy);
6470
6471        let client_order_id = ClientOrderId::from("O-001");
6472
6473        strategy.cancel_gtd_expiry(&client_order_id);
6474
6475        assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
6476    }
6477
6478    #[rstest]
6479    #[case::matching_order(Ustr::from("GTD-EXPIRY:O-PRESENT"))]
6480    #[case::empty_order_id(Ustr::from("GTD-EXPIRY:"))]
6481    fn test_route_time_event_ignores_unregistered_gtd_timer(#[case] timer_name: Ustr) {
6482        let mut strategy = create_test_strategy();
6483        register_strategy(&mut strategy);
6484
6485        let order = make_accepted_limit_order("O-PRESENT");
6486        let client_order_id = order.client_order_id();
6487        add_order_to_cache(&strategy, &order);
6488        let event = TimeEvent::new(
6489            timer_name,
6490            UUID4::new(),
6491            UnixNanos::default(),
6492            UnixNanos::default(),
6493        );
6494
6495        route_time_event(&mut strategy, &event);
6496
6497        let cache = strategy.core.cache_ref();
6498        let cached_order = cache.order(&client_order_id).unwrap();
6499        assert_eq!(cached_order.status(), OrderStatus::Accepted);
6500        assert!(!strategy.core.gtd_timers.contains_key(&client_order_id));
6501    }
6502
6503    #[rstest]
6504    #[case::filled(make_filled)]
6505    #[case::canceled(make_canceled)]
6506    #[case::rejected(make_rejected)]
6507    #[case::expired(make_expired)]
6508    #[case::fill_voided(make_terminal_fill_voided)]
6509    fn test_handle_order_event_cancels_gtd_timer_for_terminal_event(
6510        #[case] make_event: fn(ClientOrderId) -> OrderEventAny,
6511    ) {
6512        let mut strategy = create_test_strategy();
6513        register_strategy(&mut strategy);
6514        start_strategy(&mut strategy);
6515
6516        let client_order_id = ClientOrderId::from("O-001");
6517        strategy
6518            .core
6519            .gtd_timers
6520            .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
6521
6522        strategy.handle_order_event(make_event(client_order_id));
6523
6524        assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
6525    }
6526
6527    #[rstest]
6528    #[case::partial_fill(make_filled)]
6529    #[case::non_reopened_fill_void(make_terminal_fill_voided)]
6530    fn test_handle_order_event_keeps_gtd_timer_when_cached_order_remains_open(
6531        #[case] make_event: fn(ClientOrderId) -> OrderEventAny,
6532    ) {
6533        let mut strategy = create_test_strategy();
6534        register_strategy(&mut strategy);
6535        start_strategy(&mut strategy);
6536
6537        let client_order_id = ClientOrderId::from("O-001");
6538        let order = make_accepted_limit_order(client_order_id.as_str());
6539        add_order_to_cache(&strategy, &order);
6540        strategy
6541            .core
6542            .gtd_timers
6543            .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
6544
6545        strategy.handle_order_event(make_event(client_order_id));
6546
6547        assert!(strategy.has_gtd_expiry_timer(&client_order_id));
6548    }
6549
6550    #[rstest]
6551    #[case::filled(make_filled)]
6552    #[case::canceled(make_canceled)]
6553    #[case::rejected(make_rejected)]
6554    #[case::expired(make_expired)]
6555    #[case::fill_voided(make_terminal_fill_voided)]
6556    fn test_handle_order_event_cancels_gtd_timer_when_stopped(
6557        #[case] make_event: fn(ClientOrderId) -> OrderEventAny,
6558    ) {
6559        let mut strategy = create_test_strategy();
6560        register_strategy(&mut strategy);
6561        start_strategy(&mut strategy);
6562
6563        let client_order_id = ClientOrderId::from("O-001");
6564        strategy
6565            .core
6566            .gtd_timers
6567            .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
6568
6569        stop_strategy(&mut strategy);
6570        assert_eq!(strategy.state(), ComponentState::Stopped);
6571
6572        strategy.handle_order_event(make_event(client_order_id));
6573
6574        assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
6575    }
6576
6577    #[rstest]
6578    fn test_handle_order_event_skips_gtd_cancel_for_non_terminal() {
6579        let mut strategy = create_test_strategy();
6580        register_strategy(&mut strategy);
6581        start_strategy(&mut strategy);
6582
6583        let client_order_id = ClientOrderId::from("O-001");
6584        strategy
6585            .core
6586            .gtd_timers
6587            .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
6588
6589        strategy.handle_order_event(make_accepted(client_order_id));
6590
6591        assert!(strategy.has_gtd_expiry_timer(&client_order_id));
6592    }
6593
6594    #[rstest]
6595    fn test_handle_reopened_fill_void_keeps_gtd_timer() {
6596        let mut strategy = create_test_strategy();
6597        register_strategy(&mut strategy);
6598        start_strategy(&mut strategy);
6599
6600        let client_order_id = ClientOrderId::from("O-001");
6601        strategy
6602            .core
6603            .gtd_timers
6604            .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
6605
6606        strategy.handle_order_event(make_fill_voided(client_order_id, true));
6607
6608        assert!(strategy.has_gtd_expiry_timer(&client_order_id));
6609    }
6610
6611    #[rstest]
6612    fn test_cancel_denied_after_expiry_does_not_resubmit_cancel() {
6613        let mut strategy = create_gtd_managed_strategy();
6614        let clock = register_gtd_strategy(&mut strategy);
6615        start_strategy(&mut strategy);
6616        clock.borrow_mut().set_time(UnixNanos::from(1_000_000_000));
6617        let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6618            get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
6619        msgbus::register_trading_command_endpoint(
6620            MessagingSwitchboard::exec_engine_queue_execute(),
6621            exec_handler,
6622        );
6623        let order = make_accepted_gtd_limit_order("O-001", UnixNanos::from(1));
6624        let client_order_id = order.client_order_id();
6625        add_order_to_cache(&strategy, &order);
6626        strategy.handle_order_event(make_cancel_denied(client_order_id));
6627        assert!(exec_messages.get_messages().is_empty());
6628        assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
6629        assert_eq!(
6630            strategy
6631                .core
6632                .cache_ref()
6633                .order(&client_order_id)
6634                .unwrap()
6635                .status(),
6636            OrderStatus::Accepted
6637        );
6638    }
6639
6640    #[rstest]
6641    #[case(false)]
6642    #[case(true)]
6643    fn test_handle_cancel_refusal_restores_future_gtd_timer(#[case] denied: bool) {
6644        let mut strategy = create_gtd_managed_strategy();
6645        let clock = register_gtd_strategy(&mut strategy);
6646        start_strategy(&mut strategy);
6647        let expire_time = UnixNanos::from(clock.borrow().timestamp_ns().as_u64() + 1_000_000_000);
6648
6649        let order = make_accepted_gtd_limit_order("O-001", expire_time);
6650        let client_order_id = order.client_order_id();
6651        add_order_to_cache(&strategy, &order);
6652
6653        strategy.handle_order_event(if denied {
6654            make_cancel_denied(client_order_id)
6655        } else {
6656            make_cancel_rejected(client_order_id)
6657        });
6658
6659        assert_eq!(
6660            strategy.core.gtd_timers.get(&client_order_id),
6661            Some(&Ustr::from("GTD-EXPIRY:O-001"))
6662        );
6663    }
6664
6665    #[rstest]
6666    fn test_cancel_rejected_restores_timer_after_cancel_order_removes_it() {
6667        let mut strategy = create_gtd_managed_strategy();
6668        let clock = register_gtd_strategy(&mut strategy);
6669        start_strategy(&mut strategy);
6670        let expire_time = UnixNanos::from(clock.borrow().timestamp_ns().as_u64() + 1_000_000_000);
6671        let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6672            get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
6673        msgbus::register_trading_command_endpoint(
6674            MessagingSwitchboard::exec_engine_queue_execute(),
6675            exec_handler,
6676        );
6677
6678        let order = make_accepted_gtd_limit_order("O-001", expire_time);
6679        let client_order_id = order.client_order_id();
6680        add_order_to_cache(&strategy, &order);
6681        strategy
6682            .core
6683            .gtd_timers
6684            .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
6685
6686        strategy.cancel_order(client_order_id, None, None).unwrap();
6687        assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
6688
6689        let event = make_cancel_rejected(client_order_id);
6690        strategy
6691            .core
6692            .cache_rc()
6693            .borrow_mut()
6694            .update_order(&event)
6695            .unwrap();
6696        assert_eq!(
6697            strategy
6698                .core
6699                .cache_ref()
6700                .order(&client_order_id)
6701                .unwrap()
6702                .status(),
6703            OrderStatus::Accepted
6704        );
6705
6706        strategy.handle_order_event(event);
6707
6708        assert_eq!(
6709            strategy.core.gtd_timers.get(&client_order_id),
6710            Some(&Ustr::from("GTD-EXPIRY:O-001"))
6711        );
6712        assert_eq!(exec_messages.get_messages().len(), 1);
6713    }
6714
6715    #[rstest]
6716    fn test_cancel_rejected_while_submitted_restores_timer_through_acceptance() {
6717        let mut strategy = create_gtd_managed_strategy();
6718        let clock = register_gtd_strategy(&mut strategy);
6719        start_strategy(&mut strategy);
6720        let expire_time = UnixNanos::from(clock.borrow().timestamp_ns().as_u64() + 1_000_000_000);
6721        let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6722            get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
6723        msgbus::register_trading_command_endpoint(
6724            MessagingSwitchboard::exec_engine_queue_execute(),
6725            exec_handler,
6726        );
6727
6728        let order = make_submitted_gtd_limit_order("O-001", expire_time);
6729        let client_order_id = order.client_order_id();
6730        add_order_to_cache(&strategy, &order);
6731        strategy.set_gtd_expiry(&order).unwrap();
6732
6733        strategy.cancel_order(client_order_id, None, None).unwrap();
6734        assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
6735
6736        let cancel_rejected = make_cancel_rejected(client_order_id);
6737        strategy
6738            .core
6739            .cache_rc()
6740            .borrow_mut()
6741            .update_order(&cancel_rejected)
6742            .unwrap();
6743        assert_eq!(
6744            strategy
6745                .core
6746                .cache_ref()
6747                .order(&client_order_id)
6748                .unwrap()
6749                .status(),
6750            OrderStatus::Submitted
6751        );
6752        strategy.handle_order_event(cancel_rejected);
6753        assert_eq!(
6754            strategy.core.gtd_timers.get(&client_order_id),
6755            Some(&Ustr::from("GTD-EXPIRY:O-001"))
6756        );
6757        assert_eq!(strategy.core.gtd_timers.len(), 1);
6758
6759        let accepted = make_accepted(client_order_id);
6760        strategy
6761            .core
6762            .cache_rc()
6763            .borrow_mut()
6764            .update_order(&accepted)
6765            .unwrap();
6766        strategy.handle_order_event(accepted);
6767
6768        assert_eq!(
6769            strategy.core.gtd_timers.get(&client_order_id),
6770            Some(&Ustr::from("GTD-EXPIRY:O-001"))
6771        );
6772        assert_eq!(strategy.core.gtd_timers.len(), 1);
6773        assert_eq!(exec_messages.get_messages().len(), 1);
6774    }
6775
6776    #[rstest]
6777    #[case::future_expiry(false)]
6778    #[case::elapsed_expiry(true)]
6779    fn test_cancel_rejected_after_acceptance_restores_gtd_expiry(#[case] expired: bool) {
6780        let mut strategy = create_gtd_managed_strategy();
6781        let clock = register_gtd_strategy(&mut strategy);
6782        start_strategy(&mut strategy);
6783        let expire_time = UnixNanos::from(1_000_000_000);
6784        let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6785            get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
6786        msgbus::register_trading_command_endpoint(
6787            MessagingSwitchboard::exec_engine_queue_execute(),
6788            exec_handler,
6789        );
6790        let order = make_submitted_gtd_limit_order("O-001", expire_time);
6791        let client_order_id = order.client_order_id();
6792        add_order_to_cache(&strategy, &order);
6793        strategy.set_gtd_expiry(&order).unwrap();
6794
6795        strategy.cancel_order(client_order_id, None, None).unwrap();
6796        let accepted = make_accepted(client_order_id);
6797        strategy
6798            .core
6799            .cache_rc()
6800            .borrow_mut()
6801            .update_order(&accepted)
6802            .unwrap();
6803        strategy.handle_order_event(accepted);
6804        assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
6805        assert_eq!(exec_messages.get_messages().len(), 1);
6806
6807        if expired {
6808            clock.borrow_mut().set_time(expire_time);
6809        }
6810        let cancel_rejected = make_cancel_rejected(client_order_id);
6811        strategy
6812            .core
6813            .cache_rc()
6814            .borrow_mut()
6815            .update_order(&cancel_rejected)
6816            .unwrap();
6817        strategy.handle_order_event(cancel_rejected);
6818
6819        if !expired {
6820            assert_eq!(
6821                strategy.core.gtd_timers.get(&client_order_id),
6822                Some(&Ustr::from("GTD-EXPIRY:O-001"))
6823            );
6824            assert_eq!(
6825                strategy
6826                    .core
6827                    .cache_ref()
6828                    .order(&client_order_id)
6829                    .unwrap()
6830                    .status(),
6831                OrderStatus::Accepted
6832            );
6833            assert_eq!(exec_messages.get_messages().len(), 1);
6834            let events = clock.borrow_mut().advance_time(expire_time, true);
6835            assert_eq!(events.len(), 1);
6836            route_time_event(&mut strategy, &events[0]);
6837        }
6838
6839        assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
6840        assert_eq!(
6841            strategy
6842                .core
6843                .cache_ref()
6844                .order(&client_order_id)
6845                .unwrap()
6846                .status(),
6847            OrderStatus::PendingCancel
6848        );
6849        let commands = exec_messages.get_messages();
6850        assert_eq!(commands.len(), 2);
6851        for command in commands {
6852            assert!(matches!(
6853                command,
6854                TradingCommand::CancelOrder(command) if command.client_order_id == client_order_id
6855            ));
6856        }
6857    }
6858
6859    #[rstest]
6860    fn test_cancel_rejected_while_submitted_retries_already_expired_gtd_order() {
6861        let mut strategy = create_gtd_managed_strategy();
6862        let clock = register_gtd_strategy(&mut strategy);
6863        start_strategy(&mut strategy);
6864        clock.borrow_mut().set_time(UnixNanos::from(1_000_000_000));
6865        let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6866            get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
6867        msgbus::register_trading_command_endpoint(
6868            MessagingSwitchboard::exec_engine_queue_execute(),
6869            exec_handler,
6870        );
6871
6872        let order = make_submitted_gtd_limit_order("O-001", UnixNanos::from(1));
6873        let client_order_id = order.client_order_id();
6874        add_order_to_cache(&strategy, &order);
6875
6876        strategy.handle_order_event(make_cancel_rejected(client_order_id));
6877
6878        assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
6879        assert!(matches!(
6880            exec_messages.get_messages().as_slice(),
6881            [TradingCommand::CancelOrder(command)]
6882                if command.client_order_id == client_order_id
6883        ));
6884        assert_eq!(
6885            strategy
6886                .core
6887                .cache_ref()
6888                .order(&client_order_id)
6889                .unwrap()
6890                .status(),
6891            OrderStatus::PendingCancel
6892        );
6893    }
6894
6895    #[rstest]
6896    fn test_cancel_rejected_while_submitted_preserves_existing_gtd_timer() {
6897        let mut strategy = create_gtd_managed_strategy();
6898        let clock = register_gtd_strategy(&mut strategy);
6899        start_strategy(&mut strategy);
6900        let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6901            get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
6902        msgbus::register_trading_command_endpoint(
6903            MessagingSwitchboard::exec_engine_queue_execute(),
6904            exec_handler,
6905        );
6906
6907        let order =
6908            make_submitted_gtd_limit_order("O-001", UnixNanos::from(2_000_000_000_000_000_000));
6909        let client_order_id = order.client_order_id();
6910        add_order_to_cache(&strategy, &order);
6911        let timer_name = Ustr::from("GTD-EXPIRY:O-001");
6912        strategy.core.gtd_timers.insert(client_order_id, timer_name);
6913
6914        strategy.handle_order_event(make_cancel_rejected(client_order_id));
6915
6916        assert_eq!(
6917            strategy.core.gtd_timers.get(&client_order_id),
6918            Some(&timer_name)
6919        );
6920        assert_eq!(strategy.core.gtd_timers.len(), 1);
6921        // The map entry is seeded without a clock alert, so an unintended
6922        // re-arm is observable only as a new alert on the clock.
6923        assert!(!clock.borrow().timer_names().contains(&timer_name.as_str()));
6924        assert!(exec_messages.get_messages().is_empty());
6925    }
6926
6927    #[rstest]
6928    fn test_handle_cancel_rejected_retries_already_expired_gtd_order() {
6929        let mut strategy = create_gtd_managed_strategy();
6930        let clock = register_gtd_strategy(&mut strategy);
6931        start_strategy(&mut strategy);
6932        clock.borrow_mut().set_time(UnixNanos::from(1_000_000_000));
6933        let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6934            get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
6935        msgbus::register_trading_command_endpoint(
6936            MessagingSwitchboard::exec_engine_queue_execute(),
6937            exec_handler,
6938        );
6939
6940        let order = make_accepted_gtd_limit_order("O-001", UnixNanos::from(1));
6941        let client_order_id = order.client_order_id();
6942        add_order_to_cache(&strategy, &order);
6943
6944        strategy.handle_order_event(make_cancel_rejected(client_order_id));
6945
6946        assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
6947        assert!(matches!(
6948            exec_messages.get_messages().as_slice(),
6949            [TradingCommand::CancelOrder(command)]
6950                if command.client_order_id == client_order_id
6951        ));
6952        assert_eq!(
6953            strategy
6954                .core
6955                .cache_ref()
6956                .order(&client_order_id)
6957                .unwrap()
6958                .status(),
6959            OrderStatus::PendingCancel
6960        );
6961    }
6962
6963    #[rstest]
6964    fn test_handle_cancel_rejected_preserves_existing_gtd_timer() {
6965        let mut strategy = create_gtd_managed_strategy();
6966        let _clock = register_gtd_strategy(&mut strategy);
6967        start_strategy(&mut strategy);
6968        let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6969            get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
6970        msgbus::register_trading_command_endpoint(
6971            MessagingSwitchboard::exec_engine_queue_execute(),
6972            exec_handler,
6973        );
6974
6975        let order =
6976            make_accepted_gtd_limit_order("O-001", UnixNanos::from(2_000_000_000_000_000_000));
6977        let client_order_id = order.client_order_id();
6978        add_order_to_cache(&strategy, &order);
6979        strategy
6980            .core
6981            .gtd_timers
6982            .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
6983
6984        strategy.handle_order_event(make_cancel_rejected(client_order_id));
6985
6986        assert_eq!(
6987            strategy.core.gtd_timers.get(&client_order_id),
6988            Some(&Ustr::from("GTD-EXPIRY:O-001"))
6989        );
6990        assert!(exec_messages.get_messages().is_empty());
6991    }
6992
6993    #[rstest]
6994    fn test_handle_cancel_rejected_preserves_existing_timer_for_expired_order() {
6995        let mut strategy = create_gtd_managed_strategy();
6996        let clock = register_gtd_strategy(&mut strategy);
6997        start_strategy(&mut strategy);
6998        clock.borrow_mut().set_time(UnixNanos::from(1_000_000_000));
6999        let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
7000            get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
7001        msgbus::register_trading_command_endpoint(
7002            MessagingSwitchboard::exec_engine_queue_execute(),
7003            exec_handler,
7004        );
7005
7006        let order = make_accepted_gtd_limit_order("O-001", UnixNanos::from(1));
7007        let client_order_id = order.client_order_id();
7008        add_order_to_cache(&strategy, &order);
7009        strategy
7010            .core
7011            .gtd_timers
7012            .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
7013
7014        strategy.handle_order_event(make_cancel_rejected(client_order_id));
7015
7016        assert_eq!(
7017            strategy.core.gtd_timers.get(&client_order_id),
7018            Some(&Ustr::from("GTD-EXPIRY:O-001"))
7019        );
7020        assert!(exec_messages.get_messages().is_empty());
7021    }
7022
7023    #[rstest]
7024    fn test_restored_gtd_timer_event_cancels_order() {
7025        let mut strategy = create_gtd_managed_strategy();
7026        let clock = register_gtd_strategy(&mut strategy);
7027        start_strategy(&mut strategy);
7028        let expire_time = UnixNanos::from(clock.borrow().timestamp_ns().as_u64() + 1_000_000_000);
7029        let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
7030            get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
7031        msgbus::register_trading_command_endpoint(
7032            MessagingSwitchboard::exec_engine_queue_execute(),
7033            exec_handler,
7034        );
7035
7036        let order = make_accepted_gtd_limit_order("O-001", expire_time);
7037        let client_order_id = order.client_order_id();
7038        add_order_to_cache(&strategy, &order);
7039
7040        strategy.handle_order_event(make_cancel_rejected(client_order_id));
7041        assert!(strategy.has_gtd_expiry_timer(&client_order_id));
7042
7043        let events = clock.borrow_mut().advance_time(expire_time, true);
7044        for event in &events {
7045            route_time_event(&mut strategy, event);
7046        }
7047
7048        assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
7049        assert!(matches!(
7050            exec_messages.get_messages().as_slice(),
7051            [TradingCommand::CancelOrder(command)]
7052                if command.client_order_id == client_order_id
7053        ));
7054    }
7055
7056    enum IneligibleRestoreCase {
7057        MissingOrder,
7058        ClosedOrder,
7059        NonGtdOrder,
7060        ManagementDisabled,
7061        Stopped,
7062        Emulated,
7063    }
7064
7065    fn assert_cancel_rejected_does_not_restore(case: &IneligibleRestoreCase) {
7066        let mut strategy = match case {
7067            IneligibleRestoreCase::ManagementDisabled => create_test_strategy(),
7068            _ => create_gtd_managed_strategy(),
7069        };
7070        let _clock = register_gtd_strategy(&mut strategy);
7071        start_strategy(&mut strategy);
7072        let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
7073            get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
7074        msgbus::register_trading_command_endpoint(
7075            MessagingSwitchboard::exec_engine_queue_execute(),
7076            exec_handler,
7077        );
7078
7079        let client_order_id = ClientOrderId::from("O-001");
7080        let order = match case {
7081            IneligibleRestoreCase::MissingOrder => None,
7082            IneligibleRestoreCase::NonGtdOrder => {
7083                Some(make_accepted_limit_order(client_order_id.as_str()))
7084            }
7085            IneligibleRestoreCase::ClosedOrder => {
7086                let mut order = make_accepted_gtd_limit_order(
7087                    client_order_id.as_str(),
7088                    UnixNanos::from(2_000_000_000_000_000_000),
7089                );
7090                order.apply(make_canceled(client_order_id)).unwrap();
7091                Some(order)
7092            }
7093            IneligibleRestoreCase::Emulated => {
7094                let mut order = make_submitted_gtd_limit_order(
7095                    client_order_id.as_str(),
7096                    UnixNanos::from(2_000_000_000_000_000_000),
7097                );
7098                order.set_emulation_trigger(Some(TriggerType::BidAsk));
7099                Some(order)
7100            }
7101            IneligibleRestoreCase::ManagementDisabled | IneligibleRestoreCase::Stopped => {
7102                Some(make_accepted_gtd_limit_order(
7103                    client_order_id.as_str(),
7104                    UnixNanos::from(2_000_000_000_000_000_000),
7105                ))
7106            }
7107        };
7108
7109        if let Some(order) = order {
7110            add_order_to_cache(&strategy, &order);
7111        }
7112
7113        if matches!(case, IneligibleRestoreCase::Stopped) {
7114            stop_strategy(&mut strategy);
7115        }
7116
7117        strategy.handle_order_event(make_cancel_rejected(client_order_id));
7118
7119        assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
7120        assert!(exec_messages.get_messages().is_empty());
7121    }
7122
7123    #[rstest]
7124    fn test_handle_cancel_rejected_does_not_restore_missing_order() {
7125        assert_cancel_rejected_does_not_restore(&IneligibleRestoreCase::MissingOrder);
7126    }
7127
7128    #[rstest]
7129    fn test_handle_cancel_rejected_does_not_restore_closed_order() {
7130        assert_cancel_rejected_does_not_restore(&IneligibleRestoreCase::ClosedOrder);
7131    }
7132
7133    #[rstest]
7134    fn test_handle_cancel_rejected_does_not_restore_non_gtd_order() {
7135        assert_cancel_rejected_does_not_restore(&IneligibleRestoreCase::NonGtdOrder);
7136    }
7137
7138    #[rstest]
7139    fn test_handle_cancel_rejected_does_not_restore_when_disabled() {
7140        assert_cancel_rejected_does_not_restore(&IneligibleRestoreCase::ManagementDisabled);
7141    }
7142
7143    #[rstest]
7144    fn test_handle_cancel_rejected_does_not_restore_when_stopped() {
7145        assert_cancel_rejected_does_not_restore(&IneligibleRestoreCase::Stopped);
7146    }
7147
7148    #[rstest]
7149    fn test_handle_cancel_rejected_does_not_restore_emulated_order() {
7150        assert_cancel_rejected_does_not_restore(&IneligibleRestoreCase::Emulated);
7151    }
7152
7153    #[rstest]
7154    fn test_handle_order_event_skips_dispatch_when_stopped() {
7155        let mut strategy = create_test_strategy();
7156        register_strategy(&mut strategy);
7157        start_strategy(&mut strategy);
7158        stop_strategy(&mut strategy);
7159        assert_eq!(strategy.state(), ComponentState::Stopped);
7160
7161        strategy.handle_order_event(make_rejected(ClientOrderId::from("O-001")));
7162
7163        assert!(!strategy.on_order_event_called);
7164        assert!(!strategy.on_order_rejected_called);
7165    }
7166
7167    #[rstest]
7168    #[case::enabled(true)]
7169    #[case::disabled(false)]
7170    fn test_component_start_reactivates_cached_gtd_orders(#[case] enabled: bool) {
7171        let mut strategy = create_gtd_managed_strategy();
7172        strategy.core.config.manage_gtd_expiry = enabled;
7173        let clock = register_gtd_strategy(&mut strategy);
7174        clock.borrow_mut().set_time(UnixNanos::from(2_000_000_000));
7175        let future = make_accepted_gtd_limit_order("O-FUTURE", UnixNanos::from(3_000_000_000));
7176        let expired = make_accepted_gtd_limit_order("O-EXPIRED", UnixNanos::from(1_000_000_000));
7177        add_order_to_cache(&strategy, &future);
7178        add_order_to_cache(&strategy, &expired);
7179        strategy.core.cache_rc().borrow_mut().build_index();
7180        let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
7181            get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
7182        msgbus::register_trading_command_endpoint(
7183            MessagingSwitchboard::exec_engine_queue_execute(),
7184            exec_handler,
7185        );
7186
7187        Component::start(&mut strategy).unwrap();
7188
7189        assert_eq!(strategy.state(), ComponentState::Running);
7190        assert_eq!(strategy.start_calls, 1);
7191        assert_eq!(
7192            strategy.has_gtd_expiry_timer(&future.client_order_id()),
7193            enabled
7194        );
7195        assert!(!strategy.has_gtd_expiry_timer(&expired.client_order_id()));
7196        assert_eq!(
7197            clock.borrow().timer_names(),
7198            if enabled {
7199                vec!["GTD-EXPIRY:O-FUTURE"]
7200            } else {
7201                vec![]
7202            }
7203        );
7204        let commands = exec_messages.get_messages();
7205
7206        if enabled {
7207            assert!(
7208                matches!(commands.as_slice(), [TradingCommand::CancelOrder(command)]
7209                if command.client_order_id == expired.client_order_id())
7210            );
7211            assert_eq!(
7212                strategy
7213                    .core
7214                    .cache_ref()
7215                    .order(&expired.client_order_id())
7216                    .unwrap()
7217                    .status(),
7218                OrderStatus::PendingCancel
7219            );
7220        } else {
7221            assert!(commands.is_empty());
7222            assert_eq!(
7223                strategy
7224                    .core
7225                    .cache_ref()
7226                    .order(&expired.client_order_id())
7227                    .unwrap()
7228                    .status(),
7229                OrderStatus::Accepted
7230            );
7231        }
7232    }
7233
7234    #[rstest]
7235    fn test_on_start_calls_reactivate_gtd_timers_when_enabled() {
7236        let config = StrategyConfig {
7237            strategy_id: Some(StrategyId::from("TEST-001")),
7238            order_id_tag: Some("001".to_string()),
7239            manage_gtd_expiry: true,
7240            ..Default::default()
7241        };
7242        let mut strategy = TestStrategy::new(config);
7243        register_strategy(&mut strategy);
7244
7245        let result = Strategy::on_start(&mut strategy);
7246        assert!(result.is_ok());
7247    }
7248
7249    #[rstest]
7250    fn test_on_start_does_not_panic_when_gtd_disabled() {
7251        let config = StrategyConfig {
7252            strategy_id: Some(StrategyId::from("TEST-001")),
7253            order_id_tag: Some("001".to_string()),
7254            manage_gtd_expiry: false,
7255            ..Default::default()
7256        };
7257        let mut strategy = TestStrategy::new(config);
7258        register_strategy(&mut strategy);
7259
7260        let result = Strategy::on_start(&mut strategy);
7261        assert!(result.is_ok());
7262    }
7263
7264    #[rstest]
7265    fn test_on_start_errors_when_strategy_id_is_not_set() {
7266        let mut strategy = TestStrategy::new(StrategyConfig::default());
7267
7268        let err = Strategy::on_start(&mut strategy).unwrap_err().to_string();
7269
7270        assert_eq!(err, "Strategy not registered: strategy_id is not set");
7271    }
7272
7273    // -- QUERY TESTS ---------------------------------------------------------------------------------
7274
7275    #[rstest]
7276    fn test_query_account_when_registered() {
7277        let mut strategy = create_test_strategy();
7278        register_strategy(&mut strategy);
7279
7280        let account_id = AccountId::from("ACC-001");
7281
7282        let result = strategy.query_account(account_id, None, None);
7283
7284        assert!(result.is_ok());
7285    }
7286
7287    #[rstest]
7288    fn test_query_account_with_client_id() {
7289        let mut strategy = create_test_strategy();
7290        register_strategy(&mut strategy);
7291
7292        let account_id = AccountId::from("ACC-001");
7293        let client_id = ClientId::from("BINANCE");
7294
7295        let result = strategy.query_account(account_id, Some(client_id), None);
7296
7297        assert!(result.is_ok());
7298    }
7299
7300    #[rstest]
7301    fn test_query_order_when_registered() {
7302        let mut strategy = create_test_strategy();
7303        register_strategy(&mut strategy);
7304
7305        let order = OrderAny::Market(MarketOrder::test_default());
7306
7307        let result = strategy.query_order(&order, None, None);
7308
7309        assert!(result.is_ok());
7310    }
7311
7312    #[rstest]
7313    fn test_query_order_with_client_id() {
7314        let mut strategy = create_test_strategy();
7315        register_strategy(&mut strategy);
7316
7317        let order = OrderAny::Market(MarketOrder::test_default());
7318        let client_id = ClientId::from("BINANCE");
7319
7320        let result = strategy.query_order(&order, Some(client_id), None);
7321
7322        assert!(result.is_ok());
7323    }
7324
7325    #[rstest]
7326    fn test_is_exiting_returns_false_by_default() {
7327        let strategy = create_test_strategy();
7328        assert!(!strategy.is_exiting());
7329    }
7330
7331    #[rstest]
7332    fn test_is_exiting_returns_true_when_set_manually() {
7333        let mut strategy = create_test_strategy();
7334        register_strategy(&mut strategy);
7335
7336        // Manually set the exiting state (as market_exit would do)
7337        strategy.core.is_exiting = true;
7338
7339        assert!(strategy.is_exiting());
7340    }
7341
7342    #[rstest]
7343    fn test_market_exit_sets_is_exiting_flag() {
7344        // Test the state changes that market_exit would make
7345        let mut strategy = create_test_strategy();
7346        register_strategy(&mut strategy);
7347
7348        assert!(!strategy.core.is_exiting);
7349
7350        // Simulate what market_exit does to the state
7351        strategy.core.is_exiting = true;
7352        strategy.core.market_exit_attempts = 0;
7353
7354        assert!(strategy.core.is_exiting);
7355        assert_eq!(strategy.core.market_exit_attempts, 0);
7356    }
7357
7358    #[rstest]
7359    fn test_market_exit_uses_config_time_in_force_and_reduce_only() {
7360        let config = StrategyConfig {
7361            strategy_id: Some(StrategyId::from("TEST-001")),
7362            order_id_tag: Some("001".to_string()),
7363            market_exit_time_in_force: TimeInForce::Ioc,
7364            market_exit_reduce_only: false,
7365            ..Default::default()
7366        };
7367        let strategy = TestStrategy::new(config);
7368
7369        assert_eq!(
7370            strategy.core.config.market_exit_time_in_force,
7371            TimeInForce::Ioc
7372        );
7373        assert!(!strategy.core.config.market_exit_reduce_only);
7374    }
7375
7376    #[rstest]
7377    fn test_market_exit_resets_attempt_counter() {
7378        let mut strategy = create_test_strategy();
7379        register_strategy(&mut strategy);
7380
7381        // Manually set attempts to simulate prior exit
7382        strategy.core.market_exit_attempts = 50;
7383
7384        // Reset via the reset method
7385        strategy.core.reset_market_exit_state();
7386
7387        assert_eq!(strategy.core.market_exit_attempts, 0);
7388    }
7389
7390    #[rstest]
7391    fn test_market_exit_second_call_returns_early_when_exiting() {
7392        let mut strategy = create_test_strategy();
7393        register_strategy(&mut strategy);
7394
7395        // First set exiting to true to simulate an in-progress exit
7396        strategy.core.is_exiting = true;
7397
7398        // Second call should return Ok and not change state
7399        let result = strategy.market_exit();
7400        assert!(result.is_ok());
7401        assert!(strategy.core.is_exiting);
7402    }
7403
7404    #[rstest]
7405    fn test_finalize_market_exit_resets_state() {
7406        let mut strategy = create_test_strategy();
7407        register_strategy(&mut strategy);
7408
7409        // Set up exiting state
7410        strategy.core.is_exiting = true;
7411        strategy.core.pending_stop = true;
7412        strategy.core.market_exit_attempts = 50;
7413
7414        strategy.finalize_market_exit();
7415
7416        assert!(!strategy.core.is_exiting);
7417        assert!(!strategy.core.pending_stop);
7418        assert_eq!(strategy.core.market_exit_attempts, 0);
7419    }
7420
7421    #[rstest]
7422    fn test_market_exit_config_defaults() {
7423        let config = StrategyConfig::default();
7424
7425        assert!(!config.manage_stop);
7426        assert_eq!(config.market_exit_interval_ms, 100);
7427        assert_eq!(config.market_exit_max_attempts, 100);
7428    }
7429
7430    #[rstest]
7431    fn test_market_exit_with_custom_config() {
7432        let config = StrategyConfig {
7433            strategy_id: Some(StrategyId::from("TEST-001")),
7434            manage_stop: true,
7435            market_exit_interval_ms: 50,
7436            market_exit_max_attempts: 200,
7437            ..Default::default()
7438        };
7439        let strategy = TestStrategy::new(config);
7440
7441        assert!(strategy.core.config.manage_stop);
7442        assert_eq!(strategy.core.config.market_exit_interval_ms, 50);
7443        assert_eq!(strategy.core.config.market_exit_max_attempts, 200);
7444    }
7445
7446    #[derive(Debug)]
7447    struct MarketExitHookTrackingStrategy {
7448        core: StrategyCore,
7449        on_market_exit_called: bool,
7450        post_market_exit_called: bool,
7451    }
7452
7453    impl MarketExitHookTrackingStrategy {
7454        fn new(config: StrategyConfig) -> Self {
7455            Self {
7456                core: StrategyCore::new(config),
7457                on_market_exit_called: false,
7458                post_market_exit_called: false,
7459            }
7460        }
7461    }
7462
7463    impl DataActor for MarketExitHookTrackingStrategy {}
7464
7465    nautilus_strategy!(MarketExitHookTrackingStrategy, {
7466        fn on_market_exit(&mut self) {
7467            self.on_market_exit_called = true;
7468        }
7469
7470        fn post_market_exit(&mut self) {
7471            self.post_market_exit_called = true;
7472        }
7473    });
7474
7475    #[rstest]
7476    fn test_market_exit_calls_on_market_exit_hook() {
7477        let config = StrategyConfig {
7478            strategy_id: Some(StrategyId::from("TEST-001")),
7479            order_id_tag: Some("001".to_string()),
7480            ..Default::default()
7481        };
7482        let mut strategy = MarketExitHookTrackingStrategy::new(config);
7483
7484        let trader_id = TraderId::from("TRADER-001");
7485        let clock = Rc::new(RefCell::new(VirtualClock::new()));
7486        let cache = Rc::new(RefCell::new(Cache::default()));
7487        let portfolio = Rc::new(RefCell::new(Portfolio::new(
7488            clock.clone(),
7489            cache.clone(),
7490            None,
7491        )));
7492        strategy
7493            .core
7494            .register(trader_id, clock, cache, portfolio)
7495            .unwrap();
7496        strategy.initialize().unwrap();
7497        strategy.start().unwrap();
7498
7499        let _ = strategy.market_exit();
7500
7501        assert!(strategy.on_market_exit_called);
7502    }
7503
7504    #[rstest]
7505    fn test_finalize_market_exit_calls_post_market_exit_hook() {
7506        let config = StrategyConfig {
7507            strategy_id: Some(StrategyId::from("TEST-001")),
7508            order_id_tag: Some("001".to_string()),
7509            ..Default::default()
7510        };
7511        let mut strategy = MarketExitHookTrackingStrategy::new(config);
7512
7513        let trader_id = TraderId::from("TRADER-001");
7514        let clock = Rc::new(RefCell::new(VirtualClock::new()));
7515        let cache = Rc::new(RefCell::new(Cache::default()));
7516        let portfolio = Rc::new(RefCell::new(Portfolio::new(
7517            clock.clone(),
7518            cache.clone(),
7519            None,
7520        )));
7521        strategy
7522            .core
7523            .register(trader_id, clock, cache, portfolio)
7524            .unwrap();
7525
7526        strategy.core.is_exiting = true;
7527        strategy.finalize_market_exit();
7528
7529        assert!(strategy.post_market_exit_called);
7530    }
7531
7532    #[derive(Debug)]
7533    struct FailingPostExitStrategy {
7534        core: StrategyCore,
7535    }
7536
7537    impl FailingPostExitStrategy {
7538        fn new(config: StrategyConfig) -> Self {
7539            Self {
7540                core: StrategyCore::new(config),
7541            }
7542        }
7543    }
7544
7545    impl DataActor for FailingPostExitStrategy {}
7546
7547    nautilus_strategy!(FailingPostExitStrategy, {
7548        fn post_market_exit(&mut self) {
7549            panic!("Simulated error in post_market_exit");
7550        }
7551    });
7552
7553    #[rstest]
7554    fn test_finalize_market_exit_handles_hook_panic() {
7555        let config = StrategyConfig {
7556            strategy_id: Some(StrategyId::from("TEST-001")),
7557            order_id_tag: Some("001".to_string()),
7558            ..Default::default()
7559        };
7560        let mut strategy = FailingPostExitStrategy::new(config);
7561
7562        let trader_id = TraderId::from("TRADER-001");
7563        let clock = Rc::new(RefCell::new(VirtualClock::new()));
7564        let cache = Rc::new(RefCell::new(Cache::default()));
7565        let portfolio = Rc::new(RefCell::new(Portfolio::new(
7566            clock.clone(),
7567            cache.clone(),
7568            None,
7569        )));
7570        strategy
7571            .core
7572            .register(trader_id, clock, cache, portfolio)
7573            .unwrap();
7574
7575        strategy.core.is_exiting = true;
7576        strategy.core.pending_stop = true;
7577
7578        // This should not panic - it should catch the panic in post_market_exit
7579        strategy.finalize_market_exit();
7580
7581        // State should still be reset
7582        assert!(!strategy.core.is_exiting);
7583        assert!(!strategy.core.pending_stop);
7584    }
7585
7586    #[rstest]
7587    fn test_check_market_exit_increments_attempts_before_finalizing() {
7588        let mut strategy = create_test_strategy();
7589        register_strategy(&mut strategy);
7590
7591        strategy.core.is_exiting = true;
7592        assert_eq!(strategy.core.market_exit_attempts, 0);
7593
7594        let event = TimeEvent::new(
7595            Ustr::from("MARKET_EXIT_CHECK:TEST-001"),
7596            UUID4::new(),
7597            UnixNanos::default(),
7598            UnixNanos::default(),
7599        );
7600        strategy.check_market_exit(event);
7601
7602        // With no orders/positions, check_market_exit will finalize immediately
7603        // which resets attempts to 0. This is correct behavior.
7604        // The attempt WAS incremented to 1 during the check, then reset on finalize.
7605        assert!(!strategy.core.is_exiting);
7606        assert_eq!(strategy.core.market_exit_attempts, 0);
7607    }
7608
7609    #[rstest]
7610    fn test_route_time_event_is_idempotent_when_callback_forwards() {
7611        let mut strategy = TestStrategy::new(StrategyConfig {
7612            strategy_id: Some(StrategyId::from("TEST-001")),
7613            ..Default::default()
7614        });
7615        register_strategy(&mut strategy);
7616
7617        let order = make_accepted_limit_order("O-PRESENT");
7618        add_order_to_cache(&strategy, &order);
7619        strategy.core.cache_rc().borrow_mut().build_index();
7620        strategy.core.is_exiting = true;
7621        let event = TimeEvent::new(
7622            strategy.core.market_exit_timer_name,
7623            UUID4::new(),
7624            UnixNanos::default(),
7625            UnixNanos::default(),
7626        );
7627
7628        route_time_event(&mut strategy, &event);
7629        Strategy::on_time_event(&mut strategy, &event).unwrap();
7630
7631        assert!(strategy.core.is_exiting);
7632        assert_eq!(strategy.core.market_exit_attempts, 1);
7633        assert_eq!(
7634            strategy.core.managed_time_event_last_id,
7635            Some(event.event_id)
7636        );
7637    }
7638
7639    #[rstest]
7640    fn test_route_time_event_deduplicates_overridden_managed_handlers() {
7641        let mut strategy = TimerOverrideStrategy {
7642            core: StrategyCore::new(StrategyConfig {
7643                strategy_id: Some(StrategyId::from("TEST-001")),
7644                ..Default::default()
7645            }),
7646            gtd_expiries: 0,
7647            market_exit_checks: 0,
7648        };
7649        let market_event = TimeEvent::new(
7650            strategy.core.market_exit_timer_name,
7651            UUID4::new(),
7652            UnixNanos::default(),
7653            UnixNanos::default(),
7654        );
7655
7656        route_time_event(&mut strategy, &market_event);
7657        DataActor::on_time_event(&mut strategy, &market_event).unwrap();
7658
7659        let client_order_id = ClientOrderId::from("O-001");
7660        let gtd_timer_name = Ustr::from("GTD-EXPIRY:O-001");
7661        strategy
7662            .core
7663            .gtd_timers
7664            .insert(client_order_id, gtd_timer_name);
7665        let gtd_event = TimeEvent::new(
7666            gtd_timer_name,
7667            UUID4::new(),
7668            UnixNanos::default(),
7669            UnixNanos::default(),
7670        );
7671
7672        route_time_event(&mut strategy, &gtd_event);
7673        DataActor::on_time_event(&mut strategy, &gtd_event).unwrap();
7674
7675        assert_eq!(strategy.market_exit_checks, 1);
7676        assert_eq!(strategy.gtd_expiries, 1);
7677        assert!(strategy.core.gtd_timers.contains_key(&client_order_id));
7678        assert_eq!(
7679            strategy.core.managed_time_event_last_id,
7680            Some(gtd_event.event_id)
7681        );
7682    }
7683
7684    #[rstest]
7685    fn test_route_time_event_ignores_unowned_market_exit_timer() {
7686        let mut strategy = TestStrategy::new(StrategyConfig {
7687            strategy_id: Some(StrategyId::from("TEST-001")),
7688            ..Default::default()
7689        });
7690        register_strategy(&mut strategy);
7691
7692        let order = make_accepted_limit_order("O-PRESENT");
7693        add_order_to_cache(&strategy, &order);
7694        strategy.core.cache_rc().borrow_mut().build_index();
7695        strategy.core.is_exiting = true;
7696        let event = TimeEvent::new(
7697            Ustr::from("MARKET_EXIT_CHECK:OTHER-001"),
7698            UUID4::new(),
7699            UnixNanos::default(),
7700            UnixNanos::default(),
7701        );
7702
7703        route_time_event(&mut strategy, &event);
7704
7705        assert!(strategy.core.is_exiting);
7706        assert_eq!(strategy.core.market_exit_attempts, 0);
7707        assert_eq!(strategy.core.managed_time_event_last_id, None);
7708    }
7709
7710    #[rstest]
7711    #[case(OrderStatus::Accepted)]
7712    #[case(OrderStatus::Submitted)]
7713    fn test_check_market_exit_waits_for_open_or_inflight_order(#[case] status: OrderStatus) {
7714        let mut strategy = create_test_strategy();
7715        register_strategy(&mut strategy);
7716        start_strategy(&mut strategy);
7717        let mut order = OrderTestBuilder::new(OrderType::Limit)
7718            .trader_id(TraderId::from("TRADER-001"))
7719            .strategy_id(StrategyId::from("TEST-001"))
7720            .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
7721            .client_order_id(ClientOrderId::from("O-EXIT-WORKING"))
7722            .side(OrderSide::Buy)
7723            .quantity(Quantity::from(3))
7724            .price(Price::from("50002.00"))
7725            .build();
7726        let account_id = AccountId::from("ACC-001");
7727        order
7728            .apply(TestOrderEventStubs::submitted(&order, account_id))
7729            .unwrap();
7730
7731        if status == OrderStatus::Accepted {
7732            order
7733                .apply(TestOrderEventStubs::accepted(
7734                    &order,
7735                    account_id,
7736                    VenueOrderId::from("V-EXIT-WORKING"),
7737                ))
7738                .unwrap();
7739        }
7740
7741        add_order_to_cache(&strategy, &order);
7742        strategy.core.cache_rc().borrow_mut().build_index();
7743        strategy.core.is_exiting = true;
7744
7745        let event = TimeEvent::new(
7746            strategy.core.market_exit_timer_name,
7747            UUID4::new(),
7748            0.into(),
7749            0.into(),
7750        );
7751
7752        strategy.check_market_exit(event);
7753
7754        assert!(strategy.is_exiting());
7755        assert_eq!(strategy.core.market_exit_attempts, 1);
7756        assert_eq!(strategy.state(), ComponentState::Running);
7757        assert_eq!(
7758            strategy
7759                .cache()
7760                .order(&order.client_order_id())
7761                .unwrap()
7762                .status(),
7763            status
7764        );
7765
7766        strategy
7767            .core
7768            .cache_rc()
7769            .borrow_mut()
7770            .update_order(&make_canceled(order.client_order_id()))
7771            .unwrap();
7772
7773        let event = TimeEvent::new(
7774            strategy.core.market_exit_timer_name,
7775            UUID4::new(),
7776            0.into(),
7777            0.into(),
7778        );
7779        strategy.check_market_exit(event);
7780
7781        assert!(!strategy.is_exiting());
7782        assert_eq!(strategy.core.market_exit_attempts, 0);
7783    }
7784
7785    #[rstest]
7786    #[case(false)]
7787    #[case(true)]
7788    fn test_check_market_exit_finalizes_when_max_attempts_reached(#[case] pending_stop: bool) {
7789        let config = StrategyConfig {
7790            strategy_id: Some(StrategyId::from("TEST-001")),
7791            order_id_tag: Some("001".to_string()),
7792            market_exit_max_attempts: 3,
7793            ..Default::default()
7794        };
7795        let mut strategy = TestStrategy::new(config);
7796        register_strategy(&mut strategy);
7797        start_strategy(&mut strategy);
7798
7799        let order = OrderTestBuilder::new(OrderType::Market)
7800            .trader_id(TraderId::from("TRADER-001"))
7801            .strategy_id(StrategyId::from("TEST-001"))
7802            .client_order_id(ClientOrderId::from("O-EXIT-MAX-LOCAL"))
7803            .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
7804            .side(OrderSide::Buy)
7805            .quantity(Quantity::from(3))
7806            .build();
7807        add_order_to_cache(&strategy, &order);
7808
7809        strategy.core.is_exiting = true;
7810        strategy.core.pending_stop = pending_stop;
7811        strategy.core.market_exit_attempts = 2; // One below max
7812
7813        let event = TimeEvent::new(
7814            Ustr::from("MARKET_EXIT_CHECK:TEST-001"),
7815            UUID4::new(),
7816            UnixNanos::default(),
7817            UnixNanos::default(),
7818        );
7819        strategy.check_market_exit(event);
7820
7821        // Should have finalized since attempts >= max_attempts
7822        assert!(!strategy.core.is_exiting);
7823        assert!(!strategy.core.pending_stop);
7824        assert_eq!(strategy.core.market_exit_attempts, 0);
7825        assert_eq!(
7826            strategy.state(),
7827            if pending_stop {
7828                ComponentState::Stopped
7829            } else {
7830                ComponentState::Running
7831            }
7832        );
7833        assert_eq!(
7834            strategy
7835                .cache()
7836                .order(&order.client_order_id())
7837                .unwrap()
7838                .status(),
7839            OrderStatus::Initialized
7840        );
7841    }
7842
7843    #[rstest]
7844    #[case(OrderStatus::Initialized, false)]
7845    #[case(OrderStatus::Emulated, false)]
7846    #[case(OrderStatus::Released, false)]
7847    #[case(OrderStatus::Initialized, true)]
7848    #[case(OrderStatus::Emulated, true)]
7849    #[case(OrderStatus::Released, true)]
7850    fn test_market_exit_cancels_active_local_orders_before_finalizing(
7851        #[case] status: OrderStatus,
7852        #[case] manage_stop: bool,
7853    ) {
7854        let mut strategy = TestStrategy::new(StrategyConfig {
7855            strategy_id: Some(StrategyId::from("TEST-001")),
7856            order_id_tag: Some("001".to_string()),
7857            manage_stop,
7858            ..Default::default()
7859        });
7860
7861        let clock = register_strategy_with_clock(&mut strategy);
7862        clock
7863            .borrow_mut()
7864            .register_default_handler(TimeEventCallback::from(|_event: TimeEvent| {}));
7865        start_strategy(&mut strategy);
7866        let (emulator_handler, emulator_messages): (
7867            _,
7868            TypedIntoMessageSavingHandler<TradingCommand>,
7869        ) = get_typed_into_message_saving_handler(Some(Ustr::from("OrderEmulator.execute")));
7870        msgbus::register_trading_command_endpoint(
7871            MessagingSwitchboard::order_emulator_execute(),
7872            emulator_handler,
7873        );
7874        let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
7875            get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
7876        msgbus::register_trading_command_endpoint(
7877            MessagingSwitchboard::exec_engine_queue_execute(),
7878            exec_handler,
7879        );
7880        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
7881            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
7882        msgbus::register_trading_command_endpoint(
7883            MessagingSwitchboard::risk_engine_queue_execute(),
7884            risk_handler,
7885        );
7886        let mut order = OrderTestBuilder::new(OrderType::StopMarket)
7887            .trader_id(TraderId::from("TRADER-001"))
7888            .strategy_id(StrategyId::from("TEST-001"))
7889            .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
7890            .client_order_id(ClientOrderId::from("O-EXIT-LOCAL"))
7891            .side(OrderSide::Buy)
7892            .trigger_price(Price::from("51000.00"))
7893            .quantity(Quantity::from(2))
7894            .emulation_trigger(TriggerType::BidAsk)
7895            .build();
7896
7897        match status {
7898            OrderStatus::Emulated => {
7899                let event = OrderEmulatedSpec::builder()
7900                    .trader_id(order.trader_id())
7901                    .strategy_id(order.strategy_id())
7902                    .instrument_id(order.instrument_id())
7903                    .client_order_id(order.client_order_id())
7904                    .build();
7905                order.apply(OrderEventAny::Emulated(event)).unwrap();
7906                add_order_to_cache(&strategy, &order);
7907            }
7908            OrderStatus::Released => {
7909                let event = OrderReleasedSpec::builder()
7910                    .trader_id(order.trader_id())
7911                    .strategy_id(order.strategy_id())
7912                    .instrument_id(order.instrument_id())
7913                    .client_order_id(order.client_order_id())
7914                    .released_price(Price::from("51001.00"))
7915                    .build();
7916                order.apply(OrderEventAny::Released(event)).unwrap();
7917                order.set_emulation_trigger(None);
7918                add_order_to_cache(&strategy, &order);
7919            }
7920            OrderStatus::Initialized => {
7921                order.set_emulation_trigger(None);
7922                strategy
7923                    .submit_order(order.clone(), None, None, None)
7924                    .unwrap();
7925            }
7926            _ => unreachable!(),
7927        }
7928
7929        assert_eq!(
7930            strategy
7931                .cache()
7932                .order(&order.client_order_id())
7933                .unwrap()
7934                .status(),
7935            status
7936        );
7937
7938        if manage_stop {
7939            assert!(!Strategy::stop(&mut strategy));
7940        } else {
7941            strategy.market_exit().unwrap();
7942        }
7943
7944        let event = TimeEvent::new(
7945            strategy.core.market_exit_timer_name,
7946            UUID4::new(),
7947            0.into(),
7948            0.into(),
7949        );
7950        strategy.check_market_exit(event);
7951
7952        let emulator_messages = emulator_messages.get_messages();
7953        let exec_messages = exec_messages.get_messages();
7954        assert_eq!(emulator_messages.len() + exec_messages.len(), 1);
7955        let command = emulator_messages
7956            .iter()
7957            .chain(&exec_messages)
7958            .next()
7959            .unwrap();
7960        assert!(matches!(command, TradingCommand::CancelOrder(cancel)
7961            if cancel.client_order_id == order.client_order_id()
7962                && cancel.instrument_id == order.instrument_id()
7963                && cancel.strategy_id == order.strategy_id()));
7964        assert_eq!(
7965            emulator_messages.len(),
7966            usize::from(status == OrderStatus::Emulated)
7967        );
7968        assert_eq!(
7969            risk_messages.get_messages().len(),
7970            usize::from(status == OrderStatus::Initialized)
7971        );
7972        assert!(strategy.is_exiting());
7973        assert_eq!(strategy.core.pending_stop, manage_stop);
7974        assert_eq!(strategy.state(), ComponentState::Running);
7975        assert_eq!(strategy.core.market_exit_attempts, 1);
7976
7977        strategy
7978            .core
7979            .cache_rc()
7980            .borrow_mut()
7981            .update_order(&make_canceled(order.client_order_id()))
7982            .unwrap();
7983
7984        let event = TimeEvent::new(
7985            strategy.core.market_exit_timer_name,
7986            UUID4::new(),
7987            0.into(),
7988            0.into(),
7989        );
7990        strategy.check_market_exit(event);
7991
7992        assert!(!strategy.is_exiting());
7993        assert!(!strategy.core.pending_stop);
7994        assert_eq!(strategy.core.market_exit_attempts, 0);
7995        assert_eq!(
7996            strategy.state(),
7997            if manage_stop {
7998                ComponentState::Stopped
7999            } else {
8000                ComponentState::Running
8001            }
8002        );
8003    }
8004
8005    #[rstest]
8006    #[case(false)]
8007    #[case(true)]
8008    fn test_market_exit_canceled_emulated_order_cannot_release_to_risk(#[case] manage_stop: bool) {
8009        let mut strategy = TestStrategy::new(StrategyConfig {
8010            strategy_id: Some(StrategyId::from("TEST-001")),
8011            order_id_tag: Some("001".to_string()),
8012            manage_stop,
8013            ..Default::default()
8014        });
8015
8016        let clock = register_strategy_with_clock(&mut strategy);
8017        clock
8018            .borrow_mut()
8019            .register_default_handler(TimeEventCallback::from(|_event: TimeEvent| {}));
8020        start_strategy(&mut strategy);
8021        let cache = strategy.core.cache_rc();
8022        let instrument = currency_pair_btcusdt();
8023        cache
8024            .borrow_mut()
8025            .add_instrument(InstrumentAny::CurrencyPair(instrument.clone()))
8026            .unwrap();
8027        let emulator = Rc::new(RefCell::new(OrderEmulator::new(clock, cache.clone())));
8028        OrderEmulator::register_msgbus_handlers(&emulator);
8029        let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
8030            get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
8031        msgbus::register_trading_command_endpoint(
8032            MessagingSwitchboard::risk_engine_queue_execute(),
8033            risk_handler,
8034        );
8035        let order_ids = [
8036            ClientOrderId::from("O-EXIT-EMULATED"),
8037            ClientOrderId::from("O-SIBLING-EMULATED"),
8038        ];
8039
8040        for (client_order_id, strategy_id) in order_ids.into_iter().zip([
8041            StrategyId::from("TEST-001"),
8042            StrategyId::from("SIBLING-001"),
8043        ]) {
8044            let order = OrderTestBuilder::new(OrderType::StopMarket)
8045                .trader_id(TraderId::from("TRADER-001"))
8046                .strategy_id(strategy_id)
8047                .instrument_id(instrument.id)
8048                .client_order_id(client_order_id)
8049                .side(OrderSide::Buy)
8050                .trigger_price(Price::from("51000.00"))
8051                .quantity(Quantity::from(2))
8052                .emulation_trigger(TriggerType::BidAsk)
8053                .build();
8054
8055            if strategy_id == StrategyId::from("TEST-001") {
8056                strategy.submit_order(order, None, None, None).unwrap();
8057            } else {
8058                add_order_to_cache(&strategy, &order);
8059                let command = make_submit_command(&order);
8060                emulator
8061                    .borrow_mut()
8062                    .execute(TradingCommand::SubmitOrder(command));
8063            }
8064        }
8065
8066        assert_eq!(
8067            cache.borrow().order(&order_ids[0]).unwrap().status(),
8068            OrderStatus::Emulated
8069        );
8070        assert_eq!(
8071            cache.borrow().order(&order_ids[1]).unwrap().status(),
8072            OrderStatus::Emulated
8073        );
8074        assert!(risk_messages.get_messages().is_empty());
8075
8076        if manage_stop {
8077            assert!(!Strategy::stop(&mut strategy));
8078        } else {
8079            strategy.market_exit().unwrap();
8080        }
8081
8082        assert!(strategy.is_exiting());
8083        assert_eq!(
8084            cache.borrow().order(&order_ids[0]).unwrap().status(),
8085            OrderStatus::Canceled
8086        );
8087        assert_eq!(
8088            cache.borrow().order(&order_ids[1]).unwrap().status(),
8089            OrderStatus::Emulated
8090        );
8091        assert!(
8092            !emulator
8093                .borrow()
8094                .get_submit_order_commands()
8095                .contains_key(&order_ids[0])
8096        );
8097
8098        let event = TimeEvent::new(
8099            strategy.core.market_exit_timer_name,
8100            UUID4::new(),
8101            0.into(),
8102            0.into(),
8103        );
8104        strategy.check_market_exit(event);
8105
8106        assert!(!strategy.is_exiting());
8107        assert_eq!(
8108            strategy.state(),
8109            if manage_stop {
8110                ComponentState::Stopped
8111            } else {
8112                ComponentState::Running
8113            }
8114        );
8115
8116        let quote = QuoteTick::new(
8117            instrument.id,
8118            Price::from("51001.00"),
8119            Price::from("51002.00"),
8120            Quantity::from(3),
8121            Quantity::from(4),
8122            1.into(),
8123            1.into(),
8124        );
8125        emulator.borrow_mut().on_quote_tick(quote);
8126
8127        assert_eq!(
8128            cache.borrow().order(&order_ids[0]).unwrap().status(),
8129            OrderStatus::Canceled
8130        );
8131        assert_eq!(
8132            cache.borrow().order(&order_ids[1]).unwrap().status(),
8133            OrderStatus::Released
8134        );
8135        let commands = risk_messages.get_messages();
8136        assert_eq!(commands.len(), 1);
8137        assert!(
8138            matches!(&commands[0], TradingCommand::SubmitOrder(command) if command.client_order_id == order_ids[1])
8139        );
8140    }
8141
8142    #[rstest]
8143    fn test_check_market_exit_finalizes_when_no_orders_or_positions() {
8144        let mut strategy = create_test_strategy();
8145        register_strategy(&mut strategy);
8146
8147        strategy.core.is_exiting = true;
8148
8149        let event = TimeEvent::new(
8150            Ustr::from("MARKET_EXIT_CHECK:TEST-001"),
8151            UUID4::new(),
8152            UnixNanos::default(),
8153            UnixNanos::default(),
8154        );
8155        strategy.check_market_exit(event);
8156
8157        // Should have finalized since there are no orders or positions
8158        assert!(!strategy.core.is_exiting);
8159    }
8160
8161    #[rstest]
8162    fn test_market_exit_timer_name_format() {
8163        let config = StrategyConfig {
8164            strategy_id: Some(StrategyId::from("MY-STRATEGY-001")),
8165            ..Default::default()
8166        };
8167        let strategy = TestStrategy::new(config);
8168
8169        assert_eq!(
8170            strategy.core.market_exit_timer_name,
8171            "MARKET_EXIT_CHECK:MY-STRATEGY-001"
8172        );
8173    }
8174
8175    #[rstest]
8176    fn test_reset_market_exit_state() {
8177        let mut strategy = create_test_strategy();
8178
8179        strategy.core.is_exiting = true;
8180        strategy.core.pending_stop = true;
8181        strategy.core.market_exit_attempts = 50;
8182
8183        strategy.core.reset_market_exit_state();
8184
8185        assert!(!strategy.core.is_exiting);
8186        assert!(!strategy.core.pending_stop);
8187        assert_eq!(strategy.core.market_exit_attempts, 0);
8188    }
8189
8190    #[rstest]
8191    fn test_cancel_market_exit_resets_state_without_hooks() {
8192        let config = StrategyConfig {
8193            strategy_id: Some(StrategyId::from("TEST-001")),
8194            order_id_tag: Some("001".to_string()),
8195            ..Default::default()
8196        };
8197        let mut strategy = MarketExitHookTrackingStrategy::new(config);
8198
8199        let trader_id = TraderId::from("TRADER-001");
8200        let clock = Rc::new(RefCell::new(VirtualClock::new()));
8201        let cache = Rc::new(RefCell::new(Cache::default()));
8202        let portfolio = Rc::new(RefCell::new(Portfolio::new(
8203            clock.clone(),
8204            cache.clone(),
8205            None,
8206        )));
8207        strategy
8208            .core
8209            .register(trader_id, clock, cache, portfolio)
8210            .unwrap();
8211
8212        // Set up exiting state
8213        strategy.core.is_exiting = true;
8214        strategy.core.pending_stop = true;
8215        strategy.core.market_exit_attempts = 50;
8216
8217        // Call cancel_market_exit
8218        strategy.cancel_market_exit();
8219
8220        // State should be reset
8221        assert!(!strategy.core.is_exiting);
8222        assert!(!strategy.core.pending_stop);
8223        assert_eq!(strategy.core.market_exit_attempts, 0);
8224
8225        // Hooks should NOT have been called
8226        assert!(!strategy.on_market_exit_called);
8227        assert!(!strategy.post_market_exit_called);
8228    }
8229
8230    #[rstest]
8231    fn test_market_exit_returns_early_when_not_running() {
8232        let mut strategy = create_test_strategy();
8233        register_strategy(&mut strategy);
8234
8235        // State is not Running (default is PreInitialized)
8236        assert!(!strategy.is_running());
8237
8238        let result = strategy.market_exit();
8239
8240        // Should return Ok but not set is_exiting
8241        assert!(result.is_ok());
8242        assert!(!strategy.core.is_exiting);
8243    }
8244
8245    #[rstest]
8246    fn test_stop_with_manage_stop_false_cleans_up_active_exit() {
8247        let config = StrategyConfig {
8248            strategy_id: Some(StrategyId::from("TEST-001")),
8249            order_id_tag: Some("001".to_string()),
8250            manage_stop: false,
8251            ..Default::default()
8252        };
8253        let mut strategy = TestStrategy::new(config);
8254        register_strategy(&mut strategy);
8255
8256        // Simulate an active market exit
8257        strategy.core.is_exiting = true;
8258        strategy.core.market_exit_attempts = 5;
8259
8260        // Call stop
8261        let should_proceed = Strategy::stop(&mut strategy);
8262
8263        // Should clean up state and allow stop to proceed
8264        assert!(should_proceed);
8265        assert!(!strategy.core.is_exiting);
8266        assert_eq!(strategy.core.market_exit_attempts, 0);
8267    }
8268
8269    #[rstest]
8270    fn test_stop_with_manage_stop_true_defers_when_running() {
8271        let config = StrategyConfig {
8272            strategy_id: Some(StrategyId::from("TEST-001")),
8273            order_id_tag: Some("001".to_string()),
8274            manage_stop: true,
8275            ..Default::default()
8276        };
8277        let mut strategy = TestStrategy::new(config);
8278
8279        // Custom setup with a default callback so timer scheduling succeeds
8280        let trader_id = TraderId::from("TRADER-001");
8281        let clock = Rc::new(RefCell::new(VirtualClock::new()));
8282        clock
8283            .borrow_mut()
8284            .register_default_handler(TimeEventCallback::from(|_event: TimeEvent| {}));
8285        let cache = Rc::new(RefCell::new(Cache::default()));
8286        let portfolio = Rc::new(RefCell::new(Portfolio::new(
8287            clock.clone(),
8288            cache.clone(),
8289            None,
8290        )));
8291        strategy
8292            .core
8293            .register(trader_id, clock, cache, portfolio)
8294            .unwrap();
8295        strategy.initialize().unwrap();
8296        strategy.start().unwrap();
8297
8298        let should_proceed = Strategy::stop(&mut strategy);
8299
8300        // Should set pending_stop and defer
8301        assert!(!should_proceed);
8302        assert!(strategy.core.pending_stop);
8303    }
8304
8305    #[rstest]
8306    fn test_stop_with_manage_stop_true_returns_early_if_pending() {
8307        let config = StrategyConfig {
8308            strategy_id: Some(StrategyId::from("TEST-001")),
8309            order_id_tag: Some("001".to_string()),
8310            manage_stop: true,
8311            ..Default::default()
8312        };
8313        let mut strategy = TestStrategy::new(config);
8314        register_strategy(&mut strategy);
8315        start_strategy(&mut strategy);
8316        strategy.core.pending_stop = true;
8317
8318        // Call stop again
8319        let should_proceed = Strategy::stop(&mut strategy);
8320
8321        // Should return early without changing state
8322        assert!(!should_proceed);
8323        assert!(strategy.core.pending_stop);
8324    }
8325
8326    #[rstest]
8327    fn test_stop_with_manage_stop_true_proceeds_when_not_running() {
8328        let config = StrategyConfig {
8329            strategy_id: Some(StrategyId::from("TEST-001")),
8330            order_id_tag: Some("001".to_string()),
8331            manage_stop: true,
8332            ..Default::default()
8333        };
8334        let mut strategy = TestStrategy::new(config);
8335        register_strategy(&mut strategy);
8336
8337        // State is not Running (default)
8338        assert!(!strategy.is_running());
8339
8340        let should_proceed = Strategy::stop(&mut strategy);
8341
8342        // Should proceed with stop
8343        assert!(should_proceed);
8344    }
8345
8346    #[rstest]
8347    fn test_finalize_market_exit_stops_strategy_when_pending() {
8348        let config = StrategyConfig {
8349            strategy_id: Some(StrategyId::from("TEST-001")),
8350            order_id_tag: Some("001".to_string()),
8351            ..Default::default()
8352        };
8353        let mut strategy = TestStrategy::new(config);
8354        register_strategy(&mut strategy);
8355        start_strategy(&mut strategy);
8356
8357        // Simulate a market exit with pending stop
8358        strategy.core.is_exiting = true;
8359        strategy.core.pending_stop = true;
8360
8361        strategy.finalize_market_exit();
8362
8363        // Should have transitioned to Stopped
8364        assert_eq!(strategy.state(), ComponentState::Stopped);
8365        assert!(!strategy.core.is_exiting);
8366        assert!(!strategy.core.pending_stop);
8367    }
8368
8369    #[rstest]
8370    fn test_finalize_market_exit_stays_running_when_not_pending() {
8371        let config = StrategyConfig {
8372            strategy_id: Some(StrategyId::from("TEST-001")),
8373            order_id_tag: Some("001".to_string()),
8374            ..Default::default()
8375        };
8376        let mut strategy = TestStrategy::new(config);
8377        register_strategy(&mut strategy);
8378        start_strategy(&mut strategy);
8379
8380        // Simulate a market exit without pending stop
8381        strategy.core.is_exiting = true;
8382        strategy.core.pending_stop = false;
8383
8384        strategy.finalize_market_exit();
8385
8386        // Should stay Running
8387        assert_eq!(strategy.state(), ComponentState::Running);
8388        assert!(!strategy.core.is_exiting);
8389    }
8390
8391    #[rstest]
8392    fn test_submit_order_denied_during_market_exit_when_not_reduce_only() {
8393        let mut strategy = create_test_strategy();
8394        register_strategy(&mut strategy);
8395        start_strategy(&mut strategy);
8396        strategy.core.is_exiting = true;
8397
8398        let (event_handler, event_messages): (_, TypedMessageSavingHandler<OrderEventAny>) =
8399            get_typed_message_saving_handler(Some(Ustr::from("events.order.denied")));
8400        let order = OrderAny::Market(MarketOrder::new(
8401            TraderId::from("TRADER-001"),
8402            StrategyId::from("TEST-001"),
8403            InstrumentId::from("BTCUSDT.BINANCE"),
8404            ClientOrderId::from("O-20250208-0001"),
8405            OrderSide::Buy,
8406            Quantity::from(100_000),
8407            TimeInForce::Gtc,
8408            UUID4::new(),
8409            UnixNanos::default(),
8410            false, // not reduce_only
8411            false,
8412            None,
8413            None,
8414            None,
8415            None,
8416            None,
8417            None,
8418            None,
8419            None,
8420        ));
8421        let topic = format!("events.order.{}", order.strategy_id());
8422        msgbus::subscribe_order_events(topic.clone().into(), event_handler.clone(), None);
8423        let client_order_id = order.client_order_id();
8424        let result = strategy.submit_order(order.clone(), None, None, None);
8425
8426        msgbus::unsubscribe_order_events(topic.into(), &event_handler);
8427
8428        assert!(result.is_ok());
8429        let cache = strategy.cache();
8430        let cached_order = cache.order(&client_order_id).unwrap();
8431        assert_eq!(cached_order.status(), OrderStatus::Denied);
8432
8433        let event_messages = event_messages.get_messages();
8434        assert_eq!(event_messages.len(), 2);
8435        assert_eq!(
8436            event_messages[0],
8437            OrderEventAny::Initialized(order.init_event().clone())
8438        );
8439        let OrderEventAny::Denied(denied) = &event_messages[1] else {
8440            panic!("expected OrderDenied event");
8441        };
8442        assert_eq!(denied.reason, Ustr::from("MARKET_EXIT_IN_PROGRESS"));
8443    }
8444
8445    #[rstest]
8446    fn test_submit_order_list_denied_during_market_exit_publishes_init_then_denied_events() {
8447        let mut strategy = create_test_strategy();
8448        register_strategy(&mut strategy);
8449        start_strategy(&mut strategy);
8450        strategy.core.is_exiting = true;
8451
8452        let orders = vec![
8453            make_initialized_market_order("O-20250208-LIST-DENY-001"),
8454            make_initialized_market_order("O-20250208-LIST-DENY-002"),
8455        ];
8456        let client_order_id1 = orders[0].client_order_id();
8457        let client_order_id2 = orders[1].client_order_id();
8458        let cache_rc = strategy.core.cache_rc();
8459        let timeline = Rc::new(RefCell::new(Vec::new()));
8460        let event_messages = Rc::new(RefCell::new(Vec::new()));
8461
8462        let event_handler = {
8463            let event_messages = event_messages.clone();
8464            let timeline = timeline.clone();
8465            TypedHandler::from_with_id("events.order.list_denied", move |event: &OrderEventAny| {
8466                match event {
8467                    OrderEventAny::Initialized(e) if e.client_order_id == client_order_id1 => {
8468                        assert!(cache_rc.borrow().order_exists(&client_order_id1));
8469                        timeline.borrow_mut().push("init1");
8470                    }
8471                    OrderEventAny::Initialized(e) if e.client_order_id == client_order_id2 => {
8472                        assert!(cache_rc.borrow().order_exists(&client_order_id2));
8473                        timeline.borrow_mut().push("init2");
8474                    }
8475                    OrderEventAny::Denied(e) if e.client_order_id == client_order_id1 => {
8476                        assert_eq!(e.reason, Ustr::from("MARKET_EXIT_IN_PROGRESS"));
8477                        let cache = cache_rc.borrow();
8478                        let cached_order = cache.order(&client_order_id1).unwrap();
8479                        assert_eq!(cached_order.status(), OrderStatus::Denied);
8480                        timeline.borrow_mut().push("denied1");
8481                    }
8482                    OrderEventAny::Denied(e) if e.client_order_id == client_order_id2 => {
8483                        assert_eq!(e.reason, Ustr::from("MARKET_EXIT_IN_PROGRESS"));
8484                        let cache = cache_rc.borrow();
8485                        let cached_order = cache.order(&client_order_id2).unwrap();
8486                        assert_eq!(cached_order.status(), OrderStatus::Denied);
8487                        timeline.borrow_mut().push("denied2");
8488                    }
8489                    _ => panic!("unexpected order event {event:?}"),
8490                }
8491                event_messages.borrow_mut().push(event.clone());
8492            })
8493        };
8494        let risk_handler = {
8495            let timeline = timeline.clone();
8496            TypedIntoHandler::from_with_id(
8497                "RiskEngine.queue_execute",
8498                move |_command: TradingCommand| {
8499                    timeline.borrow_mut().push("command");
8500                },
8501            )
8502        };
8503        msgbus::register_trading_command_endpoint(
8504            MessagingSwitchboard::risk_engine_queue_execute(),
8505            risk_handler,
8506        );
8507
8508        let topic = format!("events.order.{}", orders[0].strategy_id());
8509        msgbus::subscribe_order_events(topic.clone().into(), event_handler.clone(), None);
8510        let result = strategy.submit_order_list(orders.clone(), None, None, None);
8511
8512        msgbus::unsubscribe_order_events(topic.into(), &event_handler);
8513
8514        assert!(result.is_ok());
8515
8516        let cache = strategy.cache();
8517        let cached_order1 = cache.order(&client_order_id1).unwrap();
8518        let cached_order2 = cache.order(&client_order_id2).unwrap();
8519        assert_eq!(cached_order1.status(), OrderStatus::Denied);
8520        assert_eq!(cached_order2.status(), OrderStatus::Denied);
8521
8522        let event_messages = event_messages.borrow();
8523        assert_eq!(event_messages.len(), 4);
8524        assert_eq!(
8525            event_messages[0],
8526            OrderEventAny::Initialized(orders[0].init_event().clone())
8527        );
8528        assert!(matches!(
8529            &event_messages[1],
8530            OrderEventAny::Denied(e)
8531                if e.client_order_id == client_order_id1
8532                    && e.reason == Ustr::from("MARKET_EXIT_IN_PROGRESS")
8533        ));
8534        assert_eq!(
8535            event_messages[2],
8536            OrderEventAny::Initialized(orders[1].init_event().clone())
8537        );
8538        assert!(matches!(
8539            &event_messages[3],
8540            OrderEventAny::Denied(e)
8541                if e.client_order_id == client_order_id2
8542                    && e.reason == Ustr::from("MARKET_EXIT_IN_PROGRESS")
8543        ));
8544        assert_eq!(
8545            timeline.borrow().as_slice(),
8546            &["init1", "denied1", "init2", "denied2"]
8547        );
8548    }
8549
8550    #[rstest]
8551    fn test_submit_order_list_market_exit_rejects_non_initialized_without_events() {
8552        let mut strategy = create_test_strategy();
8553        register_strategy(&mut strategy);
8554        start_strategy(&mut strategy);
8555        strategy.core.is_exiting = true;
8556
8557        let order = make_accepted_market_order("O-20250208-LIST-DENY-ACCEPTED");
8558        let topic = format!("events.order.{}", order.strategy_id());
8559        let (event_handler, event_messages): (_, TypedMessageSavingHandler<OrderEventAny>) =
8560            get_typed_message_saving_handler(Some(Ustr::from("events.order.list_invalid")));
8561
8562        msgbus::subscribe_order_events(topic.clone().into(), event_handler.clone(), None);
8563        let result = strategy.submit_order_list(vec![order], None, None, None);
8564
8565        msgbus::unsubscribe_order_events(topic.into(), &event_handler);
8566
8567        assert!(result.is_err());
8568        assert!(
8569            result
8570                .unwrap_err()
8571                .to_string()
8572                .contains("expected INITIALIZED")
8573        );
8574        assert!(event_messages.get_messages().is_empty());
8575    }
8576
8577    #[rstest]
8578    fn test_submit_order_list_rejects_mixed_venues_with_friendly_error() {
8579        let mut strategy = create_test_strategy();
8580        register_strategy(&mut strategy);
8581        start_strategy(&mut strategy);
8582
8583        let binance_order = make_initialized_market_order("O-MIXED-VENUE-001");
8584        let bybit_order = OrderAny::Market(MarketOrder::new(
8585            TraderId::from("TRADER-001"),
8586            StrategyId::from("TEST-001"),
8587            InstrumentId::from("BTCUSDT.BYBIT"),
8588            ClientOrderId::from("O-MIXED-VENUE-002"),
8589            OrderSide::Buy,
8590            Quantity::from(100_000),
8591            TimeInForce::Gtc,
8592            UUID4::new(),
8593            UnixNanos::default(),
8594            false,
8595            false,
8596            None,
8597            None,
8598            None,
8599            None,
8600            None,
8601            None,
8602            None,
8603            None,
8604        ));
8605
8606        let result = strategy.submit_order_list(vec![binance_order, bybit_order], None, None, None);
8607
8608        let err = result.unwrap_err();
8609        let msg = err.to_string();
8610        assert!(
8611            msg.contains("OrderList denied: orders must share the same venue"),
8612            "unexpected error: {msg}",
8613        );
8614        assert!(msg.contains("BINANCE"), "expected BINANCE in error: {msg}");
8615        assert!(msg.contains("BYBIT"), "expected BYBIT in error: {msg}");
8616    }
8617
8618    #[rstest]
8619    fn test_submit_order_allowed_during_market_exit_when_reduce_only() {
8620        let mut strategy = create_test_strategy();
8621        register_strategy(&mut strategy);
8622        start_strategy(&mut strategy);
8623        strategy.core.is_exiting = true;
8624
8625        let order = OrderAny::Market(MarketOrder::new(
8626            TraderId::from("TRADER-001"),
8627            StrategyId::from("TEST-001"),
8628            InstrumentId::from("BTCUSDT.BINANCE"),
8629            ClientOrderId::from("O-20250208-0001"),
8630            OrderSide::Buy,
8631            Quantity::from(100_000),
8632            TimeInForce::Gtc,
8633            UUID4::new(),
8634            UnixNanos::default(),
8635            true, // reduce_only
8636            false,
8637            None,
8638            None,
8639            None,
8640            None,
8641            None,
8642            None,
8643            None,
8644            None,
8645        ));
8646        let client_order_id = order.client_order_id();
8647        let result = strategy.submit_order(order, None, None, None);
8648
8649        assert!(result.is_ok());
8650        let cache = strategy.cache();
8651        let cached_order = cache.order(&client_order_id).unwrap();
8652        assert_ne!(cached_order.status(), OrderStatus::Denied);
8653    }
8654
8655    #[rstest]
8656    fn test_submit_order_allowed_during_market_exit_when_tagged() {
8657        let mut strategy = create_test_strategy();
8658        register_strategy(&mut strategy);
8659        start_strategy(&mut strategy);
8660        strategy.core.is_exiting = true;
8661
8662        let order = OrderAny::Market(MarketOrder::new(
8663            TraderId::from("TRADER-001"),
8664            StrategyId::from("TEST-001"),
8665            InstrumentId::from("BTCUSDT.BINANCE"),
8666            ClientOrderId::from("O-20250208-0002"),
8667            OrderSide::Buy,
8668            Quantity::from(100_000),
8669            TimeInForce::Gtc,
8670            UUID4::new(),
8671            UnixNanos::default(),
8672            false, // not reduce_only
8673            false,
8674            None,
8675            None,
8676            None,
8677            None,
8678            None,
8679            None,
8680            None,
8681            Some(vec![Ustr::from("MARKET_EXIT")]),
8682        ));
8683        let client_order_id = order.client_order_id();
8684        let result = strategy.submit_order(order, None, None, None);
8685
8686        assert!(result.is_ok());
8687        let cache = strategy.cache();
8688        let cached_order = cache.order(&client_order_id).unwrap();
8689        assert_ne!(cached_order.status(), OrderStatus::Denied);
8690    }
8691
8692    #[derive(Debug)]
8693    struct MacroTestSimple {
8694        core: StrategyCore,
8695    }
8696
8697    nautilus_strategy!(MacroTestSimple);
8698
8699    impl DataActor for MacroTestSimple {}
8700
8701    #[derive(Debug)]
8702    struct MacroTestWithHooks {
8703        core: StrategyCore,
8704        start_trace: Vec<&'static str>,
8705        start_fails: bool,
8706    }
8707
8708    nautilus_strategy!(MacroTestWithHooks, {
8709        fn on_start(&mut self) -> anyhow::Result<()> {
8710            self.start_trace.push("strategy");
8711            anyhow::ensure!(!self.start_fails, "Strategy start failed");
8712            Ok(())
8713        }
8714
8715        fn on_order_rejected(&mut self, _event: OrderRejected) {}
8716    });
8717
8718    impl DataActor for MacroTestWithHooks {
8719        fn on_start(&mut self) -> anyhow::Result<()> {
8720            self.start_trace.push("actor");
8721            Ok(())
8722        }
8723    }
8724
8725    #[rstest]
8726    #[case::success(false)]
8727    #[case::failure(true)]
8728    fn test_component_start_dispatches_strategy_before_actor(#[case] start_fails: bool) {
8729        let mut strategy = MacroTestWithHooks {
8730            core: create_test_strategy().core,
8731            start_trace: Vec::new(),
8732            start_fails,
8733        };
8734
8735        strategy.initialize().unwrap();
8736
8737        let result = Component::start(&mut strategy);
8738
8739        if start_fails {
8740            assert_eq!(result.unwrap_err().to_string(), "Strategy start failed");
8741            assert_eq!(strategy.state(), ComponentState::Starting);
8742            assert_eq!(strategy.start_trace, vec!["strategy"]);
8743        } else {
8744            result.unwrap();
8745            assert_eq!(strategy.state(), ComponentState::Running);
8746            assert_eq!(strategy.start_trace, vec!["strategy", "actor"]);
8747        }
8748    }
8749
8750    #[derive(Debug)]
8751    struct MacroTestCustomField {
8752        inner: StrategyCore,
8753    }
8754
8755    nautilus_strategy!(MacroTestCustomField, inner, {
8756        fn external_order_instrument_ids(&self) -> Option<Vec<InstrumentId>> {
8757            None
8758        }
8759    });
8760
8761    impl DataActor for MacroTestCustomField {}
8762
8763    #[rstest]
8764    fn test_strategy_behavior_does_not_require_native_core_access() {
8765        fn assert_strategy<T: Strategy + DataActor>() {}
8766
8767        assert_strategy::<CoreFreeStrategy>();
8768
8769        let mut strategy = CoreFreeStrategy { started: false };
8770        DataActor::on_start(&mut strategy).unwrap();
8771
8772        assert!(strategy.started);
8773    }
8774
8775    #[rstest]
8776    fn test_nautilus_strategy_macro_forms() {
8777        let config = StrategyConfig {
8778            strategy_id: Some(StrategyId::from("MACRO-001")),
8779            order_id_tag: Some("001".to_string()),
8780            ..Default::default()
8781        };
8782
8783        let simple = MacroTestSimple {
8784            core: StrategyCore::new(config.clone()),
8785        };
8786        assert_eq!(simple.strategy_id(), config.strategy_id);
8787        assert_eq!(simple.config().order_id_tag, config.order_id_tag);
8788        assert_eq!(simple.actor_id(), ActorId::from("MACRO-001"));
8789
8790        let hooks = MacroTestWithHooks {
8791            core: StrategyCore::new(config.clone()),
8792            start_trace: Vec::new(),
8793            start_fails: false,
8794        };
8795        assert_eq!(hooks.strategy_id(), config.strategy_id);
8796        assert_eq!(hooks.config().order_id_tag, config.order_id_tag);
8797        assert_eq!(hooks.actor_id(), ActorId::from("MACRO-001"));
8798
8799        let custom = MacroTestCustomField {
8800            inner: StrategyCore::new(config.clone()),
8801        };
8802        assert_eq!(custom.strategy_id(), config.strategy_id);
8803        assert_eq!(custom.config().order_id_tag, config.order_id_tag);
8804        assert_eq!(custom.actor_id(), ActorId::from("MACRO-001"));
8805        assert!(custom.external_order_instrument_ids().is_none());
8806    }
8807
8808    #[rstest]
8809    fn test_cancel_all_orders_returns_all_errors_without_logging() {
8810        let _guard = init_logging(
8811            TraderId::from("TRADER-001"),
8812            UUID4::new(),
8813            Default::default(),
8814            Default::default(),
8815        )
8816        .unwrap();
8817        let mut strategy = create_test_strategy();
8818        register_strategy(&mut strategy);
8819        let (handler, messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
8820            get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
8821        msgbus::register_trading_command_endpoint(
8822            MessagingSwitchboard::exec_engine_queue_execute(),
8823            handler,
8824        );
8825        let mut first = make_accepted_market_order("O-CANCEL-001");
8826        let mut second = make_accepted_market_order("O-CANCEL-002");
8827        for order in [&mut first, &mut second] {
8828            let OrderAny::Market(order) = order else {
8829                unreachable!()
8830            };
8831
8832            order.account_id = None;
8833        }
8834
8835        let third = make_accepted_market_order("O-CANCEL-003");
8836        for order in [&first, &second, &third] {
8837            add_order_to_cache(&strategy, order);
8838        }
8839
8840        strategy.core.cache_rc().borrow_mut().build_index();
8841        arm_shutdown_on_error(true);
8842        let result = strategy.cancel_all_orders(first.instrument_id(), None, None, true, None);
8843        let trigger = take_shutdown_on_error_trigger();
8844        disarm_shutdown_on_error();
8845
8846        assert_eq!(
8847            result.unwrap_err().to_string(),
8848            "Cannot generate pending cancel event for O-CANCEL-001: account_id is not set; Cannot generate pending cancel event for O-CANCEL-002: account_id is not set"
8849        );
8850        assert_eq!(trigger, None);
8851        let messages = messages.get_messages();
8852        assert_eq!(messages.len(), 1);
8853        assert!(
8854            matches!(&messages[0], TradingCommand::CancelOrder(command) if command.client_order_id == third.client_order_id())
8855        );
8856        let cache = strategy.cache();
8857        assert_eq!(
8858            cache.order(&first.client_order_id()).unwrap().status(),
8859            OrderStatus::Accepted
8860        );
8861        assert_eq!(
8862            cache.order(&second.client_order_id()).unwrap().status(),
8863            OrderStatus::Accepted
8864        );
8865        assert_eq!(
8866            cache.order(&third.client_order_id()).unwrap().status(),
8867            OrderStatus::PendingCancel
8868        );
8869    }
8870}