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 external_order_claims(&self) -> Option<Vec<InstrumentId>> {
82 None
83 }
84
85 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 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 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, 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 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 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 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 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 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 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 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 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 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 #[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 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 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 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 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 }
918 }
919 }
920
921 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 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 #[allow(unused_variables)]
963 fn on_order_initialized(&mut self, event: OrderInitialized) {}
964
965 #[allow(unused_variables)]
969 fn on_order_denied(&mut self, event: OrderDenied) {}
970
971 #[allow(unused_variables)]
975 fn on_order_emulated(&mut self, event: OrderEmulated) {}
976
977 #[allow(unused_variables)]
981 fn on_order_released(&mut self, event: OrderReleased) {}
982
983 #[allow(unused_variables)]
987 fn on_order_submitted(&mut self, event: OrderSubmitted) {}
988
989 #[allow(unused_variables)]
993 fn on_order_rejected(&mut self, event: OrderRejected) {}
994
995 #[allow(unused_variables)]
999 fn on_order_accepted(&mut self, event: OrderAccepted) {}
1000
1001 #[allow(unused_variables)]
1005 fn on_order_expired(&mut self, event: OrderExpired) {}
1006
1007 #[allow(unused_variables)]
1011 fn on_order_triggered(&mut self, event: OrderTriggered) {}
1012
1013 #[allow(unused_variables)]
1017 fn on_order_pending_update(&mut self, event: OrderPendingUpdate) {}
1018
1019 #[allow(unused_variables)]
1023 fn on_order_pending_cancel(&mut self, event: OrderPendingCancel) {}
1024
1025 #[allow(unused_variables)]
1029 fn on_order_modify_rejected(&mut self, event: OrderModifyRejected) {}
1030
1031 #[allow(unused_variables)]
1035 fn on_order_cancel_rejected(&mut self, event: OrderCancelRejected) {}
1036
1037 #[allow(unused_variables)]
1041 fn on_order_updated(&mut self, event: OrderUpdated) {}
1042
1043 #[allow(unused_variables)]
1049 fn on_position_opened(&mut self, event: PositionOpened) {}
1050
1051 #[allow(unused_variables)]
1055 fn on_position_changed(&mut self, event: PositionChanged) {}
1056
1057 #[allow(unused_variables)]
1061 fn on_position_closed(&mut self, event: PositionClosed) {}
1062
1063 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 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 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 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 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 #[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 #[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}