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