1pub 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
49pub trait Strategy: DataActor {
71 fn core_mut(&mut self) -> &mut StrategyCore;
76
77 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 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 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 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 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 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 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 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 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 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 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 #[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 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 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 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 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 }
840 }
841 }
842
843 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 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 #[allow(unused_variables)]
885 fn on_order_initialized(&mut self, event: OrderInitialized) {}
886
887 #[allow(unused_variables)]
891 fn on_order_denied(&mut self, event: OrderDenied) {}
892
893 #[allow(unused_variables)]
897 fn on_order_emulated(&mut self, event: OrderEmulated) {}
898
899 #[allow(unused_variables)]
903 fn on_order_released(&mut self, event: OrderReleased) {}
904
905 #[allow(unused_variables)]
909 fn on_order_submitted(&mut self, event: OrderSubmitted) {}
910
911 #[allow(unused_variables)]
915 fn on_order_rejected(&mut self, event: OrderRejected) {}
916
917 #[allow(unused_variables)]
921 fn on_order_accepted(&mut self, event: OrderAccepted) {}
922
923 #[allow(unused_variables)]
927 fn on_order_expired(&mut self, event: OrderExpired) {}
928
929 #[allow(unused_variables)]
933 fn on_order_triggered(&mut self, event: OrderTriggered) {}
934
935 #[allow(unused_variables)]
939 fn on_order_pending_update(&mut self, event: OrderPendingUpdate) {}
940
941 #[allow(unused_variables)]
945 fn on_order_pending_cancel(&mut self, event: OrderPendingCancel) {}
946
947 #[allow(unused_variables)]
951 fn on_order_modify_rejected(&mut self, event: OrderModifyRejected) {}
952
953 #[allow(unused_variables)]
957 fn on_order_cancel_rejected(&mut self, event: OrderCancelRejected) {}
958
959 #[allow(unused_variables)]
963 fn on_order_updated(&mut self, event: OrderUpdated) {}
964
965 #[allow(unused_variables)]
971 fn on_position_opened(&mut self, event: PositionOpened) {}
972
973 #[allow(unused_variables)]
977 fn on_position_changed(&mut self, event: PositionChanged) {}
978
979 #[allow(unused_variables)]
983 fn on_position_closed(&mut self, event: PositionClosed) {}
984
985 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 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 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 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 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 #[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 #[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}