1pub mod config;
17pub mod core;
18
19pub use core::StrategyCore;
20use std::panic::{AssertUnwindSafe, catch_unwind};
21
22use ahash::AHashSet;
23pub use config::{ImportableStrategyConfig, StrategyConfig};
24use nautilus_common::{
25 actor::DataActor,
26 component::Component,
27 enums::ComponentState,
28 logging::{EVT, RECV},
29 messages::execution::{
30 BatchCancelOrders, CancelAllOrders, CancelOrder, ModifyOrder, QueryAccount, QueryOrder,
31 SubmitOrder, SubmitOrderList, TradingCommand,
32 },
33 msgbus,
34 timer::TimeEvent,
35};
36use nautilus_core::{Params, UUID4};
37use nautilus_model::{
38 enums::{OrderSide, OrderStatus, PositionSide, TimeInForce, TriggerType},
39 events::{
40 OrderAccepted, OrderCancelRejected, OrderDenied, OrderEmulated, OrderEventAny,
41 OrderExpired, OrderInitialized, OrderModifyRejected, OrderPendingCancel,
42 OrderPendingUpdate, OrderRejected, OrderReleased, OrderSubmitted, OrderTriggered,
43 OrderUpdated, PositionChanged, PositionClosed, PositionEvent, PositionOpened,
44 },
45 identifiers::{AccountId, ClientId, ClientOrderId, InstrumentId, PositionId, StrategyId},
46 orders::{Order, OrderAny, OrderCore, OrderList},
47 position::Position,
48 types::{Price, Quantity},
49};
50use ustr::Ustr;
51
52pub trait Strategy: DataActor {
84 fn core(&self) -> &StrategyCore;
88
89 fn core_mut(&mut self) -> &mut StrategyCore;
93
94 fn external_order_claims(&self) -> Option<Vec<InstrumentId>> {
99 None
100 }
101
102 fn submit_order(
108 &mut self,
109 order: OrderAny,
110 position_id: Option<PositionId>,
111 client_id: Option<ClientId>,
112 ) -> anyhow::Result<()> {
113 self.submit_order_with_params(order, position_id, client_id, Params::new())
114 }
115
116 fn submit_order_with_params(
122 &mut self,
123 order: OrderAny,
124 position_id: Option<PositionId>,
125 client_id: Option<ClientId>,
126 params: Params,
127 ) -> anyhow::Result<()> {
128 let core = self.core_mut();
129
130 let trader_id = core.trader_id().expect("Trader ID not set");
131 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
132 let ts_init = core.clock().timestamp_ns();
133
134 let market_exit_tag = core.market_exit_tag;
135 let is_market_exit_order = order
136 .tags()
137 .is_some_and(|tags| tags.contains(&market_exit_tag));
138
139 if core.is_exiting && !order.is_reduce_only() && !is_market_exit_order {
140 self.deny_order(&order, Ustr::from("MARKET_EXIT_IN_PROGRESS"));
141 return Ok(());
142 }
143
144 let core = self.core_mut();
145 let params = if params.is_empty() {
146 None
147 } else {
148 Some(params)
149 };
150
151 {
152 let cache_rc = core.cache_rc();
153 let mut cache = cache_rc.borrow_mut();
154 cache.add_order(order.clone(), position_id, client_id, true)?;
155 }
156
157 let command = SubmitOrder::new(
158 trader_id,
159 client_id,
160 strategy_id,
161 order.instrument_id(),
162 order.client_order_id(),
163 order.init_event().clone(),
164 order.exec_algorithm_id(),
165 position_id,
166 params,
167 UUID4::new(),
168 ts_init,
169 );
170
171 let manager = core.order_manager();
172
173 if matches!(order.emulation_trigger(), Some(trigger) if trigger != TriggerType::NoTrigger) {
174 manager.send_emulator_command(TradingCommand::SubmitOrder(command));
175 } else if order.exec_algorithm_id().is_some() {
176 manager.send_algo_command(command, order.exec_algorithm_id().unwrap());
177 } else {
178 manager.send_risk_command(TradingCommand::SubmitOrder(command));
179 }
180
181 self.set_gtd_expiry(&order)?;
182 Ok(())
183 }
184
185 fn submit_order_list(
192 &mut self,
193 mut orders: Vec<OrderAny>,
194 position_id: Option<PositionId>,
195 client_id: Option<ClientId>,
196 ) -> anyhow::Result<()> {
197 let should_deny = {
198 let core = self.core_mut();
199 let tag = core.market_exit_tag;
200 core.is_exiting
201 && orders.iter().any(|o| {
202 !o.is_reduce_only() && !o.tags().is_some_and(|tags| tags.contains(&tag))
203 })
204 };
205
206 if should_deny {
207 self.deny_order_list(&orders, Ustr::from("MARKET_EXIT_IN_PROGRESS"));
208 return Ok(());
209 }
210
211 let core = self.core_mut();
212 let trader_id = core.trader_id().expect("Trader ID not set");
213 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
214 let ts_init = core.clock().timestamp_ns();
215
216 let order_list = if orders.first().is_some_and(|o| o.order_list_id().is_some()) {
218 OrderList::from_orders(&orders, ts_init)
219 } else {
220 core.order_factory().create_list(&mut orders, ts_init)
221 };
222
223 {
224 let cache_rc = core.cache_rc();
225 let cache = cache_rc.borrow();
226 if cache.order_list_exists(&order_list.id) {
227 anyhow::bail!("OrderList denied: duplicate {}", order_list.id);
228 }
229
230 for order in &orders {
231 if order.status() != OrderStatus::Initialized {
232 anyhow::bail!(
233 "Order in list denied: invalid status for {}, expected INITIALIZED",
234 order.client_order_id()
235 );
236 }
237
238 if cache.order_exists(&order.client_order_id()) {
239 anyhow::bail!(
240 "Order in list denied: duplicate {}",
241 order.client_order_id()
242 );
243 }
244 }
245 }
246
247 {
248 let cache_rc = core.cache_rc();
249 let mut cache = cache_rc.borrow_mut();
250 cache.add_order_list(order_list.clone())?;
251 for order in &orders {
252 cache.add_order(order.clone(), position_id, client_id, true)?;
253 }
254 }
255
256 let first_order = orders.first();
257 let order_inits: Vec<_> = orders.iter().map(|o| o.init_event().clone()).collect();
258 let exec_algorithm_id = first_order.and_then(|o| o.exec_algorithm_id());
259
260 let command = SubmitOrderList::new(
261 trader_id,
262 client_id,
263 strategy_id,
264 order_list,
265 order_inits,
266 exec_algorithm_id,
267 position_id,
268 None, UUID4::new(),
270 ts_init,
271 );
272
273 let has_emulated_order = orders.iter().any(|o| {
274 matches!(o.emulation_trigger(), Some(trigger) if trigger != TriggerType::NoTrigger)
275 || o.is_emulated()
276 });
277
278 let manager = core.order_manager();
279
280 if has_emulated_order {
281 manager.send_emulator_command(TradingCommand::SubmitOrderList(command));
282 } else if let Some(algo_id) = exec_algorithm_id {
283 let endpoint = format!("{algo_id}.execute");
284 msgbus::send_any(endpoint.into(), &TradingCommand::SubmitOrderList(command));
285 } else {
286 manager.send_risk_command(TradingCommand::SubmitOrderList(command));
287 }
288
289 for order in &orders {
290 self.set_gtd_expiry(order)?;
291 }
292
293 Ok(())
294 }
295
296 fn submit_order_list_with_params(
303 &mut self,
304 mut orders: Vec<OrderAny>,
305 position_id: Option<PositionId>,
306 client_id: Option<ClientId>,
307 params: Params,
308 ) -> anyhow::Result<()> {
309 let should_deny = {
310 let core = self.core_mut();
311 let tag = core.market_exit_tag;
312 core.is_exiting
313 && orders.iter().any(|o| {
314 !o.is_reduce_only() && !o.tags().is_some_and(|tags| tags.contains(&tag))
315 })
316 };
317
318 if should_deny {
319 self.deny_order_list(&orders, Ustr::from("MARKET_EXIT_IN_PROGRESS"));
320 return Ok(());
321 }
322
323 let core = self.core_mut();
324
325 let trader_id = core.trader_id().expect("Trader ID not set");
326 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
327 let ts_init = core.clock().timestamp_ns();
328
329 let order_list = if orders.first().is_some_and(|o| o.order_list_id().is_some()) {
331 OrderList::from_orders(&orders, ts_init)
332 } else {
333 core.order_factory().create_list(&mut orders, ts_init)
334 };
335
336 {
337 let cache_rc = core.cache_rc();
338 let cache = cache_rc.borrow();
339 if cache.order_list_exists(&order_list.id) {
340 anyhow::bail!("OrderList denied: duplicate {}", order_list.id);
341 }
342
343 for order in &orders {
344 if order.status() != OrderStatus::Initialized {
345 anyhow::bail!(
346 "Order in list denied: invalid status for {}, expected INITIALIZED",
347 order.client_order_id()
348 );
349 }
350
351 if cache.order_exists(&order.client_order_id()) {
352 anyhow::bail!(
353 "Order in list denied: duplicate {}",
354 order.client_order_id()
355 );
356 }
357 }
358 }
359
360 {
361 let cache_rc = core.cache_rc();
362 let mut cache = cache_rc.borrow_mut();
363 cache.add_order_list(order_list.clone())?;
364 for order in &orders {
365 cache.add_order(order.clone(), position_id, client_id, true)?;
366 }
367 }
368
369 let params_opt = if params.is_empty() {
370 None
371 } else {
372 Some(params)
373 };
374
375 let first_order = orders.first();
376 let order_inits: Vec<_> = orders.iter().map(|o| o.init_event().clone()).collect();
377 let exec_algorithm_id = first_order.and_then(|o| o.exec_algorithm_id());
378
379 let command = SubmitOrderList::new(
380 trader_id,
381 client_id,
382 strategy_id,
383 order_list,
384 order_inits,
385 exec_algorithm_id,
386 position_id,
387 params_opt,
388 UUID4::new(),
389 ts_init,
390 );
391
392 let has_emulated_order = orders.iter().any(|o| {
393 matches!(o.emulation_trigger(), Some(trigger) if trigger != TriggerType::NoTrigger)
394 || o.is_emulated()
395 });
396
397 let manager = core.order_manager();
398
399 if has_emulated_order {
400 manager.send_emulator_command(TradingCommand::SubmitOrderList(command));
401 } else if let Some(algo_id) = exec_algorithm_id {
402 let endpoint = format!("{algo_id}.execute");
403 msgbus::send_any(endpoint.into(), &TradingCommand::SubmitOrderList(command));
404 } else {
405 manager.send_risk_command(TradingCommand::SubmitOrderList(command));
406 }
407
408 for order in &orders {
409 self.set_gtd_expiry(order)?;
410 }
411
412 Ok(())
413 }
414
415 fn modify_order(
421 &mut self,
422 order: OrderAny,
423 quantity: Option<Quantity>,
424 price: Option<Price>,
425 trigger_price: Option<Price>,
426 client_id: Option<ClientId>,
427 ) -> anyhow::Result<()> {
428 self.modify_order_with_params(
429 order,
430 quantity,
431 price,
432 trigger_price,
433 client_id,
434 Params::new(),
435 )
436 }
437
438 fn modify_order_with_params(
444 &mut self,
445 order: OrderAny,
446 quantity: Option<Quantity>,
447 price: Option<Price>,
448 trigger_price: Option<Price>,
449 client_id: Option<ClientId>,
450 params: Params,
451 ) -> anyhow::Result<()> {
452 let core = self.core_mut();
453
454 let trader_id = core.trader_id().expect("Trader ID not set");
455 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
456 let ts_init = core.clock().timestamp_ns();
457
458 let params = if params.is_empty() {
459 None
460 } else {
461 Some(params)
462 };
463
464 let command = ModifyOrder::new(
465 trader_id,
466 client_id,
467 strategy_id,
468 order.instrument_id(),
469 order.client_order_id(),
470 order.venue_order_id(),
471 quantity,
472 price,
473 trigger_price,
474 UUID4::new(),
475 ts_init,
476 params,
477 );
478
479 let manager = core.order_manager();
480
481 if matches!(order.emulation_trigger(), Some(trigger) if trigger != TriggerType::NoTrigger) {
482 manager.send_emulator_command(TradingCommand::ModifyOrder(command));
483 } else if order.exec_algorithm_id().is_some() {
484 manager.send_risk_command(TradingCommand::ModifyOrder(command));
485 } else {
486 manager.send_exec_command(TradingCommand::ModifyOrder(command));
487 }
488 Ok(())
489 }
490
491 fn cancel_order(&mut self, order: OrderAny, client_id: Option<ClientId>) -> anyhow::Result<()> {
497 self.cancel_order_with_params(order, client_id, Params::new())
498 }
499
500 fn cancel_order_with_params(
506 &mut self,
507 order: OrderAny,
508 client_id: Option<ClientId>,
509 params: Params,
510 ) -> anyhow::Result<()> {
511 let core = self.core_mut();
512
513 let trader_id = core.trader_id().expect("Trader ID not set");
514 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
515 let ts_init = core.clock().timestamp_ns();
516
517 let params = if params.is_empty() {
518 None
519 } else {
520 Some(params)
521 };
522
523 let command = CancelOrder::new(
524 trader_id,
525 client_id,
526 strategy_id,
527 order.instrument_id(),
528 order.client_order_id(),
529 order.venue_order_id(),
530 UUID4::new(),
531 ts_init,
532 params,
533 );
534
535 let manager = core.order_manager();
536
537 if matches!(order.emulation_trigger(), Some(trigger) if trigger != TriggerType::NoTrigger)
538 || order.is_emulated()
539 {
540 manager.send_emulator_command(TradingCommand::CancelOrder(command));
541 } else if let Some(algo_id) = order.exec_algorithm_id() {
542 let endpoint = format!("{algo_id}.execute");
543 msgbus::send_any(endpoint.into(), &TradingCommand::CancelOrder(command));
544 } else {
545 manager.send_exec_command(TradingCommand::CancelOrder(command));
546 }
547 Ok(())
548 }
549
550 fn cancel_orders(
557 &mut self,
558 mut orders: Vec<OrderAny>,
559 client_id: Option<ClientId>,
560 params: Option<Params>,
561 ) -> anyhow::Result<()> {
562 if orders.is_empty() {
563 anyhow::bail!("Cannot batch cancel empty order list");
564 }
565
566 let core = self.core_mut();
567 let trader_id = core.trader_id().expect("Trader ID not set");
568 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
569 let ts_init = core.clock().timestamp_ns();
570
571 let manager = core.order_manager();
572
573 let first = orders.remove(0);
574 let instrument_id = first.instrument_id();
575
576 if first.is_emulated() || first.is_active_local() {
577 anyhow::bail!("Cannot include emulated or local orders in batch cancel");
578 }
579
580 let mut cancels = Vec::with_capacity(orders.len() + 1);
581 cancels.push(CancelOrder::new(
582 trader_id,
583 client_id,
584 strategy_id,
585 instrument_id,
586 first.client_order_id(),
587 first.venue_order_id(),
588 UUID4::new(),
589 ts_init,
590 params.clone(),
591 ));
592
593 for order in orders {
594 if order.instrument_id() != instrument_id {
595 anyhow::bail!(
596 "Cannot batch cancel orders for different instruments: {} vs {}",
597 instrument_id,
598 order.instrument_id()
599 );
600 }
601
602 if order.is_emulated() || order.is_active_local() {
603 anyhow::bail!("Cannot include emulated or local orders in batch cancel");
604 }
605
606 cancels.push(CancelOrder::new(
607 trader_id,
608 client_id,
609 strategy_id,
610 instrument_id,
611 order.client_order_id(),
612 order.venue_order_id(),
613 UUID4::new(),
614 ts_init,
615 params.clone(),
616 ));
617 }
618
619 let command = BatchCancelOrders::new(
620 trader_id,
621 client_id,
622 strategy_id,
623 instrument_id,
624 cancels,
625 UUID4::new(),
626 ts_init,
627 params,
628 );
629
630 manager.send_exec_command(TradingCommand::BatchCancelOrders(command));
631 Ok(())
632 }
633
634 fn cancel_all_orders(
640 &mut self,
641 instrument_id: InstrumentId,
642 order_side: Option<OrderSide>,
643 client_id: Option<ClientId>,
644 ) -> anyhow::Result<()> {
645 self.cancel_all_orders_with_params(instrument_id, order_side, client_id, Params::new())
646 }
647
648 fn cancel_all_orders_with_params(
654 &mut self,
655 instrument_id: InstrumentId,
656 order_side: Option<OrderSide>,
657 client_id: Option<ClientId>,
658 params: Params,
659 ) -> anyhow::Result<()> {
660 let params = if params.is_empty() {
661 None
662 } else {
663 Some(params)
664 };
665 let core = self.core_mut();
666
667 let trader_id = core.trader_id().expect("Trader ID not set");
668 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
669 let ts_init = core.clock().timestamp_ns();
670 let cache = core.cache();
671
672 let open_orders = cache.orders_open(
673 None,
674 Some(&instrument_id),
675 Some(&strategy_id),
676 None,
677 order_side,
678 );
679
680 let emulated_orders = cache.orders_emulated(
681 None,
682 Some(&instrument_id),
683 Some(&strategy_id),
684 None,
685 order_side,
686 );
687
688 let inflight_orders = cache.orders_inflight(
689 None,
690 Some(&instrument_id),
691 Some(&strategy_id),
692 None,
693 order_side,
694 );
695
696 let exec_algorithm_ids = cache.exec_algorithm_ids();
697 let mut algo_orders = Vec::new();
698
699 for algo_id in &exec_algorithm_ids {
700 let orders = cache.orders_for_exec_algorithm(
701 algo_id,
702 None,
703 Some(&instrument_id),
704 Some(&strategy_id),
705 None,
706 order_side,
707 );
708 algo_orders.extend(orders.iter().map(|o| (*o).clone()));
709 }
710
711 let open_count = open_orders.len();
712 let emulated_count = emulated_orders.len();
713 let inflight_count = inflight_orders.len();
714 let algo_count = algo_orders.len();
715
716 drop(cache);
717
718 if open_count == 0 && emulated_count == 0 && inflight_count == 0 && algo_count == 0 {
719 let side_str = order_side.map(|s| format!(" {s}")).unwrap_or_default();
720 log::info!("No {instrument_id} open, emulated, or inflight{side_str} orders to cancel");
721 return Ok(());
722 }
723
724 let manager = core.order_manager();
725
726 let side_str = order_side.map(|s| format!(" {s}")).unwrap_or_default();
727
728 if open_count > 0 {
729 log::info!(
730 "Canceling {open_count} open{side_str} {instrument_id} order{}",
731 if open_count == 1 { "" } else { "s" }
732 );
733 }
734
735 if emulated_count > 0 {
736 log::info!(
737 "Canceling {emulated_count} emulated{side_str} {instrument_id} order{}",
738 if emulated_count == 1 { "" } else { "s" }
739 );
740 }
741
742 if inflight_count > 0 {
743 log::info!(
744 "Canceling {inflight_count} inflight{side_str} {instrument_id} order{}",
745 if inflight_count == 1 { "" } else { "s" }
746 );
747 }
748
749 if open_count > 0 || inflight_count > 0 {
750 let command = CancelAllOrders::new(
751 trader_id,
752 client_id,
753 strategy_id,
754 instrument_id,
755 order_side.unwrap_or(OrderSide::NoOrderSide),
756 UUID4::new(),
757 ts_init,
758 params.clone(),
759 );
760
761 manager.send_exec_command(TradingCommand::CancelAllOrders(command));
762 }
763
764 if emulated_count > 0 {
765 let command = CancelAllOrders::new(
766 trader_id,
767 client_id,
768 strategy_id,
769 instrument_id,
770 order_side.unwrap_or(OrderSide::NoOrderSide),
771 UUID4::new(),
772 ts_init,
773 params,
774 );
775
776 manager.send_emulator_command(TradingCommand::CancelAllOrders(command));
777 }
778
779 for order in algo_orders {
780 self.cancel_order(order, client_id)?;
781 }
782
783 Ok(())
784 }
785
786 fn close_position(
792 &mut self,
793 position: &Position,
794 client_id: Option<ClientId>,
795 tags: Option<Vec<Ustr>>,
796 time_in_force: Option<TimeInForce>,
797 reduce_only: Option<bool>,
798 quote_quantity: Option<bool>,
799 ) -> anyhow::Result<()> {
800 let core = self.core_mut();
801
802 if position.is_closed() {
803 log::warn!("Cannot close position (already closed): {}", position.id);
804 return Ok(());
805 }
806
807 let closing_side = OrderCore::closing_side(position.side);
808
809 let order = core.order_factory().market(
810 position.instrument_id,
811 closing_side,
812 position.quantity,
813 time_in_force,
814 reduce_only.or(Some(true)),
815 quote_quantity,
816 None,
817 None,
818 tags,
819 None,
820 );
821
822 self.submit_order(order, Some(position.id), client_id)
823 }
824
825 #[allow(clippy::too_many_arguments)]
831 fn close_all_positions(
832 &mut self,
833 instrument_id: InstrumentId,
834 position_side: Option<PositionSide>,
835 client_id: Option<ClientId>,
836 tags: Option<Vec<Ustr>>,
837 time_in_force: Option<TimeInForce>,
838 reduce_only: Option<bool>,
839 quote_quantity: Option<bool>,
840 ) -> anyhow::Result<()> {
841 let core = self.core_mut();
842 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
843 let cache = core.cache();
844
845 let positions_open = cache.positions_open(
846 None,
847 Some(&instrument_id),
848 Some(&strategy_id),
849 None,
850 position_side,
851 );
852
853 let side_str = position_side.map(|s| format!(" {s}")).unwrap_or_default();
854
855 if positions_open.is_empty() {
856 log::info!("No {instrument_id} open{side_str} positions to close");
857 return Ok(());
858 }
859
860 let count = positions_open.len();
861 log::info!(
862 "Closing {count} open{side_str} position{}",
863 if count == 1 { "" } else { "s" }
864 );
865
866 let positions_data: Vec<_> = positions_open
867 .iter()
868 .map(|p| (p.id, p.instrument_id, p.side, p.quantity, p.is_closed()))
869 .collect();
870
871 drop(cache);
872
873 for (pos_id, pos_instrument_id, pos_side, pos_quantity, is_closed) in positions_data {
874 if is_closed {
875 continue;
876 }
877
878 let core = self.core_mut();
879 let closing_side = OrderCore::closing_side(pos_side);
880 let order = core.order_factory().market(
881 pos_instrument_id,
882 closing_side,
883 pos_quantity,
884 time_in_force,
885 reduce_only.or(Some(true)),
886 quote_quantity,
887 None,
888 None,
889 tags.clone(),
890 None,
891 );
892
893 self.submit_order(order, Some(pos_id), client_id)?;
894 }
895
896 Ok(())
897 }
898
899 fn query_account(
908 &mut self,
909 account_id: AccountId,
910 client_id: Option<ClientId>,
911 ) -> anyhow::Result<()> {
912 let core = self.core_mut();
913
914 let trader_id = core.trader_id().expect("Trader ID not set");
915 let ts_init = core.clock().timestamp_ns();
916
917 let command = QueryAccount::new(trader_id, client_id, account_id, UUID4::new(), ts_init);
918
919 core.order_manager()
920 .send_exec_command(TradingCommand::QueryAccount(command));
921 Ok(())
922 }
923
924 fn query_order(&mut self, order: &OrderAny, client_id: Option<ClientId>) -> anyhow::Result<()> {
933 let core = self.core_mut();
934
935 let trader_id = core.trader_id().expect("Trader ID not set");
936 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
937 let ts_init = core.clock().timestamp_ns();
938
939 let command = QueryOrder::new(
940 trader_id,
941 client_id,
942 strategy_id,
943 order.instrument_id(),
944 order.client_order_id(),
945 order.venue_order_id(),
946 UUID4::new(),
947 ts_init,
948 );
949
950 core.order_manager()
951 .send_exec_command(TradingCommand::QueryOrder(command));
952 Ok(())
953 }
954
955 fn handle_order_event(&mut self, event: OrderEventAny) {
957 {
958 let core = self.core_mut();
959
960 if core.actor.state() != ComponentState::Running {
961 return;
962 }
963
964 let id = &core.actor.actor_id;
965 let is_warning = matches!(
966 &event,
967 OrderEventAny::Denied(_)
968 | OrderEventAny::Rejected(_)
969 | OrderEventAny::CancelRejected(_)
970 | OrderEventAny::ModifyRejected(_)
971 );
972
973 if is_warning {
974 log::warn!("{id} {RECV}{EVT} {event}");
975 } else if core.config.log_events {
976 log::info!("{id} {RECV}{EVT} {event}");
977 }
978 }
979
980 let client_order_id = event.client_order_id();
981 let is_terminal = matches!(
982 &event,
983 OrderEventAny::Filled(_)
984 | OrderEventAny::Canceled(_)
985 | OrderEventAny::Rejected(_)
986 | OrderEventAny::Expired(_)
987 | OrderEventAny::Denied(_)
988 );
989
990 match &event {
991 OrderEventAny::Initialized(e) => self.on_order_initialized(e.clone()),
992 OrderEventAny::Denied(e) => self.on_order_denied(*e),
993 OrderEventAny::Emulated(e) => self.on_order_emulated(*e),
994 OrderEventAny::Released(e) => self.on_order_released(*e),
995 OrderEventAny::Submitted(e) => self.on_order_submitted(*e),
996 OrderEventAny::Rejected(e) => self.on_order_rejected(*e),
997 OrderEventAny::Accepted(e) => self.on_order_accepted(*e),
998 OrderEventAny::Canceled(e) => {
999 let _ = DataActor::on_order_canceled(self, e);
1000 }
1001 OrderEventAny::Expired(e) => self.on_order_expired(*e),
1002 OrderEventAny::Triggered(e) => self.on_order_triggered(*e),
1003 OrderEventAny::PendingUpdate(e) => self.on_order_pending_update(*e),
1004 OrderEventAny::PendingCancel(e) => self.on_order_pending_cancel(*e),
1005 OrderEventAny::ModifyRejected(e) => self.on_order_modify_rejected(*e),
1006 OrderEventAny::CancelRejected(e) => self.on_order_cancel_rejected(*e),
1007 OrderEventAny::Updated(e) => self.on_order_updated(*e),
1008 OrderEventAny::Filled(e) => {
1009 let _ = DataActor::on_order_filled(self, e);
1010 }
1011 }
1012
1013 if is_terminal {
1014 self.cancel_gtd_expiry(&client_order_id);
1015 }
1016
1017 let core = self.core_mut();
1018 if let Some(manager) = &mut core.order_manager {
1019 manager.handle_event(&event);
1020 }
1021 }
1022
1023 fn handle_position_event(&mut self, event: PositionEvent) {
1025 {
1026 let core = self.core_mut();
1027
1028 if core.actor.state() != ComponentState::Running {
1029 return;
1030 }
1031
1032 if core.config.log_events {
1033 let id = &core.actor.actor_id;
1034 log::info!("{id} {RECV}{EVT} {event:?}");
1035 }
1036 }
1037
1038 match event {
1039 PositionEvent::PositionOpened(e) => self.on_position_opened(e),
1040 PositionEvent::PositionChanged(e) => self.on_position_changed(e),
1041 PositionEvent::PositionClosed(e) => self.on_position_closed(e),
1042 PositionEvent::PositionAdjusted(_) => {
1043 }
1045 }
1046 }
1047
1048 fn on_start(&mut self) -> anyhow::Result<()> {
1059 let core = self.core_mut();
1060 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
1061 log::info!("Starting {strategy_id}");
1062
1063 if core.config.manage_gtd_expiry {
1064 self.reactivate_gtd_timers();
1065 }
1066
1067 Ok(())
1068 }
1069
1070 fn on_time_event(&mut self, event: &TimeEvent) -> anyhow::Result<()> {
1079 if event.name.starts_with("GTD-EXPIRY:") {
1080 self.expire_gtd_order(event.clone());
1081 } else if event.name.starts_with("MARKET_EXIT_CHECK:") {
1082 self.check_market_exit(event.clone());
1083 }
1084 Ok(())
1085 }
1086
1087 #[allow(unused_variables)]
1093 fn on_order_initialized(&mut self, event: OrderInitialized) {}
1094
1095 #[allow(unused_variables)]
1099 fn on_order_denied(&mut self, event: OrderDenied) {}
1100
1101 #[allow(unused_variables)]
1105 fn on_order_emulated(&mut self, event: OrderEmulated) {}
1106
1107 #[allow(unused_variables)]
1111 fn on_order_released(&mut self, event: OrderReleased) {}
1112
1113 #[allow(unused_variables)]
1117 fn on_order_submitted(&mut self, event: OrderSubmitted) {}
1118
1119 #[allow(unused_variables)]
1123 fn on_order_rejected(&mut self, event: OrderRejected) {}
1124
1125 #[allow(unused_variables)]
1129 fn on_order_accepted(&mut self, event: OrderAccepted) {}
1130
1131 #[allow(unused_variables)]
1135 fn on_order_expired(&mut self, event: OrderExpired) {}
1136
1137 #[allow(unused_variables)]
1141 fn on_order_triggered(&mut self, event: OrderTriggered) {}
1142
1143 #[allow(unused_variables)]
1147 fn on_order_pending_update(&mut self, event: OrderPendingUpdate) {}
1148
1149 #[allow(unused_variables)]
1153 fn on_order_pending_cancel(&mut self, event: OrderPendingCancel) {}
1154
1155 #[allow(unused_variables)]
1159 fn on_order_modify_rejected(&mut self, event: OrderModifyRejected) {}
1160
1161 #[allow(unused_variables)]
1165 fn on_order_cancel_rejected(&mut self, event: OrderCancelRejected) {}
1166
1167 #[allow(unused_variables)]
1171 fn on_order_updated(&mut self, event: OrderUpdated) {}
1172
1173 #[allow(unused_variables)]
1179 fn on_position_opened(&mut self, event: PositionOpened) {}
1180
1181 #[allow(unused_variables)]
1185 fn on_position_changed(&mut self, event: PositionChanged) {}
1186
1187 #[allow(unused_variables)]
1191 fn on_position_closed(&mut self, event: PositionClosed) {}
1192
1193 fn on_market_exit(&mut self) {}
1197
1198 fn post_market_exit(&mut self) {}
1202
1203 fn is_exiting(&self) -> bool {
1207 self.core().is_exiting
1208 }
1209
1210 fn market_exit(&mut self) -> anyhow::Result<()> {
1226 let core = self.core_mut();
1227 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
1228
1229 if core.actor.state() != ComponentState::Running {
1230 log::warn!("{strategy_id} Cannot market exit: strategy is not running");
1231 return Ok(());
1232 }
1233
1234 if core.is_exiting {
1235 log::warn!("{strategy_id} Market exit called when already in progress");
1236 return Ok(());
1237 }
1238
1239 core.is_exiting = true;
1240 core.market_exit_attempts = 0;
1241 let time_in_force = core.config.market_exit_time_in_force;
1242 let reduce_only = core.config.market_exit_reduce_only;
1243
1244 log::info!("{strategy_id} Initiating market exit...");
1245
1246 self.on_market_exit();
1247
1248 let core = self.core_mut();
1249 let cache = core.cache();
1250
1251 let open_orders = cache.orders_open(None, None, Some(&strategy_id), None, None);
1252 let inflight_orders = cache.orders_inflight(None, None, Some(&strategy_id), None, None);
1253 let open_positions = cache.positions_open(None, None, Some(&strategy_id), None, None);
1254
1255 let mut instruments: AHashSet<InstrumentId> = AHashSet::new();
1256
1257 for order in &open_orders {
1258 instruments.insert(order.instrument_id());
1259 }
1260 for order in &inflight_orders {
1261 instruments.insert(order.instrument_id());
1262 }
1263 for position in &open_positions {
1264 instruments.insert(position.instrument_id);
1265 }
1266
1267 let market_exit_tag = core.market_exit_tag;
1268 let instruments: Vec<_> = instruments.into_iter().collect();
1269 drop(cache);
1270
1271 for instrument_id in instruments {
1272 if let Err(e) = self.cancel_all_orders(instrument_id, None, None) {
1273 log::error!("Error canceling orders for {instrument_id}: {e}");
1274 }
1275
1276 if let Err(e) = self.close_all_positions(
1277 instrument_id,
1278 None,
1279 None,
1280 Some(vec![market_exit_tag]),
1281 Some(time_in_force),
1282 Some(reduce_only),
1283 None,
1284 ) {
1285 log::error!("Error closing positions for {instrument_id}: {e}");
1286 }
1287 }
1288
1289 let core = self.core_mut();
1290 let interval_ms = core.config.market_exit_interval_ms;
1291 let timer_name = core.market_exit_timer_name;
1292
1293 log::info!("{strategy_id} Setting market exit timer at {interval_ms}ms intervals");
1294
1295 let interval_ns = interval_ms * 1_000_000;
1296 let result = core.clock().set_timer_ns(
1297 timer_name.as_str(),
1298 interval_ns,
1299 None,
1300 None,
1301 None,
1302 None,
1303 None,
1304 );
1305
1306 if let Err(e) = result {
1307 core.is_exiting = false;
1309 core.market_exit_attempts = 0;
1310 return Err(e);
1311 }
1312
1313 Ok(())
1314 }
1315
1316 fn check_market_exit(&mut self, _event: TimeEvent) {
1320 if !self.is_exiting() {
1322 return;
1323 }
1324
1325 let core = self.core_mut();
1326 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
1327
1328 core.market_exit_attempts += 1;
1329 let attempts = core.market_exit_attempts;
1330 let max_attempts = core.config.market_exit_max_attempts;
1331
1332 log::debug!(
1333 "{strategy_id} Market exit check triggered (attempt {attempts}/{max_attempts})"
1334 );
1335
1336 if attempts >= max_attempts {
1337 let cache = core.cache();
1338 let open_orders_count = cache
1339 .orders_open(None, None, Some(&strategy_id), None, None)
1340 .len();
1341 let inflight_orders_count = cache
1342 .orders_inflight(None, None, Some(&strategy_id), None, None)
1343 .len();
1344 let open_positions_count = cache
1345 .positions_open(None, None, Some(&strategy_id), None, None)
1346 .len();
1347 drop(cache);
1348
1349 log::warn!(
1350 "{strategy_id} Market exit max attempts ({max_attempts}) reached, \
1351 completing with open orders: {open_orders_count}, \
1352 inflight orders: {inflight_orders_count}, \
1353 open positions: {open_positions_count}"
1354 );
1355
1356 self.finalize_market_exit();
1357 return;
1358 }
1359
1360 let cache = core.cache();
1361 let open_orders = cache.orders_open(None, None, Some(&strategy_id), None, None);
1362 let inflight_orders = cache.orders_inflight(None, None, Some(&strategy_id), None, None);
1363
1364 if !open_orders.is_empty() || !inflight_orders.is_empty() {
1365 return;
1366 }
1367
1368 let open_positions = cache.positions_open(None, None, Some(&strategy_id), None, None);
1369
1370 if !open_positions.is_empty() {
1371 let positions_data: Vec<_> = open_positions
1373 .iter()
1374 .map(|p| (p.id, p.instrument_id, p.side, p.quantity, p.is_closed()))
1375 .collect();
1376
1377 drop(cache);
1378
1379 for (pos_id, instrument_id, side, quantity, is_closed) in positions_data {
1380 if is_closed {
1381 continue;
1382 }
1383
1384 let core = self.core_mut();
1385 let time_in_force = core.config.market_exit_time_in_force;
1386 let reduce_only = core.config.market_exit_reduce_only;
1387 let market_exit_tag = core.market_exit_tag;
1388 let closing_side = OrderCore::closing_side(side);
1389 let order = core.order_factory().market(
1390 instrument_id,
1391 closing_side,
1392 quantity,
1393 Some(time_in_force),
1394 Some(reduce_only),
1395 None,
1396 None,
1397 None,
1398 Some(vec![market_exit_tag]),
1399 None,
1400 );
1401
1402 if let Err(e) = self.submit_order(order, Some(pos_id), None) {
1403 log::error!("Error re-submitting close order for position {pos_id}: {e}");
1404 }
1405 }
1406 return;
1407 }
1408
1409 drop(cache);
1410 self.finalize_market_exit();
1411 }
1412
1413 fn finalize_market_exit(&mut self) {
1418 let (strategy_id, should_stop) = {
1419 let core = self.core_mut();
1420 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
1421 let should_stop = core.pending_stop;
1422 (strategy_id, should_stop)
1423 };
1424
1425 self.cancel_market_exit();
1426
1427 let hook_result = catch_unwind(AssertUnwindSafe(|| {
1428 self.post_market_exit();
1429 }));
1430
1431 if let Err(e) = hook_result {
1432 log::error!("{strategy_id} Error in post_market_exit: {e:?}");
1433 }
1434
1435 if should_stop {
1436 log::info!("{strategy_id} Market exit complete, stopping strategy");
1437
1438 if let Err(e) = Component::stop(self) {
1439 log::error!("{strategy_id} Failed to stop: {e}");
1440 }
1441 }
1442
1443 let core = self.core_mut();
1444 debug_assert!(
1445 !(core.pending_stop
1446 && !core.is_exiting
1447 && core.actor.state() == ComponentState::Running),
1448 "INVARIANT: stuck state after finalize_market_exit"
1449 );
1450 }
1451
1452 fn cancel_market_exit(&mut self) {
1456 let core = self.core_mut();
1457 let timer_name = core.market_exit_timer_name;
1458
1459 if core.clock().timer_names().contains(&timer_name.as_str()) {
1460 core.clock().cancel_timer(timer_name.as_str());
1461 }
1462
1463 core.is_exiting = false;
1464 core.pending_stop = false;
1465 core.market_exit_attempts = 0;
1466 }
1467
1468 fn stop(&mut self) -> bool {
1480 let (manage_stop, is_exiting, should_initiate_exit) = {
1481 let core = self.core_mut();
1482 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
1483 let manage_stop = core.config.manage_stop;
1484 let state = core.actor.state();
1485 let pending_stop = core.pending_stop;
1486 let is_exiting = core.is_exiting;
1487
1488 if manage_stop {
1489 if state != ComponentState::Running {
1490 return true; }
1492
1493 if pending_stop {
1494 return false; }
1496
1497 core.pending_stop = true;
1498 let should_initiate_exit = !is_exiting;
1499
1500 if should_initiate_exit {
1501 log::info!("{strategy_id} Initiating market exit before stop");
1502 }
1503
1504 (manage_stop, is_exiting, should_initiate_exit)
1505 } else {
1506 (manage_stop, is_exiting, false)
1507 }
1508 };
1509
1510 if manage_stop {
1511 if should_initiate_exit && let Err(e) = self.market_exit() {
1512 log::warn!("Market exit failed during stop: {e}, proceeding with stop");
1513 self.core_mut().pending_stop = false;
1514 return true;
1515 }
1516 debug_assert!(
1517 self.is_exiting(),
1518 "INVARIANT: deferring stop but not exiting"
1519 );
1520 return false; }
1522
1523 if is_exiting {
1525 self.cancel_market_exit();
1526 }
1527
1528 true }
1530
1531 fn deny_order(&mut self, order: &OrderAny, reason: Ustr) {
1536 let core = self.core_mut();
1537 let trader_id = core.trader_id().expect("Trader ID not set");
1538 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
1539 let ts_now = core.clock().timestamp_ns();
1540
1541 let event = OrderDenied::new(
1542 trader_id,
1543 strategy_id,
1544 order.instrument_id(),
1545 order.client_order_id(),
1546 reason,
1547 UUID4::new(),
1548 ts_now,
1549 ts_now,
1550 );
1551
1552 log::warn!(
1553 "{strategy_id} Order {} denied: {reason}",
1554 order.client_order_id()
1555 );
1556
1557 {
1559 let cache_rc = core.cache_rc();
1560 let mut cache = cache_rc.borrow_mut();
1561 if !cache.order_exists(&order.client_order_id()) {
1562 let _ = cache.add_order(order.clone(), None, None, true);
1563 }
1564 }
1565
1566 let mut order_clone = order.clone();
1568 if let Err(e) = order_clone.apply(OrderEventAny::Denied(event)) {
1569 log::warn!("Failed to apply OrderDenied event: {e}");
1570 return;
1571 }
1572
1573 {
1574 let cache_rc = core.cache_rc();
1575 let mut cache = cache_rc.borrow_mut();
1576 let _ = cache.update_order(&order_clone);
1577 }
1578 }
1579
1580 fn deny_order_list(&mut self, orders: &[OrderAny], reason: Ustr) {
1584 for order in orders {
1585 if !order.is_closed() {
1586 self.deny_order(order, reason);
1587 }
1588 }
1589 }
1590
1591 fn set_gtd_expiry(&mut self, order: &OrderAny) -> anyhow::Result<()> {
1601 let core = self.core_mut();
1602
1603 if !core.config.manage_gtd_expiry || order.time_in_force() != TimeInForce::Gtd {
1604 return Ok(());
1605 }
1606
1607 let Some(expire_time) = order.expire_time() else {
1608 return Ok(());
1609 };
1610
1611 let client_order_id = order.client_order_id();
1612 let timer_name = format!("GTD-EXPIRY:{client_order_id}");
1613
1614 let current_time_ns = {
1615 let clock = core.clock();
1616 clock.timestamp_ns()
1617 };
1618
1619 if current_time_ns >= expire_time.as_u64() {
1620 log::info!("GTD order {client_order_id} already expired, canceling immediately");
1621 return self.cancel_order(order.clone(), None);
1622 }
1623
1624 {
1625 let mut clock = core.clock();
1626 clock.set_time_alert_ns(&timer_name, expire_time, None, None)?;
1627 }
1628
1629 core.gtd_timers
1630 .insert(client_order_id, Ustr::from(&timer_name));
1631
1632 log::debug!("Set GTD expiry timer for {client_order_id} at {expire_time}");
1633 Ok(())
1634 }
1635
1636 fn cancel_gtd_expiry(&mut self, client_order_id: &ClientOrderId) {
1638 let core = self.core_mut();
1639
1640 if let Some(timer_name) = core.gtd_timers.remove(client_order_id) {
1641 core.clock().cancel_timer(timer_name.as_str());
1642 log::debug!("Canceled GTD expiry timer for {client_order_id}");
1643 }
1644 }
1645
1646 fn has_gtd_expiry_timer(&mut self, client_order_id: &ClientOrderId) -> bool {
1648 let core = self.core_mut();
1649 core.gtd_timers.contains_key(client_order_id)
1650 }
1651
1652 fn expire_gtd_order(&mut self, event: TimeEvent) {
1656 let timer_name = event.name.to_string();
1657 let Some(client_order_id_str) = timer_name.strip_prefix("GTD-EXPIRY:") else {
1658 log::error!("Invalid GTD timer name format: {timer_name}");
1659 return;
1660 };
1661
1662 let client_order_id = ClientOrderId::from(client_order_id_str);
1663
1664 let core = self.core_mut();
1665 core.gtd_timers.remove(&client_order_id);
1666
1667 let cache = core.cache();
1668 let Some(order) = cache.order(&client_order_id) else {
1669 log::warn!("GTD order {client_order_id} not found in cache");
1670 return;
1671 };
1672
1673 let order = order.clone();
1674 drop(cache);
1675
1676 log::info!("GTD order {client_order_id} expired");
1677
1678 if let Err(e) = self.cancel_order(order, None) {
1679 log::error!("Failed to cancel expired GTD order {client_order_id}: {e}");
1680 }
1681 }
1682
1683 fn reactivate_gtd_timers(&mut self) {
1688 let core = self.core_mut();
1689 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
1690 let current_time_ns = core.clock().timestamp_ns();
1691 let cache = core.cache();
1692
1693 let open_orders = cache.orders_open(None, None, Some(&strategy_id), None, None);
1694
1695 let gtd_orders: Vec<_> = open_orders
1696 .iter()
1697 .filter(|o| o.time_in_force() == TimeInForce::Gtd)
1698 .map(|o| (*o).clone())
1699 .collect();
1700
1701 drop(cache);
1702
1703 for order in gtd_orders {
1704 let Some(expire_time) = order.expire_time() else {
1705 continue;
1706 };
1707
1708 let expire_time_ns = expire_time.as_u64();
1709 let client_order_id = order.client_order_id();
1710
1711 if current_time_ns >= expire_time_ns {
1712 log::info!("GTD order {client_order_id} already expired, canceling immediately");
1713 if let Err(e) = self.cancel_order(order, None) {
1714 log::error!("Failed to cancel expired GTD order {client_order_id}: {e}");
1715 }
1716 } else if let Err(e) = self.set_gtd_expiry(&order) {
1717 log::error!("Failed to set GTD expiry timer for {client_order_id}: {e}");
1718 }
1719 }
1720 }
1721}
1722
1723#[cfg(test)]
1724mod tests {
1725 use std::{cell::RefCell, rc::Rc};
1726
1727 use nautilus_common::{
1728 actor::DataActor,
1729 cache::Cache,
1730 clock::{Clock, TestClock},
1731 component::Component,
1732 timer::{TimeEvent, TimeEventCallback},
1733 };
1734 use nautilus_core::UnixNanos;
1735 use nautilus_model::{
1736 enums::{LiquiditySide, OrderSide, OrderType, PositionSide},
1737 events::{OrderCanceled, OrderFilled, OrderRejected},
1738 identifiers::{
1739 AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, TraderId,
1740 VenueOrderId,
1741 },
1742 orders::MarketOrder,
1743 stubs::TestDefault,
1744 types::Currency,
1745 };
1746 use nautilus_portfolio::portfolio::Portfolio;
1747 use rstest::rstest;
1748
1749 use super::*;
1750 use crate::nautilus_strategy;
1751
1752 #[derive(Debug)]
1753 struct TestStrategy {
1754 core: StrategyCore,
1755 on_order_rejected_called: bool,
1756 on_position_opened_called: bool,
1757 }
1758
1759 impl TestStrategy {
1760 fn new(config: StrategyConfig) -> Self {
1761 Self {
1762 core: StrategyCore::new(config),
1763 on_order_rejected_called: false,
1764 on_position_opened_called: false,
1765 }
1766 }
1767 }
1768
1769 impl DataActor for TestStrategy {}
1770
1771 nautilus_strategy!(TestStrategy, {
1772 fn on_order_rejected(&mut self, _event: OrderRejected) {
1773 self.on_order_rejected_called = true;
1774 }
1775
1776 fn on_position_opened(&mut self, _event: PositionOpened) {
1777 self.on_position_opened_called = true;
1778 }
1779 });
1780
1781 fn create_test_strategy() -> TestStrategy {
1782 let config = StrategyConfig {
1783 strategy_id: Some(StrategyId::from("TEST-001")),
1784 order_id_tag: Some("001".to_string()),
1785 ..Default::default()
1786 };
1787 TestStrategy::new(config)
1788 }
1789
1790 fn register_strategy(strategy: &mut TestStrategy) {
1791 let trader_id = TraderId::from("TRADER-001");
1792 let clock = Rc::new(RefCell::new(TestClock::new()));
1793 let cache = Rc::new(RefCell::new(Cache::default()));
1794 let portfolio = Rc::new(RefCell::new(Portfolio::new(
1795 cache.clone(),
1796 clock.clone(),
1797 None,
1798 )));
1799
1800 strategy
1801 .core
1802 .register(trader_id, clock, cache, portfolio)
1803 .unwrap();
1804 strategy.initialize().unwrap();
1805 }
1806
1807 fn start_strategy(strategy: &mut TestStrategy) {
1808 strategy.start().unwrap();
1809 }
1810
1811 #[rstest]
1812 fn test_strategy_creation() {
1813 let strategy = create_test_strategy();
1814 assert_eq!(
1815 strategy.core.config.strategy_id,
1816 Some(StrategyId::from("TEST-001"))
1817 );
1818 assert!(!strategy.on_order_rejected_called);
1819 assert!(!strategy.on_position_opened_called);
1820 }
1821
1822 #[rstest]
1823 fn test_strategy_registration() {
1824 let mut strategy = create_test_strategy();
1825 register_strategy(&mut strategy);
1826
1827 assert!(strategy.core.order_manager.is_some());
1828 assert!(strategy.core.order_factory.is_some());
1829 assert!(strategy.core.portfolio.is_some());
1830 }
1831
1832 #[rstest]
1833 fn test_handle_order_event_dispatches_to_handler() {
1834 let mut strategy = create_test_strategy();
1835 register_strategy(&mut strategy);
1836 start_strategy(&mut strategy);
1837
1838 let event = OrderEventAny::Rejected(OrderRejected {
1839 trader_id: TraderId::from("TRADER-001"),
1840 strategy_id: StrategyId::from("TEST-001"),
1841 instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
1842 client_order_id: ClientOrderId::from("O-001"),
1843 account_id: AccountId::from("ACC-001"),
1844 reason: "Test rejection".into(),
1845 event_id: UUID4::default(),
1846 ts_event: UnixNanos::default(),
1847 ts_init: UnixNanos::default(),
1848 reconciliation: 0,
1849 due_post_only: 0,
1850 });
1851
1852 strategy.handle_order_event(event);
1853
1854 assert!(strategy.on_order_rejected_called);
1855 }
1856
1857 #[rstest]
1858 fn test_handle_position_event_dispatches_to_handler() {
1859 let mut strategy = create_test_strategy();
1860 register_strategy(&mut strategy);
1861 start_strategy(&mut strategy);
1862
1863 let event = PositionEvent::PositionOpened(PositionOpened {
1864 trader_id: TraderId::from("TRADER-001"),
1865 strategy_id: StrategyId::from("TEST-001"),
1866 instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
1867 position_id: PositionId::test_default(),
1868 account_id: AccountId::from("ACC-001"),
1869 opening_order_id: ClientOrderId::from("O-001"),
1870 entry: OrderSide::Buy,
1871 side: PositionSide::Long,
1872 signed_qty: 1.0,
1873 quantity: Quantity::default(),
1874 last_qty: Quantity::default(),
1875 last_px: Price::default(),
1876 currency: Currency::from("USD"),
1877 avg_px_open: 0.0,
1878 event_id: UUID4::default(),
1879 ts_event: UnixNanos::default(),
1880 ts_init: UnixNanos::default(),
1881 });
1882
1883 strategy.handle_position_event(event);
1884
1885 assert!(strategy.on_position_opened_called);
1886 }
1887
1888 #[rstest]
1889 fn test_strategy_default_handlers_do_not_panic() {
1890 let mut strategy = create_test_strategy();
1891
1892 strategy.on_order_initialized(OrderInitialized::default());
1893 strategy.on_order_denied(OrderDenied::default());
1894 strategy.on_order_emulated(OrderEmulated::default());
1895 strategy.on_order_released(OrderReleased::default());
1896 strategy.on_order_submitted(OrderSubmitted::default());
1897 strategy.on_order_rejected(OrderRejected::default());
1898 let _ = DataActor::on_order_canceled(&mut strategy, &OrderCanceled::default());
1899 strategy.on_order_expired(OrderExpired::default());
1900 strategy.on_order_triggered(OrderTriggered::default());
1901 strategy.on_order_pending_update(OrderPendingUpdate::default());
1902 strategy.on_order_pending_cancel(OrderPendingCancel::default());
1903 strategy.on_order_modify_rejected(OrderModifyRejected::default());
1904 strategy.on_order_cancel_rejected(OrderCancelRejected::default());
1905 strategy.on_order_updated(OrderUpdated::default());
1906 }
1907
1908 #[rstest]
1911 fn test_has_gtd_expiry_timer_when_timer_not_set() {
1912 let mut strategy = create_test_strategy();
1913 let client_order_id = ClientOrderId::from("O-001");
1914
1915 assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
1916 }
1917
1918 #[rstest]
1919 fn test_has_gtd_expiry_timer_when_timer_set() {
1920 let mut strategy = create_test_strategy();
1921 let client_order_id = ClientOrderId::from("O-001");
1922
1923 strategy
1924 .core
1925 .gtd_timers
1926 .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
1927
1928 assert!(strategy.has_gtd_expiry_timer(&client_order_id));
1929 }
1930
1931 #[rstest]
1932 fn test_cancel_gtd_expiry_removes_timer() {
1933 let mut strategy = create_test_strategy();
1934 register_strategy(&mut strategy);
1935
1936 let client_order_id = ClientOrderId::from("O-001");
1937 strategy
1938 .core
1939 .gtd_timers
1940 .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
1941
1942 strategy.cancel_gtd_expiry(&client_order_id);
1943
1944 assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
1945 }
1946
1947 #[rstest]
1948 fn test_cancel_gtd_expiry_when_timer_not_set() {
1949 let mut strategy = create_test_strategy();
1950 register_strategy(&mut strategy);
1951
1952 let client_order_id = ClientOrderId::from("O-001");
1953
1954 strategy.cancel_gtd_expiry(&client_order_id);
1955
1956 assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
1957 }
1958
1959 #[rstest]
1960 fn test_handle_order_event_cancels_gtd_timer_on_filled() {
1961 let mut strategy = create_test_strategy();
1962 register_strategy(&mut strategy);
1963 start_strategy(&mut strategy);
1964
1965 let client_order_id = ClientOrderId::from("O-001");
1966 strategy
1967 .core
1968 .gtd_timers
1969 .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
1970
1971 let event = OrderEventAny::Filled(OrderFilled {
1972 trader_id: TraderId::from("TRADER-001"),
1973 strategy_id: StrategyId::from("TEST-001"),
1974 instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
1975 client_order_id,
1976 venue_order_id: VenueOrderId::test_default(),
1977 account_id: AccountId::from("ACC-001"),
1978 trade_id: TradeId::test_default(),
1979 position_id: None,
1980 order_side: OrderSide::Buy,
1981 order_type: OrderType::Market,
1982 last_qty: Quantity::default(),
1983 last_px: Price::default(),
1984 currency: Currency::from("USD"),
1985 liquidity_side: LiquiditySide::Taker,
1986 event_id: UUID4::default(),
1987 ts_event: UnixNanos::default(),
1988 ts_init: UnixNanos::default(),
1989 reconciliation: false,
1990 commission: None,
1991 });
1992 strategy.handle_order_event(event);
1993
1994 assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
1995 }
1996
1997 #[rstest]
1998 fn test_handle_order_event_cancels_gtd_timer_on_canceled() {
1999 let mut strategy = create_test_strategy();
2000 register_strategy(&mut strategy);
2001 start_strategy(&mut strategy);
2002
2003 let client_order_id = ClientOrderId::from("O-001");
2004 strategy
2005 .core
2006 .gtd_timers
2007 .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
2008
2009 let event = OrderEventAny::Canceled(OrderCanceled {
2010 trader_id: TraderId::from("TRADER-001"),
2011 strategy_id: StrategyId::from("TEST-001"),
2012 instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
2013 client_order_id,
2014 venue_order_id: Option::default(),
2015 account_id: Some(AccountId::from("ACC-001")),
2016 event_id: UUID4::default(),
2017 ts_event: UnixNanos::default(),
2018 ts_init: UnixNanos::default(),
2019 reconciliation: 0,
2020 });
2021 strategy.handle_order_event(event);
2022
2023 assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
2024 }
2025
2026 #[rstest]
2027 fn test_handle_order_event_cancels_gtd_timer_on_rejected() {
2028 let mut strategy = create_test_strategy();
2029 register_strategy(&mut strategy);
2030 start_strategy(&mut strategy);
2031
2032 let client_order_id = ClientOrderId::from("O-001");
2033 strategy
2034 .core
2035 .gtd_timers
2036 .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
2037
2038 let event = OrderEventAny::Rejected(OrderRejected {
2039 trader_id: TraderId::from("TRADER-001"),
2040 strategy_id: StrategyId::from("TEST-001"),
2041 instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
2042 client_order_id,
2043 account_id: AccountId::from("ACC-001"),
2044 reason: "Test rejection".into(),
2045 event_id: UUID4::default(),
2046 ts_event: UnixNanos::default(),
2047 ts_init: UnixNanos::default(),
2048 reconciliation: 0,
2049 due_post_only: 0,
2050 });
2051 strategy.handle_order_event(event);
2052
2053 assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
2054 }
2055
2056 #[rstest]
2057 fn test_handle_order_event_cancels_gtd_timer_on_expired() {
2058 let mut strategy = create_test_strategy();
2059 register_strategy(&mut strategy);
2060 start_strategy(&mut strategy);
2061
2062 let client_order_id = ClientOrderId::from("O-001");
2063 strategy
2064 .core
2065 .gtd_timers
2066 .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
2067
2068 let event = OrderEventAny::Expired(OrderExpired {
2069 trader_id: TraderId::from("TRADER-001"),
2070 strategy_id: StrategyId::from("TEST-001"),
2071 instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
2072 client_order_id,
2073 venue_order_id: Option::default(),
2074 account_id: Some(AccountId::from("ACC-001")),
2075 event_id: UUID4::default(),
2076 ts_event: UnixNanos::default(),
2077 ts_init: UnixNanos::default(),
2078 reconciliation: 0,
2079 });
2080 strategy.handle_order_event(event);
2081
2082 assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
2083 }
2084
2085 #[rstest]
2086 fn test_on_start_calls_reactivate_gtd_timers_when_enabled() {
2087 let config = StrategyConfig {
2088 strategy_id: Some(StrategyId::from("TEST-001")),
2089 order_id_tag: Some("001".to_string()),
2090 manage_gtd_expiry: true,
2091 ..Default::default()
2092 };
2093 let mut strategy = TestStrategy::new(config);
2094 register_strategy(&mut strategy);
2095
2096 let result = Strategy::on_start(&mut strategy);
2097 assert!(result.is_ok());
2098 }
2099
2100 #[rstest]
2101 fn test_on_start_does_not_panic_when_gtd_disabled() {
2102 let config = StrategyConfig {
2103 strategy_id: Some(StrategyId::from("TEST-001")),
2104 order_id_tag: Some("001".to_string()),
2105 manage_gtd_expiry: false,
2106 ..Default::default()
2107 };
2108 let mut strategy = TestStrategy::new(config);
2109 register_strategy(&mut strategy);
2110
2111 let result = Strategy::on_start(&mut strategy);
2112 assert!(result.is_ok());
2113 }
2114
2115 #[rstest]
2118 fn test_query_account_when_registered() {
2119 let mut strategy = create_test_strategy();
2120 register_strategy(&mut strategy);
2121
2122 let account_id = AccountId::from("ACC-001");
2123
2124 let result = strategy.query_account(account_id, None);
2125
2126 assert!(result.is_ok());
2127 }
2128
2129 #[rstest]
2130 fn test_query_account_with_client_id() {
2131 let mut strategy = create_test_strategy();
2132 register_strategy(&mut strategy);
2133
2134 let account_id = AccountId::from("ACC-001");
2135 let client_id = ClientId::from("BINANCE");
2136
2137 let result = strategy.query_account(account_id, Some(client_id));
2138
2139 assert!(result.is_ok());
2140 }
2141
2142 #[rstest]
2143 fn test_query_order_when_registered() {
2144 let mut strategy = create_test_strategy();
2145 register_strategy(&mut strategy);
2146
2147 let order = OrderAny::Market(MarketOrder::test_default());
2148
2149 let result = strategy.query_order(&order, None);
2150
2151 assert!(result.is_ok());
2152 }
2153
2154 #[rstest]
2155 fn test_query_order_with_client_id() {
2156 let mut strategy = create_test_strategy();
2157 register_strategy(&mut strategy);
2158
2159 let order = OrderAny::Market(MarketOrder::test_default());
2160 let client_id = ClientId::from("BINANCE");
2161
2162 let result = strategy.query_order(&order, Some(client_id));
2163
2164 assert!(result.is_ok());
2165 }
2166
2167 #[rstest]
2168 fn test_is_exiting_returns_false_by_default() {
2169 let strategy = create_test_strategy();
2170 assert!(!strategy.is_exiting());
2171 }
2172
2173 #[rstest]
2174 fn test_is_exiting_returns_true_when_set_manually() {
2175 let mut strategy = create_test_strategy();
2176 register_strategy(&mut strategy);
2177
2178 strategy.core.is_exiting = true;
2180
2181 assert!(strategy.is_exiting());
2182 }
2183
2184 #[rstest]
2185 fn test_market_exit_sets_is_exiting_flag() {
2186 let mut strategy = create_test_strategy();
2188 register_strategy(&mut strategy);
2189
2190 assert!(!strategy.core.is_exiting);
2191
2192 strategy.core.is_exiting = true;
2194 strategy.core.market_exit_attempts = 0;
2195
2196 assert!(strategy.core.is_exiting);
2197 assert_eq!(strategy.core.market_exit_attempts, 0);
2198 }
2199
2200 #[rstest]
2201 fn test_market_exit_uses_config_time_in_force_and_reduce_only() {
2202 let config = StrategyConfig {
2203 strategy_id: Some(StrategyId::from("TEST-001")),
2204 order_id_tag: Some("001".to_string()),
2205 market_exit_time_in_force: TimeInForce::Ioc,
2206 market_exit_reduce_only: false,
2207 ..Default::default()
2208 };
2209 let strategy = TestStrategy::new(config);
2210
2211 assert_eq!(
2212 strategy.core.config.market_exit_time_in_force,
2213 TimeInForce::Ioc
2214 );
2215 assert!(!strategy.core.config.market_exit_reduce_only);
2216 }
2217
2218 #[rstest]
2219 fn test_market_exit_resets_attempt_counter() {
2220 let mut strategy = create_test_strategy();
2221 register_strategy(&mut strategy);
2222
2223 strategy.core.market_exit_attempts = 50;
2225
2226 strategy.core.reset_market_exit_state();
2228
2229 assert_eq!(strategy.core.market_exit_attempts, 0);
2230 }
2231
2232 #[rstest]
2233 fn test_market_exit_second_call_returns_early_when_exiting() {
2234 let mut strategy = create_test_strategy();
2235 register_strategy(&mut strategy);
2236
2237 strategy.core.is_exiting = true;
2239
2240 let result = strategy.market_exit();
2242 assert!(result.is_ok());
2243 assert!(strategy.core.is_exiting);
2244 }
2245
2246 #[rstest]
2247 fn test_finalize_market_exit_resets_state() {
2248 let mut strategy = create_test_strategy();
2249 register_strategy(&mut strategy);
2250
2251 strategy.core.is_exiting = true;
2253 strategy.core.pending_stop = true;
2254 strategy.core.market_exit_attempts = 50;
2255
2256 strategy.finalize_market_exit();
2257
2258 assert!(!strategy.core.is_exiting);
2259 assert!(!strategy.core.pending_stop);
2260 assert_eq!(strategy.core.market_exit_attempts, 0);
2261 }
2262
2263 #[rstest]
2264 fn test_market_exit_config_defaults() {
2265 let config = StrategyConfig::default();
2266
2267 assert!(!config.manage_stop);
2268 assert_eq!(config.market_exit_interval_ms, 100);
2269 assert_eq!(config.market_exit_max_attempts, 100);
2270 }
2271
2272 #[rstest]
2273 fn test_market_exit_with_custom_config() {
2274 let config = StrategyConfig {
2275 strategy_id: Some(StrategyId::from("TEST-001")),
2276 manage_stop: true,
2277 market_exit_interval_ms: 50,
2278 market_exit_max_attempts: 200,
2279 ..Default::default()
2280 };
2281 let strategy = TestStrategy::new(config);
2282
2283 assert!(strategy.core.config.manage_stop);
2284 assert_eq!(strategy.core.config.market_exit_interval_ms, 50);
2285 assert_eq!(strategy.core.config.market_exit_max_attempts, 200);
2286 }
2287
2288 #[derive(Debug)]
2289 struct MarketExitHookTrackingStrategy {
2290 core: StrategyCore,
2291 on_market_exit_called: bool,
2292 post_market_exit_called: bool,
2293 }
2294
2295 impl MarketExitHookTrackingStrategy {
2296 fn new(config: StrategyConfig) -> Self {
2297 Self {
2298 core: StrategyCore::new(config),
2299 on_market_exit_called: false,
2300 post_market_exit_called: false,
2301 }
2302 }
2303 }
2304
2305 impl DataActor for MarketExitHookTrackingStrategy {}
2306
2307 nautilus_strategy!(MarketExitHookTrackingStrategy, {
2308 fn on_market_exit(&mut self) {
2309 self.on_market_exit_called = true;
2310 }
2311
2312 fn post_market_exit(&mut self) {
2313 self.post_market_exit_called = true;
2314 }
2315 });
2316
2317 #[rstest]
2318 fn test_market_exit_calls_on_market_exit_hook() {
2319 let config = StrategyConfig {
2320 strategy_id: Some(StrategyId::from("TEST-001")),
2321 order_id_tag: Some("001".to_string()),
2322 ..Default::default()
2323 };
2324 let mut strategy = MarketExitHookTrackingStrategy::new(config);
2325
2326 let trader_id = TraderId::from("TRADER-001");
2327 let clock = Rc::new(RefCell::new(TestClock::new()));
2328 let cache = Rc::new(RefCell::new(Cache::default()));
2329 let portfolio = Rc::new(RefCell::new(Portfolio::new(
2330 cache.clone(),
2331 clock.clone(),
2332 None,
2333 )));
2334 strategy
2335 .core
2336 .register(trader_id, clock, cache, portfolio)
2337 .unwrap();
2338 strategy.initialize().unwrap();
2339 strategy.start().unwrap();
2340
2341 let _ = strategy.market_exit();
2342
2343 assert!(strategy.on_market_exit_called);
2344 }
2345
2346 #[rstest]
2347 fn test_finalize_market_exit_calls_post_market_exit_hook() {
2348 let config = StrategyConfig {
2349 strategy_id: Some(StrategyId::from("TEST-001")),
2350 order_id_tag: Some("001".to_string()),
2351 ..Default::default()
2352 };
2353 let mut strategy = MarketExitHookTrackingStrategy::new(config);
2354
2355 let trader_id = TraderId::from("TRADER-001");
2356 let clock = Rc::new(RefCell::new(TestClock::new()));
2357 let cache = Rc::new(RefCell::new(Cache::default()));
2358 let portfolio = Rc::new(RefCell::new(Portfolio::new(
2359 cache.clone(),
2360 clock.clone(),
2361 None,
2362 )));
2363 strategy
2364 .core
2365 .register(trader_id, clock, cache, portfolio)
2366 .unwrap();
2367
2368 strategy.core.is_exiting = true;
2369 strategy.finalize_market_exit();
2370
2371 assert!(strategy.post_market_exit_called);
2372 }
2373
2374 #[derive(Debug)]
2375 struct FailingPostExitStrategy {
2376 core: StrategyCore,
2377 }
2378
2379 impl FailingPostExitStrategy {
2380 fn new(config: StrategyConfig) -> Self {
2381 Self {
2382 core: StrategyCore::new(config),
2383 }
2384 }
2385 }
2386
2387 impl DataActor for FailingPostExitStrategy {}
2388
2389 nautilus_strategy!(FailingPostExitStrategy, {
2390 fn post_market_exit(&mut self) {
2391 panic!("Simulated error in post_market_exit");
2392 }
2393 });
2394
2395 #[rstest]
2396 fn test_finalize_market_exit_handles_hook_panic() {
2397 let config = StrategyConfig {
2398 strategy_id: Some(StrategyId::from("TEST-001")),
2399 order_id_tag: Some("001".to_string()),
2400 ..Default::default()
2401 };
2402 let mut strategy = FailingPostExitStrategy::new(config);
2403
2404 let trader_id = TraderId::from("TRADER-001");
2405 let clock = Rc::new(RefCell::new(TestClock::new()));
2406 let cache = Rc::new(RefCell::new(Cache::default()));
2407 let portfolio = Rc::new(RefCell::new(Portfolio::new(
2408 cache.clone(),
2409 clock.clone(),
2410 None,
2411 )));
2412 strategy
2413 .core
2414 .register(trader_id, clock, cache, portfolio)
2415 .unwrap();
2416
2417 strategy.core.is_exiting = true;
2418 strategy.core.pending_stop = true;
2419
2420 strategy.finalize_market_exit();
2422
2423 assert!(!strategy.core.is_exiting);
2425 assert!(!strategy.core.pending_stop);
2426 }
2427
2428 #[rstest]
2429 fn test_check_market_exit_increments_attempts_before_finalizing() {
2430 let mut strategy = create_test_strategy();
2431 register_strategy(&mut strategy);
2432
2433 strategy.core.is_exiting = true;
2434 assert_eq!(strategy.core.market_exit_attempts, 0);
2435
2436 let event = TimeEvent::new(
2437 Ustr::from("MARKET_EXIT_CHECK:TEST-001"),
2438 UUID4::new(),
2439 UnixNanos::default(),
2440 UnixNanos::default(),
2441 );
2442 strategy.check_market_exit(event);
2443
2444 assert!(!strategy.core.is_exiting);
2448 assert_eq!(strategy.core.market_exit_attempts, 0);
2449 }
2450
2451 #[rstest]
2452 fn test_check_market_exit_finalizes_when_max_attempts_reached() {
2453 let config = StrategyConfig {
2454 strategy_id: Some(StrategyId::from("TEST-001")),
2455 order_id_tag: Some("001".to_string()),
2456 market_exit_max_attempts: 3,
2457 ..Default::default()
2458 };
2459 let mut strategy = TestStrategy::new(config);
2460 register_strategy(&mut strategy);
2461
2462 strategy.core.is_exiting = true;
2463 strategy.core.market_exit_attempts = 2; let event = TimeEvent::new(
2466 Ustr::from("MARKET_EXIT_CHECK:TEST-001"),
2467 UUID4::new(),
2468 UnixNanos::default(),
2469 UnixNanos::default(),
2470 );
2471 strategy.check_market_exit(event);
2472
2473 assert!(!strategy.core.is_exiting);
2475 assert_eq!(strategy.core.market_exit_attempts, 0);
2476 }
2477
2478 #[rstest]
2479 fn test_check_market_exit_finalizes_when_no_orders_or_positions() {
2480 let mut strategy = create_test_strategy();
2481 register_strategy(&mut strategy);
2482
2483 strategy.core.is_exiting = true;
2484
2485 let event = TimeEvent::new(
2486 Ustr::from("MARKET_EXIT_CHECK:TEST-001"),
2487 UUID4::new(),
2488 UnixNanos::default(),
2489 UnixNanos::default(),
2490 );
2491 strategy.check_market_exit(event);
2492
2493 assert!(!strategy.core.is_exiting);
2495 }
2496
2497 #[rstest]
2498 fn test_market_exit_timer_name_format() {
2499 let config = StrategyConfig {
2500 strategy_id: Some(StrategyId::from("MY-STRATEGY-001")),
2501 ..Default::default()
2502 };
2503 let strategy = TestStrategy::new(config);
2504
2505 assert_eq!(
2506 strategy.core.market_exit_timer_name.as_str(),
2507 "MARKET_EXIT_CHECK:MY-STRATEGY-001"
2508 );
2509 }
2510
2511 #[rstest]
2512 fn test_reset_market_exit_state() {
2513 let mut strategy = create_test_strategy();
2514
2515 strategy.core.is_exiting = true;
2516 strategy.core.pending_stop = true;
2517 strategy.core.market_exit_attempts = 50;
2518
2519 strategy.core.reset_market_exit_state();
2520
2521 assert!(!strategy.core.is_exiting);
2522 assert!(!strategy.core.pending_stop);
2523 assert_eq!(strategy.core.market_exit_attempts, 0);
2524 }
2525
2526 #[rstest]
2527 fn test_cancel_market_exit_resets_state_without_hooks() {
2528 let config = StrategyConfig {
2529 strategy_id: Some(StrategyId::from("TEST-001")),
2530 order_id_tag: Some("001".to_string()),
2531 ..Default::default()
2532 };
2533 let mut strategy = MarketExitHookTrackingStrategy::new(config);
2534
2535 let trader_id = TraderId::from("TRADER-001");
2536 let clock = Rc::new(RefCell::new(TestClock::new()));
2537 let cache = Rc::new(RefCell::new(Cache::default()));
2538 let portfolio = Rc::new(RefCell::new(Portfolio::new(
2539 cache.clone(),
2540 clock.clone(),
2541 None,
2542 )));
2543 strategy
2544 .core
2545 .register(trader_id, clock, cache, portfolio)
2546 .unwrap();
2547
2548 strategy.core.is_exiting = true;
2550 strategy.core.pending_stop = true;
2551 strategy.core.market_exit_attempts = 50;
2552
2553 strategy.cancel_market_exit();
2555
2556 assert!(!strategy.core.is_exiting);
2558 assert!(!strategy.core.pending_stop);
2559 assert_eq!(strategy.core.market_exit_attempts, 0);
2560
2561 assert!(!strategy.on_market_exit_called);
2563 assert!(!strategy.post_market_exit_called);
2564 }
2565
2566 #[rstest]
2567 fn test_market_exit_returns_early_when_not_running() {
2568 let mut strategy = create_test_strategy();
2569 register_strategy(&mut strategy);
2570
2571 assert_ne!(strategy.core.actor.state(), ComponentState::Running);
2573
2574 let result = strategy.market_exit();
2575
2576 assert!(result.is_ok());
2578 assert!(!strategy.core.is_exiting);
2579 }
2580
2581 #[rstest]
2582 fn test_stop_with_manage_stop_false_cleans_up_active_exit() {
2583 let config = StrategyConfig {
2584 strategy_id: Some(StrategyId::from("TEST-001")),
2585 order_id_tag: Some("001".to_string()),
2586 manage_stop: false,
2587 ..Default::default()
2588 };
2589 let mut strategy = TestStrategy::new(config);
2590 register_strategy(&mut strategy);
2591
2592 strategy.core.is_exiting = true;
2594 strategy.core.market_exit_attempts = 5;
2595
2596 let should_proceed = Strategy::stop(&mut strategy);
2598
2599 assert!(should_proceed);
2601 assert!(!strategy.core.is_exiting);
2602 assert_eq!(strategy.core.market_exit_attempts, 0);
2603 }
2604
2605 #[rstest]
2606 fn test_stop_with_manage_stop_true_defers_when_running() {
2607 let config = StrategyConfig {
2608 strategy_id: Some(StrategyId::from("TEST-001")),
2609 order_id_tag: Some("001".to_string()),
2610 manage_stop: true,
2611 ..Default::default()
2612 };
2613 let mut strategy = TestStrategy::new(config);
2614
2615 let trader_id = TraderId::from("TRADER-001");
2617 let clock = Rc::new(RefCell::new(TestClock::new()));
2618 clock
2619 .borrow_mut()
2620 .register_default_handler(TimeEventCallback::from(|_event: TimeEvent| {}));
2621 let cache = Rc::new(RefCell::new(Cache::default()));
2622 let portfolio = Rc::new(RefCell::new(Portfolio::new(
2623 cache.clone(),
2624 clock.clone(),
2625 None,
2626 )));
2627 strategy
2628 .core
2629 .register(trader_id, clock, cache, portfolio)
2630 .unwrap();
2631 strategy.initialize().unwrap();
2632 strategy.start().unwrap();
2633
2634 let should_proceed = Strategy::stop(&mut strategy);
2635
2636 assert!(!should_proceed);
2638 assert!(strategy.core.pending_stop);
2639 }
2640
2641 #[rstest]
2642 fn test_stop_with_manage_stop_true_returns_early_if_pending() {
2643 let config = StrategyConfig {
2644 strategy_id: Some(StrategyId::from("TEST-001")),
2645 order_id_tag: Some("001".to_string()),
2646 manage_stop: true,
2647 ..Default::default()
2648 };
2649 let mut strategy = TestStrategy::new(config);
2650 register_strategy(&mut strategy);
2651 start_strategy(&mut strategy);
2652 strategy.core.pending_stop = true;
2653
2654 let should_proceed = Strategy::stop(&mut strategy);
2656
2657 assert!(!should_proceed);
2659 assert!(strategy.core.pending_stop);
2660 }
2661
2662 #[rstest]
2663 fn test_stop_with_manage_stop_true_proceeds_when_not_running() {
2664 let config = StrategyConfig {
2665 strategy_id: Some(StrategyId::from("TEST-001")),
2666 order_id_tag: Some("001".to_string()),
2667 manage_stop: true,
2668 ..Default::default()
2669 };
2670 let mut strategy = TestStrategy::new(config);
2671 register_strategy(&mut strategy);
2672
2673 assert_ne!(strategy.core.actor.state(), ComponentState::Running);
2675
2676 let should_proceed = Strategy::stop(&mut strategy);
2677
2678 assert!(should_proceed);
2680 }
2681
2682 #[rstest]
2683 fn test_finalize_market_exit_stops_strategy_when_pending() {
2684 let config = StrategyConfig {
2685 strategy_id: Some(StrategyId::from("TEST-001")),
2686 order_id_tag: Some("001".to_string()),
2687 ..Default::default()
2688 };
2689 let mut strategy = TestStrategy::new(config);
2690 register_strategy(&mut strategy);
2691 start_strategy(&mut strategy);
2692
2693 strategy.core.is_exiting = true;
2695 strategy.core.pending_stop = true;
2696
2697 strategy.finalize_market_exit();
2698
2699 assert_eq!(strategy.core.actor.state(), ComponentState::Stopped);
2701 assert!(!strategy.core.is_exiting);
2702 assert!(!strategy.core.pending_stop);
2703 }
2704
2705 #[rstest]
2706 fn test_finalize_market_exit_stays_running_when_not_pending() {
2707 let config = StrategyConfig {
2708 strategy_id: Some(StrategyId::from("TEST-001")),
2709 order_id_tag: Some("001".to_string()),
2710 ..Default::default()
2711 };
2712 let mut strategy = TestStrategy::new(config);
2713 register_strategy(&mut strategy);
2714 start_strategy(&mut strategy);
2715
2716 strategy.core.is_exiting = true;
2718 strategy.core.pending_stop = false;
2719
2720 strategy.finalize_market_exit();
2721
2722 assert_eq!(strategy.core.actor.state(), ComponentState::Running);
2724 assert!(!strategy.core.is_exiting);
2725 }
2726
2727 #[rstest]
2728 fn test_submit_order_denied_during_market_exit_when_not_reduce_only() {
2729 let mut strategy = create_test_strategy();
2730 register_strategy(&mut strategy);
2731 start_strategy(&mut strategy);
2732 strategy.core.is_exiting = true;
2733
2734 let order = OrderAny::Market(MarketOrder::new(
2735 TraderId::from("TRADER-001"),
2736 StrategyId::from("TEST-001"),
2737 InstrumentId::from("BTCUSDT.BINANCE"),
2738 ClientOrderId::from("O-20250208-0001"),
2739 OrderSide::Buy,
2740 Quantity::from(100_000),
2741 TimeInForce::Gtc,
2742 UUID4::new(),
2743 UnixNanos::default(),
2744 false, false,
2746 None,
2747 None,
2748 None,
2749 None,
2750 None,
2751 None,
2752 None,
2753 None,
2754 ));
2755 let client_order_id = order.client_order_id();
2756 let result = strategy.submit_order(order, None, None);
2757
2758 assert!(result.is_ok());
2759 let cache = strategy.core.cache();
2760 let cached_order = cache.order(&client_order_id).unwrap();
2761 assert_eq!(cached_order.status(), OrderStatus::Denied);
2762 }
2763
2764 #[rstest]
2765 fn test_submit_order_allowed_during_market_exit_when_reduce_only() {
2766 let mut strategy = create_test_strategy();
2767 register_strategy(&mut strategy);
2768 start_strategy(&mut strategy);
2769 strategy.core.is_exiting = true;
2770
2771 let order = OrderAny::Market(MarketOrder::new(
2772 TraderId::from("TRADER-001"),
2773 StrategyId::from("TEST-001"),
2774 InstrumentId::from("BTCUSDT.BINANCE"),
2775 ClientOrderId::from("O-20250208-0001"),
2776 OrderSide::Buy,
2777 Quantity::from(100_000),
2778 TimeInForce::Gtc,
2779 UUID4::new(),
2780 UnixNanos::default(),
2781 true, false,
2783 None,
2784 None,
2785 None,
2786 None,
2787 None,
2788 None,
2789 None,
2790 None,
2791 ));
2792 let client_order_id = order.client_order_id();
2793 let result = strategy.submit_order(order, None, None);
2794
2795 assert!(result.is_ok());
2796 let cache = strategy.core.cache();
2797 let cached_order = cache.order(&client_order_id).unwrap();
2798 assert_ne!(cached_order.status(), OrderStatus::Denied);
2799 }
2800
2801 #[rstest]
2802 fn test_submit_order_allowed_during_market_exit_when_tagged() {
2803 let mut strategy = create_test_strategy();
2804 register_strategy(&mut strategy);
2805 start_strategy(&mut strategy);
2806 strategy.core.is_exiting = true;
2807
2808 let order = OrderAny::Market(MarketOrder::new(
2809 TraderId::from("TRADER-001"),
2810 StrategyId::from("TEST-001"),
2811 InstrumentId::from("BTCUSDT.BINANCE"),
2812 ClientOrderId::from("O-20250208-0002"),
2813 OrderSide::Buy,
2814 Quantity::from(100_000),
2815 TimeInForce::Gtc,
2816 UUID4::new(),
2817 UnixNanos::default(),
2818 false, false,
2820 None,
2821 None,
2822 None,
2823 None,
2824 None,
2825 None,
2826 None,
2827 Some(vec![Ustr::from("MARKET_EXIT")]),
2828 ));
2829 let client_order_id = order.client_order_id();
2830 let result = strategy.submit_order(order, None, None);
2831
2832 assert!(result.is_ok());
2833 let cache = strategy.core.cache();
2834 let cached_order = cache.order(&client_order_id).unwrap();
2835 assert_ne!(cached_order.status(), OrderStatus::Denied);
2836 }
2837
2838 #[derive(Debug)]
2839 struct MacroTestSimple {
2840 core: StrategyCore,
2841 }
2842
2843 nautilus_strategy!(MacroTestSimple);
2844
2845 impl DataActor for MacroTestSimple {}
2846
2847 #[derive(Debug)]
2848 struct MacroTestWithHooks {
2849 core: StrategyCore,
2850 }
2851
2852 nautilus_strategy!(MacroTestWithHooks, {
2853 fn on_order_rejected(&mut self, _event: OrderRejected) {}
2854 });
2855
2856 impl DataActor for MacroTestWithHooks {}
2857
2858 #[derive(Debug)]
2859 struct MacroTestCustomField {
2860 inner: StrategyCore,
2861 }
2862
2863 nautilus_strategy!(MacroTestCustomField, inner, {
2864 fn external_order_claims(&self) -> Option<Vec<InstrumentId>> {
2865 None
2866 }
2867 });
2868
2869 impl DataActor for MacroTestCustomField {}
2870
2871 #[rstest]
2872 fn test_nautilus_strategy_macro_forms() {
2873 let config = StrategyConfig {
2874 strategy_id: Some(StrategyId::from("MACRO-001")),
2875 order_id_tag: Some("001".to_string()),
2876 ..Default::default()
2877 };
2878
2879 let simple = MacroTestSimple {
2880 core: StrategyCore::new(config.clone()),
2881 };
2882 assert_eq!(simple.core().config.strategy_id, config.strategy_id);
2883
2884 let hooks = MacroTestWithHooks {
2885 core: StrategyCore::new(config.clone()),
2886 };
2887 assert_eq!(hooks.core().config.strategy_id, config.strategy_id);
2888
2889 let custom = MacroTestCustomField {
2890 inner: StrategyCore::new(config.clone()),
2891 };
2892 assert_eq!(custom.core().config.strategy_id, config.strategy_id);
2893 assert!(custom.external_order_claims().is_none());
2894 }
2895}