nautilus_trading/strategy/
mod.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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 config;
17pub mod core;
18
19pub use core::StrategyCore;
20
21pub use config::StrategyConfig;
22use indexmap::IndexMap;
23use nautilus_common::{
24    actor::DataActor,
25    logging::{EVT, RECV},
26    messages::execution::{
27        BatchCancelOrders, CancelAllOrders, CancelOrder, ModifyOrder, QueryAccount, QueryOrder,
28        SubmitOrder, SubmitOrderList, TradingCommand,
29    },
30    msgbus,
31    timer::TimeEvent,
32};
33use nautilus_core::UUID4;
34use nautilus_model::{
35    enums::{OrderSide, OrderStatus, PositionSide, TimeInForce, TriggerType},
36    events::{
37        OrderAccepted, OrderCancelRejected, OrderDenied, OrderEmulated, OrderEventAny,
38        OrderExpired, OrderInitialized, OrderModifyRejected, OrderPendingCancel,
39        OrderPendingUpdate, OrderRejected, OrderReleased, OrderSubmitted, OrderTriggered,
40        OrderUpdated, PositionChanged, PositionClosed, PositionEvent, PositionOpened,
41    },
42    identifiers::{AccountId, ClientId, ClientOrderId, InstrumentId, PositionId, StrategyId},
43    orders::{Order, OrderAny, OrderCore, OrderList},
44    position::Position,
45    types::{Price, Quantity},
46};
47use ustr::Ustr;
48
49/// Core trait for implementing trading strategies in NautilusTrader.
50///
51/// Strategies are specialized [`DataActor`]s that combine data ingestion capabilities with
52/// comprehensive order and position management functionality. By implementing this trait,
53/// custom strategies gain access to the full trading execution stack including order
54/// submission, modification, cancellation, and position management.
55///
56/// # Key Capabilities
57///
58/// - All [`DataActor`] capabilities (data subscriptions, event handling, timers).
59/// - Order lifecycle management (submit, modify, cancel).
60/// - Position management (open, close, monitor).
61/// - Access to the trading cache and portfolio.
62/// - Event routing to order manager and emulator.
63///
64/// # Implementation
65///
66/// User strategies should implement the [`Strategy::core_mut`] method to provide
67/// access to their internal [`StrategyCore`], which handles the integration with
68/// the trading engine. All order and position management methods are provided
69/// as default implementations.
70pub trait Strategy: DataActor {
71    /// Provides mutable access to the internal `StrategyCore`.
72    ///
73    /// This method must be implemented by the user's strategy struct, typically
74    /// by returning a mutable reference to its `StrategyCore` member.
75    fn core_mut(&mut self) -> &mut StrategyCore;
76
77    /// Submits an order.
78    ///
79    /// # Errors
80    ///
81    /// Returns an error if the strategy is not registered or order submission fails.
82    fn submit_order(
83        &mut self,
84        order: OrderAny,
85        position_id: Option<PositionId>,
86        client_id: Option<ClientId>,
87    ) -> anyhow::Result<()> {
88        self.submit_order_with_params(order, position_id, client_id, IndexMap::new())
89    }
90
91    /// Submits an order with adapter-specific parameters.
92    ///
93    /// # Errors
94    ///
95    /// Returns an error if the strategy is not registered or order submission fails.
96    fn submit_order_with_params(
97        &mut self,
98        order: OrderAny,
99        position_id: Option<PositionId>,
100        client_id: Option<ClientId>,
101        params: IndexMap<String, String>,
102    ) -> anyhow::Result<()> {
103        let core = self.core_mut();
104
105        let trader_id = core.trader_id().expect("Trader ID not set");
106        let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
107        let ts_init = core.clock().timestamp_ns();
108
109        let params = if params.is_empty() {
110            None
111        } else {
112            Some(params)
113        };
114
115        let command = SubmitOrder::new(
116            trader_id,
117            client_id.unwrap_or_default(),
118            strategy_id,
119            order.instrument_id(),
120            order.client_order_id(),
121            order.venue_order_id().unwrap_or_default(),
122            order.clone(),
123            order.exec_algorithm_id(),
124            position_id,
125            params,
126            UUID4::new(),
127            ts_init,
128        )?;
129
130        let Some(manager) = &mut core.order_manager else {
131            anyhow::bail!("Strategy not registered: OrderManager missing");
132        };
133
134        if matches!(order.emulation_trigger(), Some(trigger) if trigger != TriggerType::NoTrigger) {
135            manager.send_emulator_command(TradingCommand::SubmitOrder(command));
136        } else if order.exec_algorithm_id().is_some() {
137            manager.send_algo_command(command, order.exec_algorithm_id().unwrap());
138        } else {
139            manager.send_risk_command(TradingCommand::SubmitOrder(command));
140        }
141
142        self.set_gtd_expiry(&order)?;
143        Ok(())
144    }
145
146    /// Submits an order list.
147    ///
148    /// # Errors
149    ///
150    /// Returns an error if the strategy is not registered, the order list is invalid,
151    /// or order list submission fails.
152    fn submit_order_list(
153        &mut self,
154        order_list: OrderList,
155        position_id: Option<PositionId>,
156        client_id: Option<ClientId>,
157    ) -> anyhow::Result<()> {
158        let core = self.core_mut();
159
160        let trader_id = core.trader_id().expect("Trader ID not set");
161        let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
162        let ts_init = core.clock().timestamp_ns();
163        {
164            let cache_rc = core.cache();
165            if cache_rc.order_list_exists(&order_list.id) {
166                anyhow::bail!("OrderList denied: duplicate {}", order_list.id);
167            }
168
169            for order in &order_list.orders {
170                if order.status() != OrderStatus::Initialized {
171                    anyhow::bail!(
172                        "Order in list denied: invalid status for {}, expected INITIALIZED",
173                        order.client_order_id()
174                    );
175                }
176                if cache_rc.order_exists(&order.client_order_id()) {
177                    anyhow::bail!(
178                        "Order in list denied: duplicate {}",
179                        order.client_order_id()
180                    );
181                }
182            }
183        }
184
185        let command = SubmitOrderList::new(
186            trader_id,
187            client_id.unwrap_or_default(),
188            strategy_id,
189            order_list.instrument_id,
190            order_list
191                .orders
192                .first()
193                .map(|o| o.client_order_id())
194                .unwrap_or_default(),
195            order_list
196                .orders
197                .first()
198                .map(|o| o.venue_order_id().unwrap_or_default())
199                .unwrap_or_default(),
200            order_list.clone(),
201            None,
202            position_id,
203            UUID4::new(),
204            ts_init,
205        )?;
206
207        let has_emulated_order = order_list.orders.iter().any(|o| {
208            matches!(o.emulation_trigger(), Some(trigger) if trigger != TriggerType::NoTrigger)
209                || o.is_emulated()
210        });
211
212        let first_order = order_list.orders.first();
213        let exec_algorithm_id = first_order.and_then(|o| o.exec_algorithm_id());
214
215        let Some(manager) = &mut core.order_manager else {
216            anyhow::bail!("Strategy not registered: OrderManager missing");
217        };
218
219        if has_emulated_order {
220            manager.send_emulator_command(TradingCommand::SubmitOrderList(command));
221        } else if let Some(algo_id) = exec_algorithm_id {
222            let endpoint = format!("{algo_id}.execute");
223            msgbus::send_any(endpoint.into(), &TradingCommand::SubmitOrderList(command));
224        } else {
225            manager.send_risk_command(TradingCommand::SubmitOrderList(command));
226        }
227
228        for order in &order_list.orders {
229            self.set_gtd_expiry(order)?;
230        }
231
232        Ok(())
233    }
234
235    /// Modifies an order.
236    ///
237    /// # Errors
238    ///
239    /// Returns an error if the strategy is not registered or order modification fails.
240    fn modify_order(
241        &mut self,
242        order: OrderAny,
243        quantity: Option<Quantity>,
244        price: Option<Price>,
245        trigger_price: Option<Price>,
246        client_id: Option<ClientId>,
247    ) -> anyhow::Result<()> {
248        self.modify_order_with_params(
249            order,
250            quantity,
251            price,
252            trigger_price,
253            client_id,
254            IndexMap::new(),
255        )
256    }
257
258    /// Modifies an order with adapter-specific parameters.
259    ///
260    /// # Errors
261    ///
262    /// Returns an error if the strategy is not registered or order modification fails.
263    fn modify_order_with_params(
264        &mut self,
265        order: OrderAny,
266        quantity: Option<Quantity>,
267        price: Option<Price>,
268        trigger_price: Option<Price>,
269        client_id: Option<ClientId>,
270        params: IndexMap<String, String>,
271    ) -> anyhow::Result<()> {
272        let core = self.core_mut();
273
274        let trader_id = core.trader_id().expect("Trader ID not set");
275        let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
276        let ts_init = core.clock().timestamp_ns();
277
278        let params = if params.is_empty() {
279            None
280        } else {
281            Some(params)
282        };
283
284        let command = ModifyOrder::new(
285            trader_id,
286            client_id.unwrap_or_default(),
287            strategy_id,
288            order.instrument_id(),
289            order.client_order_id(),
290            order.venue_order_id().unwrap_or_default(),
291            quantity,
292            price,
293            trigger_price,
294            UUID4::new(),
295            ts_init,
296            params,
297        )?;
298
299        let Some(manager) = &mut core.order_manager else {
300            anyhow::bail!("Strategy not registered: OrderManager missing");
301        };
302
303        if matches!(order.emulation_trigger(), Some(trigger) if trigger != TriggerType::NoTrigger) {
304            manager.send_emulator_command(TradingCommand::ModifyOrder(command));
305        } else if order.exec_algorithm_id().is_some() {
306            manager.send_risk_command(TradingCommand::ModifyOrder(command));
307        } else {
308            manager.send_exec_command(TradingCommand::ModifyOrder(command));
309        }
310        Ok(())
311    }
312
313    /// Cancels an order.
314    ///
315    /// # Errors
316    ///
317    /// Returns an error if the strategy is not registered or order cancellation fails.
318    fn cancel_order(&mut self, order: OrderAny, client_id: Option<ClientId>) -> anyhow::Result<()> {
319        self.cancel_order_with_params(order, client_id, IndexMap::new())
320    }
321
322    /// Cancels an order with adapter-specific parameters.
323    ///
324    /// # Errors
325    ///
326    /// Returns an error if the strategy is not registered or order cancellation fails.
327    fn cancel_order_with_params(
328        &mut self,
329        order: OrderAny,
330        client_id: Option<ClientId>,
331        params: IndexMap<String, String>,
332    ) -> anyhow::Result<()> {
333        let core = self.core_mut();
334
335        let trader_id = core.trader_id().expect("Trader ID not set");
336        let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
337        let ts_init = core.clock().timestamp_ns();
338
339        let params = if params.is_empty() {
340            None
341        } else {
342            Some(params)
343        };
344
345        let command = CancelOrder::new(
346            trader_id,
347            client_id.unwrap_or_default(),
348            strategy_id,
349            order.instrument_id(),
350            order.client_order_id(),
351            order.venue_order_id().unwrap_or_default(),
352            UUID4::new(),
353            ts_init,
354            params,
355        )?;
356
357        let Some(manager) = &mut core.order_manager else {
358            anyhow::bail!("Strategy not registered: OrderManager missing");
359        };
360
361        if matches!(order.emulation_trigger(), Some(trigger) if trigger != TriggerType::NoTrigger)
362            || order.is_emulated()
363        {
364            manager.send_emulator_command(TradingCommand::CancelOrder(command));
365        } else if let Some(algo_id) = order.exec_algorithm_id() {
366            let endpoint = format!("{algo_id}.execute");
367            msgbus::send_any(endpoint.into(), &TradingCommand::CancelOrder(command));
368        } else {
369            manager.send_exec_command(TradingCommand::CancelOrder(command));
370        }
371        Ok(())
372    }
373
374    /// Batch cancels multiple orders for the same instrument.
375    ///
376    /// # Errors
377    ///
378    /// Returns an error if the strategy is not registered, the orders span multiple instruments,
379    /// or contain emulated/local orders.
380    fn cancel_orders(
381        &mut self,
382        mut orders: Vec<OrderAny>,
383        client_id: Option<ClientId>,
384        params: Option<IndexMap<String, String>>,
385    ) -> anyhow::Result<()> {
386        if orders.is_empty() {
387            anyhow::bail!("Cannot batch cancel empty order list");
388        }
389
390        let core = self.core_mut();
391        let trader_id = core.trader_id().expect("Trader ID not set");
392        let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
393        let ts_init = core.clock().timestamp_ns();
394
395        let Some(manager) = &mut core.order_manager else {
396            anyhow::bail!("Strategy not registered: OrderManager missing");
397        };
398
399        let first = orders.remove(0);
400        let instrument_id = first.instrument_id();
401
402        if first.is_emulated() || first.is_active_local() {
403            anyhow::bail!("Cannot include emulated or local orders in batch cancel");
404        }
405
406        let mut cancels = Vec::with_capacity(orders.len() + 1);
407        cancels.push(CancelOrder::new(
408            trader_id,
409            client_id.unwrap_or_default(),
410            strategy_id,
411            instrument_id,
412            first.client_order_id(),
413            first.venue_order_id().unwrap_or_default(),
414            UUID4::new(),
415            ts_init,
416            params.clone(),
417        )?);
418
419        for order in orders {
420            if order.instrument_id() != instrument_id {
421                anyhow::bail!(
422                    "Cannot batch cancel orders for different instruments: {} vs {}",
423                    instrument_id,
424                    order.instrument_id()
425                );
426            }
427
428            if order.is_emulated() || order.is_active_local() {
429                anyhow::bail!("Cannot include emulated or local orders in batch cancel");
430            }
431
432            cancels.push(CancelOrder::new(
433                trader_id,
434                client_id.unwrap_or_default(),
435                strategy_id,
436                instrument_id,
437                order.client_order_id(),
438                order.venue_order_id().unwrap_or_default(),
439                UUID4::new(),
440                ts_init,
441                params.clone(),
442            )?);
443        }
444
445        let command = BatchCancelOrders::new(
446            trader_id,
447            client_id.unwrap_or_default(),
448            strategy_id,
449            instrument_id,
450            cancels,
451            UUID4::new(),
452            ts_init,
453            params,
454        )?;
455
456        manager.send_exec_command(TradingCommand::BatchCancelOrders(command));
457        Ok(())
458    }
459
460    /// Cancels all open orders for the given instrument.
461    ///
462    /// # Errors
463    ///
464    /// Returns an error if the strategy is not registered or order cancellation fails.
465    fn cancel_all_orders(
466        &mut self,
467        instrument_id: InstrumentId,
468        order_side: Option<OrderSide>,
469        client_id: Option<ClientId>,
470    ) -> anyhow::Result<()> {
471        self.cancel_all_orders_with_params(instrument_id, order_side, client_id, IndexMap::new())
472    }
473
474    /// Cancels all open orders for the given instrument with adapter-specific parameters.
475    ///
476    /// # Errors
477    ///
478    /// Returns an error if the strategy is not registered or order cancellation fails.
479    fn cancel_all_orders_with_params(
480        &mut self,
481        instrument_id: InstrumentId,
482        order_side: Option<OrderSide>,
483        client_id: Option<ClientId>,
484        params: IndexMap<String, String>,
485    ) -> anyhow::Result<()> {
486        let params = if params.is_empty() {
487            None
488        } else {
489            Some(params)
490        };
491        let core = self.core_mut();
492
493        let trader_id = core.trader_id().expect("Trader ID not set");
494        let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
495        let ts_init = core.clock().timestamp_ns();
496        let cache = core.cache();
497
498        let open_orders =
499            cache.orders_open(None, Some(&instrument_id), Some(&strategy_id), order_side);
500
501        let emulated_orders =
502            cache.orders_emulated(None, Some(&instrument_id), Some(&strategy_id), order_side);
503
504        let exec_algorithm_ids = cache.exec_algorithm_ids();
505        let mut algo_orders = Vec::new();
506
507        for algo_id in &exec_algorithm_ids {
508            let orders = cache.orders_for_exec_algorithm(
509                algo_id,
510                None,
511                Some(&instrument_id),
512                Some(&strategy_id),
513                order_side,
514            );
515            algo_orders.extend(orders.iter().map(|o| (*o).clone()));
516        }
517
518        let open_count = open_orders.len();
519        let emulated_count = emulated_orders.len();
520        let algo_count = algo_orders.len();
521
522        drop(cache);
523
524        if open_count == 0 && emulated_count == 0 && algo_count == 0 {
525            let side_str = order_side.map(|s| format!(" {s}")).unwrap_or_default();
526            log::info!("No {instrument_id} open or emulated{side_str} orders to cancel");
527            return Ok(());
528        }
529
530        let Some(manager) = &mut core.order_manager else {
531            anyhow::bail!("Strategy not registered: OrderManager missing");
532        };
533
534        let side_str = order_side.map(|s| format!(" {s}")).unwrap_or_default();
535
536        if open_count > 0 {
537            log::info!(
538                "Canceling {open_count} open{side_str} {instrument_id} order{}",
539                if open_count == 1 { "" } else { "s" }
540            );
541
542            let command = CancelAllOrders::new(
543                trader_id,
544                client_id.unwrap_or_default(),
545                strategy_id,
546                instrument_id,
547                order_side.unwrap_or(OrderSide::NoOrderSide),
548                UUID4::new(),
549                ts_init,
550                params.clone(),
551            )?;
552
553            manager.send_exec_command(TradingCommand::CancelAllOrders(command));
554        }
555
556        if emulated_count > 0 {
557            log::info!(
558                "Canceling {emulated_count} emulated{side_str} {instrument_id} order{}",
559                if emulated_count == 1 { "" } else { "s" }
560            );
561
562            let command = CancelAllOrders::new(
563                trader_id,
564                client_id.unwrap_or_default(),
565                strategy_id,
566                instrument_id,
567                order_side.unwrap_or(OrderSide::NoOrderSide),
568                UUID4::new(),
569                ts_init,
570                params,
571            )?;
572
573            manager.send_emulator_command(TradingCommand::CancelAllOrders(command));
574        }
575
576        for order in algo_orders {
577            self.cancel_order(order, client_id)?;
578        }
579
580        Ok(())
581    }
582
583    /// Closes a position by submitting a market order for the opposite side.
584    ///
585    /// # Errors
586    ///
587    /// Returns an error if the strategy is not registered or position closing fails.
588    fn close_position(
589        &mut self,
590        position: &Position,
591        client_id: Option<ClientId>,
592        tags: Option<Vec<Ustr>>,
593        time_in_force: Option<TimeInForce>,
594        reduce_only: Option<bool>,
595        quote_quantity: Option<bool>,
596    ) -> anyhow::Result<()> {
597        let core = self.core_mut();
598        let Some(order_factory) = &mut core.order_factory else {
599            anyhow::bail!("Strategy not registered: OrderFactory missing");
600        };
601
602        if position.is_closed() {
603            log::warn!("Cannot close position (already closed): {}", position.id);
604            return Ok(());
605        }
606
607        let closing_side = OrderCore::closing_side(position.side);
608
609        let order = order_factory.market(
610            position.instrument_id,
611            closing_side,
612            position.quantity,
613            time_in_force,
614            reduce_only.or(Some(true)),
615            quote_quantity,
616            None,
617            None,
618            tags,
619            None,
620        );
621
622        self.submit_order(order, Some(position.id), client_id)
623    }
624
625    /// Closes all open positions for the given instrument.
626    ///
627    /// # Errors
628    ///
629    /// Returns an error if the strategy is not registered or position closing fails.
630    #[allow(clippy::too_many_arguments)]
631    fn close_all_positions(
632        &mut self,
633        instrument_id: InstrumentId,
634        position_side: Option<PositionSide>,
635        client_id: Option<ClientId>,
636        tags: Option<Vec<Ustr>>,
637        time_in_force: Option<TimeInForce>,
638        reduce_only: Option<bool>,
639        quote_quantity: Option<bool>,
640    ) -> anyhow::Result<()> {
641        let core = self.core_mut();
642        let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
643        let cache = core.cache();
644
645        let positions_open = cache.positions_open(
646            None,
647            Some(&instrument_id),
648            Some(&strategy_id),
649            position_side,
650        );
651
652        let side_str = position_side.map(|s| format!(" {s}")).unwrap_or_default();
653
654        if positions_open.is_empty() {
655            log::info!("No {instrument_id} open{side_str} positions to close");
656            return Ok(());
657        }
658
659        let count = positions_open.len();
660        log::info!(
661            "Closing {count} open{side_str} position{}",
662            if count == 1 { "" } else { "s" }
663        );
664
665        let positions_data: Vec<_> = positions_open
666            .iter()
667            .map(|p| (p.id, p.instrument_id, p.side, p.quantity, p.is_closed()))
668            .collect();
669
670        drop(cache);
671
672        for (pos_id, pos_instrument_id, pos_side, pos_quantity, is_closed) in positions_data {
673            if is_closed {
674                continue;
675            }
676
677            let core = self.core_mut();
678            let Some(order_factory) = &mut core.order_factory else {
679                anyhow::bail!("Strategy not registered: OrderFactory missing");
680            };
681
682            let closing_side = OrderCore::closing_side(pos_side);
683            let order = order_factory.market(
684                pos_instrument_id,
685                closing_side,
686                pos_quantity,
687                time_in_force,
688                reduce_only.or(Some(true)),
689                quote_quantity,
690                None,
691                None,
692                tags.clone(),
693                None,
694            );
695
696            self.submit_order(order, Some(pos_id), client_id)?;
697        }
698
699        Ok(())
700    }
701
702    /// Queries account state from the execution client.
703    ///
704    /// Creates a [`QueryAccount`] command and sends it to the execution engine,
705    /// which will request the current account state from the execution client.
706    ///
707    /// # Errors
708    ///
709    /// Returns an error if the strategy is not registered.
710    fn query_account(
711        &mut self,
712        account_id: AccountId,
713        client_id: Option<ClientId>,
714    ) -> anyhow::Result<()> {
715        let core = self.core_mut();
716
717        let trader_id = core.trader_id().expect("Trader ID not set");
718        let ts_init = core.clock().timestamp_ns();
719
720        let command = QueryAccount::new(
721            trader_id,
722            client_id.unwrap_or_default(),
723            account_id,
724            UUID4::new(),
725            ts_init,
726        )?;
727
728        let Some(manager) = &mut core.order_manager else {
729            anyhow::bail!("Strategy not registered: OrderManager missing");
730        };
731
732        manager.send_exec_command(TradingCommand::QueryAccount(command));
733        Ok(())
734    }
735
736    /// Queries order state from the execution client.
737    ///
738    /// Creates a [`QueryOrder`] command and sends it to the execution engine,
739    /// which will request the current order state from the execution client.
740    ///
741    /// # Errors
742    ///
743    /// Returns an error if the strategy is not registered.
744    fn query_order(&mut self, order: &OrderAny, client_id: Option<ClientId>) -> anyhow::Result<()> {
745        let core = self.core_mut();
746
747        let trader_id = core.trader_id().expect("Trader ID not set");
748        let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
749        let ts_init = core.clock().timestamp_ns();
750
751        let command = QueryOrder::new(
752            trader_id,
753            client_id.unwrap_or_default(),
754            strategy_id,
755            order.instrument_id(),
756            order.client_order_id(),
757            order.venue_order_id().unwrap_or_default(),
758            UUID4::new(),
759            ts_init,
760        )?;
761
762        let Some(manager) = &mut core.order_manager else {
763            anyhow::bail!("Strategy not registered: OrderManager missing");
764        };
765
766        manager.send_exec_command(TradingCommand::QueryOrder(command));
767        Ok(())
768    }
769
770    /// Handles an order event, dispatching to the appropriate handler and routing to the order manager.
771    fn handle_order_event(&mut self, event: OrderEventAny) {
772        {
773            let core = self.core_mut();
774            if core.config.log_events {
775                let id = &core.actor.actor_id;
776                log::info!("{id} {RECV}{EVT} {event}");
777            }
778        }
779
780        let client_order_id = event.client_order_id();
781        let is_terminal = matches!(
782            &event,
783            OrderEventAny::Filled(_)
784                | OrderEventAny::Canceled(_)
785                | OrderEventAny::Rejected(_)
786                | OrderEventAny::Expired(_)
787                | OrderEventAny::Denied(_)
788        );
789
790        match &event {
791            OrderEventAny::Initialized(e) => self.on_order_initialized(e.clone()),
792            OrderEventAny::Denied(e) => self.on_order_denied(*e),
793            OrderEventAny::Emulated(e) => self.on_order_emulated(*e),
794            OrderEventAny::Released(e) => self.on_order_released(*e),
795            OrderEventAny::Submitted(e) => self.on_order_submitted(*e),
796            OrderEventAny::Rejected(e) => self.on_order_rejected(*e),
797            OrderEventAny::Accepted(e) => self.on_order_accepted(*e),
798            OrderEventAny::Canceled(e) => {
799                let _ = DataActor::on_order_canceled(self, e);
800            }
801            OrderEventAny::Expired(e) => self.on_order_expired(*e),
802            OrderEventAny::Triggered(e) => self.on_order_triggered(*e),
803            OrderEventAny::PendingUpdate(e) => self.on_order_pending_update(*e),
804            OrderEventAny::PendingCancel(e) => self.on_order_pending_cancel(*e),
805            OrderEventAny::ModifyRejected(e) => self.on_order_modify_rejected(*e),
806            OrderEventAny::CancelRejected(e) => self.on_order_cancel_rejected(*e),
807            OrderEventAny::Updated(e) => self.on_order_updated(*e),
808            OrderEventAny::Filled(e) => {
809                let _ = DataActor::on_order_filled(self, e);
810            }
811        }
812
813        if is_terminal {
814            self.cancel_gtd_expiry(&client_order_id);
815        }
816
817        let core = self.core_mut();
818        if let Some(manager) = &mut core.order_manager {
819            manager.handle_event(event);
820        }
821    }
822
823    /// Handles a position event, dispatching to the appropriate handler.
824    fn handle_position_event(&mut self, event: PositionEvent) {
825        {
826            let core = self.core_mut();
827            if core.config.log_events {
828                let id = &core.actor.actor_id;
829                log::info!("{id} {RECV}{EVT} {event:?}");
830            }
831        }
832
833        match event {
834            PositionEvent::PositionOpened(e) => self.on_position_opened(e),
835            PositionEvent::PositionChanged(e) => self.on_position_changed(e),
836            PositionEvent::PositionClosed(e) => self.on_position_closed(e),
837            PositionEvent::PositionAdjusted(_) => {
838                // No handler for adjusted events yet
839            }
840        }
841    }
842
843    // -- LIFECYCLE METHODS -----------------------------------------------------------------------
844
845    /// Called when the strategy is started.
846    ///
847    /// Override this method to implement custom initialization logic.
848    /// The default implementation reactivates GTD timers if `manage_gtd_expiry` is enabled.
849    ///
850    /// # Errors
851    ///
852    /// Returns an error if strategy initialization fails.
853    fn on_start(&mut self) -> anyhow::Result<()> {
854        let core = self.core_mut();
855        let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
856        log::info!("Starting {strategy_id}");
857
858        if core.config.manage_gtd_expiry {
859            self.reactivate_gtd_timers();
860        }
861
862        Ok(())
863    }
864
865    /// Called when a time event is received.
866    ///
867    /// Routes GTD expiry timer events to the expiry handler.
868    ///
869    /// # Errors
870    ///
871    /// Returns an error if time event handling fails.
872    fn on_time_event(&mut self, event: &TimeEvent) -> anyhow::Result<()> {
873        if event.name.starts_with("GTD-EXPIRY:") {
874            self.expire_gtd_order(event.clone());
875        }
876        Ok(())
877    }
878
879    // -- EVENT HANDLERS --------------------------------------------------------------------------
880
881    /// Called when an order is initialized.
882    ///
883    /// Override this method to implement custom logic when an order is first created.
884    #[allow(unused_variables)]
885    fn on_order_initialized(&mut self, event: OrderInitialized) {}
886
887    /// Called when an order is denied by the system.
888    ///
889    /// Override this method to implement custom logic when an order is denied before submission.
890    #[allow(unused_variables)]
891    fn on_order_denied(&mut self, event: OrderDenied) {}
892
893    /// Called when an order is emulated.
894    ///
895    /// Override this method to implement custom logic when an order is taken over by the emulator.
896    #[allow(unused_variables)]
897    fn on_order_emulated(&mut self, event: OrderEmulated) {}
898
899    /// Called when an order is released from emulation.
900    ///
901    /// Override this method to implement custom logic when an emulated order is released.
902    #[allow(unused_variables)]
903    fn on_order_released(&mut self, event: OrderReleased) {}
904
905    /// Called when an order is submitted to the venue.
906    ///
907    /// Override this method to implement custom logic when an order is submitted.
908    #[allow(unused_variables)]
909    fn on_order_submitted(&mut self, event: OrderSubmitted) {}
910
911    /// Called when an order is rejected by the venue.
912    ///
913    /// Override this method to implement custom logic when an order is rejected.
914    #[allow(unused_variables)]
915    fn on_order_rejected(&mut self, event: OrderRejected) {}
916
917    /// Called when an order is accepted by the venue.
918    ///
919    /// Override this method to implement custom logic when an order is accepted.
920    #[allow(unused_variables)]
921    fn on_order_accepted(&mut self, event: OrderAccepted) {}
922
923    /// Called when an order expires.
924    ///
925    /// Override this method to implement custom logic when an order expires.
926    #[allow(unused_variables)]
927    fn on_order_expired(&mut self, event: OrderExpired) {}
928
929    /// Called when an order is triggered.
930    ///
931    /// Override this method to implement custom logic when a stop or conditional order is triggered.
932    #[allow(unused_variables)]
933    fn on_order_triggered(&mut self, event: OrderTriggered) {}
934
935    /// Called when an order modification is pending.
936    ///
937    /// Override this method to implement custom logic when an order is pending modification.
938    #[allow(unused_variables)]
939    fn on_order_pending_update(&mut self, event: OrderPendingUpdate) {}
940
941    /// Called when an order cancellation is pending.
942    ///
943    /// Override this method to implement custom logic when an order is pending cancellation.
944    #[allow(unused_variables)]
945    fn on_order_pending_cancel(&mut self, event: OrderPendingCancel) {}
946
947    /// Called when an order modification is rejected.
948    ///
949    /// Override this method to implement custom logic when an order modification is rejected.
950    #[allow(unused_variables)]
951    fn on_order_modify_rejected(&mut self, event: OrderModifyRejected) {}
952
953    /// Called when an order cancellation is rejected.
954    ///
955    /// Override this method to implement custom logic when an order cancellation is rejected.
956    #[allow(unused_variables)]
957    fn on_order_cancel_rejected(&mut self, event: OrderCancelRejected) {}
958
959    /// Called when an order is updated.
960    ///
961    /// Override this method to implement custom logic when an order is modified.
962    #[allow(unused_variables)]
963    fn on_order_updated(&mut self, event: OrderUpdated) {}
964
965    // Note: on_order_filled is inherited from DataActor trait
966
967    /// Called when a position is opened.
968    ///
969    /// Override this method to implement custom logic when a position is opened.
970    #[allow(unused_variables)]
971    fn on_position_opened(&mut self, event: PositionOpened) {}
972
973    /// Called when a position is changed (quantity or price updated).
974    ///
975    /// Override this method to implement custom logic when a position changes.
976    #[allow(unused_variables)]
977    fn on_position_changed(&mut self, event: PositionChanged) {}
978
979    /// Called when a position is closed.
980    ///
981    /// Override this method to implement custom logic when a position is closed.
982    #[allow(unused_variables)]
983    fn on_position_closed(&mut self, event: PositionClosed) {}
984
985    // -- GTD EXPIRY MANAGEMENT -------------------------------------------------------------------
986
987    /// Sets a GTD expiry timer for an order.
988    ///
989    /// Creates a timer that will automatically cancel the order when it expires.
990    ///
991    /// # Errors
992    ///
993    /// Returns an error if timer creation fails.
994    fn set_gtd_expiry(&mut self, order: &OrderAny) -> anyhow::Result<()> {
995        let core = self.core_mut();
996
997        if !core.config.manage_gtd_expiry || order.time_in_force() != TimeInForce::Gtd {
998            return Ok(());
999        }
1000
1001        let Some(expire_time) = order.expire_time() else {
1002            return Ok(());
1003        };
1004
1005        let client_order_id = order.client_order_id();
1006        let timer_name = format!("GTD-EXPIRY:{client_order_id}");
1007
1008        let current_time_ns = {
1009            let clock = core.clock();
1010            clock.timestamp_ns()
1011        };
1012
1013        if current_time_ns >= expire_time.as_u64() {
1014            log::info!("GTD order {client_order_id} already expired, canceling immediately");
1015            return self.cancel_order(order.clone(), None);
1016        }
1017
1018        {
1019            let mut clock = core.clock();
1020            clock.set_time_alert_ns(&timer_name, expire_time, None, None)?;
1021        }
1022
1023        core.gtd_timers
1024            .insert(client_order_id, Ustr::from(&timer_name));
1025
1026        log::debug!("Set GTD expiry timer for {client_order_id} at {expire_time}");
1027        Ok(())
1028    }
1029
1030    /// Cancels a GTD expiry timer for an order.
1031    fn cancel_gtd_expiry(&mut self, client_order_id: &ClientOrderId) {
1032        let core = self.core_mut();
1033
1034        if let Some(timer_name) = core.gtd_timers.remove(client_order_id) {
1035            core.clock().cancel_timer(timer_name.as_str());
1036            log::debug!("Canceled GTD expiry timer for {client_order_id}");
1037        }
1038    }
1039
1040    /// Checks if a GTD expiry timer exists for an order.
1041    fn has_gtd_expiry_timer(&mut self, client_order_id: &ClientOrderId) -> bool {
1042        let core = self.core_mut();
1043        core.gtd_timers.contains_key(client_order_id)
1044    }
1045
1046    /// Handles GTD order expiry by canceling the order.
1047    ///
1048    /// This method is called when a GTD expiry timer fires.
1049    fn expire_gtd_order(&mut self, event: TimeEvent) {
1050        let timer_name = event.name.to_string();
1051        let Some(client_order_id_str) = timer_name.strip_prefix("GTD-EXPIRY:") else {
1052            log::error!("Invalid GTD timer name format: {timer_name}");
1053            return;
1054        };
1055
1056        let client_order_id = ClientOrderId::from(client_order_id_str);
1057
1058        let core = self.core_mut();
1059        core.gtd_timers.remove(&client_order_id);
1060
1061        let cache = core.cache();
1062        let Some(order) = cache.order(&client_order_id) else {
1063            log::warn!("GTD order {client_order_id} not found in cache");
1064            return;
1065        };
1066
1067        let order = order.clone();
1068        drop(cache);
1069
1070        log::info!("GTD order {client_order_id} expired");
1071
1072        if let Err(e) = self.cancel_order(order, None) {
1073            log::error!("Failed to cancel expired GTD order {client_order_id}: {e}");
1074        }
1075    }
1076
1077    /// Reactivates GTD timers for open orders on strategy start.
1078    ///
1079    /// Queries the cache for all open GTD orders and creates timers for those
1080    /// that haven't expired yet. Orders that have already expired are canceled immediately.
1081    fn reactivate_gtd_timers(&mut self) {
1082        let core = self.core_mut();
1083        let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
1084        let current_time_ns = core.clock().timestamp_ns();
1085        let cache = core.cache();
1086
1087        let open_orders = cache.orders_open(None, None, Some(&strategy_id), None);
1088
1089        let gtd_orders: Vec<_> = open_orders
1090            .iter()
1091            .filter(|o| o.time_in_force() == TimeInForce::Gtd)
1092            .map(|o| (*o).clone())
1093            .collect();
1094
1095        drop(cache);
1096
1097        for order in gtd_orders {
1098            let Some(expire_time) = order.expire_time() else {
1099                continue;
1100            };
1101
1102            let expire_time_ns = expire_time.as_u64();
1103            let client_order_id = order.client_order_id();
1104
1105            if current_time_ns >= expire_time_ns {
1106                log::info!("GTD order {client_order_id} already expired, canceling immediately");
1107                if let Err(e) = self.cancel_order(order, None) {
1108                    log::error!("Failed to cancel expired GTD order {client_order_id}: {e}");
1109                }
1110            } else if let Err(e) = self.set_gtd_expiry(&order) {
1111                log::error!("Failed to set GTD expiry timer for {client_order_id}: {e}");
1112            }
1113        }
1114    }
1115}
1116
1117#[cfg(test)]
1118mod tests {
1119    use std::{
1120        cell::RefCell,
1121        ops::{Deref, DerefMut},
1122        rc::Rc,
1123    };
1124
1125    use nautilus_common::{
1126        actor::{DataActor, DataActorCore},
1127        cache::Cache,
1128        clock::TestClock,
1129    };
1130    use nautilus_model::{
1131        enums::{OrderSide, PositionSide},
1132        events::OrderRejected,
1133        identifiers::{AccountId, ClientOrderId, InstrumentId, StrategyId, TraderId},
1134        types::Currency,
1135    };
1136    use nautilus_portfolio::portfolio::Portfolio;
1137    use rstest::rstest;
1138
1139    use super::*;
1140
1141    #[derive(Debug)]
1142    struct TestStrategy {
1143        core: StrategyCore,
1144        on_order_rejected_called: bool,
1145        on_position_opened_called: bool,
1146    }
1147
1148    impl TestStrategy {
1149        fn new(config: StrategyConfig) -> Self {
1150            Self {
1151                core: StrategyCore::new(config),
1152                on_order_rejected_called: false,
1153                on_position_opened_called: false,
1154            }
1155        }
1156    }
1157
1158    impl Deref for TestStrategy {
1159        type Target = DataActorCore;
1160        fn deref(&self) -> &Self::Target {
1161            &self.core.actor
1162        }
1163    }
1164
1165    impl DerefMut for TestStrategy {
1166        fn deref_mut(&mut self) -> &mut Self::Target {
1167            &mut self.core.actor
1168        }
1169    }
1170
1171    impl DataActor for TestStrategy {}
1172
1173    impl Strategy for TestStrategy {
1174        fn core_mut(&mut self) -> &mut StrategyCore {
1175            &mut self.core
1176        }
1177
1178        fn on_order_rejected(&mut self, _event: OrderRejected) {
1179            self.on_order_rejected_called = true;
1180        }
1181
1182        fn on_position_opened(&mut self, _event: PositionOpened) {
1183            self.on_position_opened_called = true;
1184        }
1185    }
1186
1187    fn create_test_strategy() -> TestStrategy {
1188        let config = StrategyConfig {
1189            strategy_id: Some(StrategyId::from("TEST-001")),
1190            order_id_tag: Some("001".to_string()),
1191            ..Default::default()
1192        };
1193        TestStrategy::new(config)
1194    }
1195
1196    fn register_strategy(strategy: &mut TestStrategy) {
1197        let trader_id = TraderId::from("TRADER-001");
1198        let clock = Rc::new(RefCell::new(TestClock::new()));
1199        let cache = Rc::new(RefCell::new(Cache::default()));
1200        let portfolio = Rc::new(RefCell::new(Portfolio::new(
1201            cache.clone(),
1202            clock.clone(),
1203            None,
1204        )));
1205
1206        strategy
1207            .core
1208            .register(trader_id, clock, cache, portfolio)
1209            .unwrap();
1210    }
1211
1212    #[rstest]
1213    fn test_strategy_creation() {
1214        let strategy = create_test_strategy();
1215        assert_eq!(
1216            strategy.core.config.strategy_id,
1217            Some(StrategyId::from("TEST-001"))
1218        );
1219        assert!(!strategy.on_order_rejected_called);
1220        assert!(!strategy.on_position_opened_called);
1221    }
1222
1223    #[rstest]
1224    fn test_strategy_registration() {
1225        let mut strategy = create_test_strategy();
1226        register_strategy(&mut strategy);
1227
1228        assert!(strategy.core.order_manager.is_some());
1229        assert!(strategy.core.order_factory.is_some());
1230        assert!(strategy.core.portfolio.is_some());
1231    }
1232
1233    #[rstest]
1234    fn test_handle_order_event_dispatches_to_handler() {
1235        let mut strategy = create_test_strategy();
1236        register_strategy(&mut strategy);
1237
1238        let event = OrderEventAny::Rejected(OrderRejected {
1239            trader_id: TraderId::from("TRADER-001"),
1240            strategy_id: StrategyId::from("TEST-001"),
1241            instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
1242            client_order_id: ClientOrderId::from("O-001"),
1243            account_id: AccountId::from("ACC-001"),
1244            reason: "Test rejection".into(),
1245            event_id: Default::default(),
1246            ts_event: Default::default(),
1247            ts_init: Default::default(),
1248            reconciliation: 0,
1249            due_post_only: 0,
1250        });
1251
1252        strategy.handle_order_event(event);
1253
1254        assert!(strategy.on_order_rejected_called);
1255    }
1256
1257    #[rstest]
1258    fn test_handle_position_event_dispatches_to_handler() {
1259        let mut strategy = create_test_strategy();
1260        register_strategy(&mut strategy);
1261
1262        let event = PositionEvent::PositionOpened(PositionOpened {
1263            trader_id: TraderId::from("TRADER-001"),
1264            strategy_id: StrategyId::from("TEST-001"),
1265            instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
1266            position_id: Default::default(),
1267            account_id: AccountId::from("ACC-001"),
1268            opening_order_id: ClientOrderId::from("O-001"),
1269            entry: OrderSide::Buy,
1270            side: PositionSide::Long,
1271            signed_qty: 1.0,
1272            quantity: Default::default(),
1273            last_qty: Default::default(),
1274            last_px: Default::default(),
1275            currency: Currency::from("USD"),
1276            avg_px_open: 0.0,
1277            event_id: Default::default(),
1278            ts_event: Default::default(),
1279            ts_init: Default::default(),
1280        });
1281
1282        strategy.handle_position_event(event);
1283
1284        assert!(strategy.on_position_opened_called);
1285    }
1286
1287    #[rstest]
1288    fn test_strategy_default_handlers_do_not_panic() {
1289        let mut strategy = create_test_strategy();
1290
1291        strategy.on_order_initialized(Default::default());
1292        strategy.on_order_denied(Default::default());
1293        strategy.on_order_emulated(Default::default());
1294        strategy.on_order_released(Default::default());
1295        strategy.on_order_submitted(Default::default());
1296        strategy.on_order_rejected(Default::default());
1297        let _ = DataActor::on_order_canceled(&mut strategy, &Default::default());
1298        strategy.on_order_expired(Default::default());
1299        strategy.on_order_triggered(Default::default());
1300        strategy.on_order_pending_update(Default::default());
1301        strategy.on_order_pending_cancel(Default::default());
1302        strategy.on_order_modify_rejected(Default::default());
1303        strategy.on_order_cancel_rejected(Default::default());
1304        strategy.on_order_updated(Default::default());
1305    }
1306
1307    // -- GTD EXPIRY TESTS ----------------------------------------------------------------------------
1308
1309    #[rstest]
1310    fn test_has_gtd_expiry_timer_when_timer_not_set() {
1311        let mut strategy = create_test_strategy();
1312        let client_order_id = ClientOrderId::from("O-001");
1313
1314        assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
1315    }
1316
1317    #[rstest]
1318    fn test_has_gtd_expiry_timer_when_timer_set() {
1319        let mut strategy = create_test_strategy();
1320        let client_order_id = ClientOrderId::from("O-001");
1321
1322        strategy
1323            .core
1324            .gtd_timers
1325            .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
1326
1327        assert!(strategy.has_gtd_expiry_timer(&client_order_id));
1328    }
1329
1330    #[rstest]
1331    fn test_cancel_gtd_expiry_removes_timer() {
1332        let mut strategy = create_test_strategy();
1333        register_strategy(&mut strategy);
1334
1335        let client_order_id = ClientOrderId::from("O-001");
1336        strategy
1337            .core
1338            .gtd_timers
1339            .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
1340
1341        strategy.cancel_gtd_expiry(&client_order_id);
1342
1343        assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
1344    }
1345
1346    #[rstest]
1347    fn test_cancel_gtd_expiry_when_timer_not_set() {
1348        let mut strategy = create_test_strategy();
1349        register_strategy(&mut strategy);
1350
1351        let client_order_id = ClientOrderId::from("O-001");
1352
1353        strategy.cancel_gtd_expiry(&client_order_id);
1354
1355        assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
1356    }
1357
1358    #[rstest]
1359    fn test_handle_order_event_cancels_gtd_timer_on_filled() {
1360        use nautilus_model::events::OrderFilled;
1361
1362        let mut strategy = create_test_strategy();
1363        register_strategy(&mut strategy);
1364
1365        let client_order_id = ClientOrderId::from("O-001");
1366        strategy
1367            .core
1368            .gtd_timers
1369            .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
1370
1371        use nautilus_model::enums::{LiquiditySide, OrderType};
1372
1373        let event = OrderEventAny::Filled(OrderFilled {
1374            trader_id: TraderId::from("TRADER-001"),
1375            strategy_id: StrategyId::from("TEST-001"),
1376            instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
1377            client_order_id,
1378            venue_order_id: Default::default(),
1379            account_id: AccountId::from("ACC-001"),
1380            trade_id: Default::default(),
1381            position_id: Default::default(),
1382            order_side: OrderSide::Buy,
1383            order_type: OrderType::Market,
1384            last_qty: Default::default(),
1385            last_px: Default::default(),
1386            currency: Currency::from("USD"),
1387            liquidity_side: LiquiditySide::Taker,
1388            event_id: Default::default(),
1389            ts_event: Default::default(),
1390            ts_init: Default::default(),
1391            reconciliation: false,
1392            commission: None,
1393        });
1394        strategy.handle_order_event(event);
1395
1396        assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
1397    }
1398
1399    #[rstest]
1400    fn test_handle_order_event_cancels_gtd_timer_on_canceled() {
1401        use nautilus_model::events::OrderCanceled;
1402
1403        let mut strategy = create_test_strategy();
1404        register_strategy(&mut strategy);
1405
1406        let client_order_id = ClientOrderId::from("O-001");
1407        strategy
1408            .core
1409            .gtd_timers
1410            .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
1411
1412        let event = OrderEventAny::Canceled(OrderCanceled {
1413            trader_id: TraderId::from("TRADER-001"),
1414            strategy_id: StrategyId::from("TEST-001"),
1415            instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
1416            client_order_id,
1417            venue_order_id: Default::default(),
1418            account_id: Some(AccountId::from("ACC-001")),
1419            event_id: Default::default(),
1420            ts_event: Default::default(),
1421            ts_init: Default::default(),
1422            reconciliation: 0,
1423        });
1424        strategy.handle_order_event(event);
1425
1426        assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
1427    }
1428
1429    #[rstest]
1430    fn test_handle_order_event_cancels_gtd_timer_on_rejected() {
1431        let mut strategy = create_test_strategy();
1432        register_strategy(&mut strategy);
1433
1434        let client_order_id = ClientOrderId::from("O-001");
1435        strategy
1436            .core
1437            .gtd_timers
1438            .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
1439
1440        let event = OrderEventAny::Rejected(OrderRejected {
1441            trader_id: TraderId::from("TRADER-001"),
1442            strategy_id: StrategyId::from("TEST-001"),
1443            instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
1444            client_order_id,
1445            account_id: AccountId::from("ACC-001"),
1446            reason: "Test rejection".into(),
1447            event_id: Default::default(),
1448            ts_event: Default::default(),
1449            ts_init: Default::default(),
1450            reconciliation: 0,
1451            due_post_only: 0,
1452        });
1453        strategy.handle_order_event(event);
1454
1455        assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
1456    }
1457
1458    #[rstest]
1459    fn test_handle_order_event_cancels_gtd_timer_on_expired() {
1460        let mut strategy = create_test_strategy();
1461        register_strategy(&mut strategy);
1462
1463        let client_order_id = ClientOrderId::from("O-001");
1464        strategy
1465            .core
1466            .gtd_timers
1467            .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
1468
1469        let event = OrderEventAny::Expired(OrderExpired {
1470            trader_id: TraderId::from("TRADER-001"),
1471            strategy_id: StrategyId::from("TEST-001"),
1472            instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
1473            client_order_id,
1474            venue_order_id: Default::default(),
1475            account_id: Some(AccountId::from("ACC-001")),
1476            event_id: Default::default(),
1477            ts_event: Default::default(),
1478            ts_init: Default::default(),
1479            reconciliation: 0,
1480        });
1481        strategy.handle_order_event(event);
1482
1483        assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
1484    }
1485
1486    #[rstest]
1487    fn test_on_start_calls_reactivate_gtd_timers_when_enabled() {
1488        let config = StrategyConfig {
1489            strategy_id: Some(StrategyId::from("TEST-001")),
1490            order_id_tag: Some("001".to_string()),
1491            manage_gtd_expiry: true,
1492            ..Default::default()
1493        };
1494        let mut strategy = TestStrategy::new(config);
1495        register_strategy(&mut strategy);
1496
1497        let result = Strategy::on_start(&mut strategy);
1498        assert!(result.is_ok());
1499    }
1500
1501    #[rstest]
1502    fn test_on_start_does_not_panic_when_gtd_disabled() {
1503        let config = StrategyConfig {
1504            strategy_id: Some(StrategyId::from("TEST-001")),
1505            order_id_tag: Some("001".to_string()),
1506            manage_gtd_expiry: false,
1507            ..Default::default()
1508        };
1509        let mut strategy = TestStrategy::new(config);
1510        register_strategy(&mut strategy);
1511
1512        let result = Strategy::on_start(&mut strategy);
1513        assert!(result.is_ok());
1514    }
1515
1516    // -- QUERY TESTS ---------------------------------------------------------------------------------
1517
1518    #[rstest]
1519    fn test_query_account_when_registered() {
1520        let mut strategy = create_test_strategy();
1521        register_strategy(&mut strategy);
1522
1523        let account_id = AccountId::from("ACC-001");
1524
1525        let result = strategy.query_account(account_id, None);
1526
1527        assert!(result.is_ok());
1528    }
1529
1530    #[rstest]
1531    fn test_query_account_with_client_id() {
1532        let mut strategy = create_test_strategy();
1533        register_strategy(&mut strategy);
1534
1535        let account_id = AccountId::from("ACC-001");
1536        let client_id = ClientId::from("BINANCE");
1537
1538        let result = strategy.query_account(account_id, Some(client_id));
1539
1540        assert!(result.is_ok());
1541    }
1542
1543    #[rstest]
1544    fn test_query_order_when_registered() {
1545        use nautilus_model::orders::MarketOrder;
1546
1547        let mut strategy = create_test_strategy();
1548        register_strategy(&mut strategy);
1549
1550        let order = OrderAny::Market(MarketOrder::default());
1551
1552        let result = strategy.query_order(&order, None);
1553
1554        assert!(result.is_ok());
1555    }
1556
1557    #[rstest]
1558    fn test_query_order_with_client_id() {
1559        use nautilus_model::orders::MarketOrder;
1560
1561        let mut strategy = create_test_strategy();
1562        register_strategy(&mut strategy);
1563
1564        let order = OrderAny::Market(MarketOrder::default());
1565        let client_id = ClientId::from("BINANCE");
1566
1567        let result = strategy.query_order(&order, Some(client_id));
1568
1569        assert!(result.is_ok());
1570    }
1571}