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 {
484 manager.send_risk_command(TradingCommand::ModifyOrder(command));
485 }
486 Ok(())
487 }
488
489 fn cancel_order(&mut self, order: OrderAny, client_id: Option<ClientId>) -> anyhow::Result<()> {
495 self.cancel_order_with_params(order, client_id, Params::new())
496 }
497
498 fn cancel_order_with_params(
504 &mut self,
505 order: OrderAny,
506 client_id: Option<ClientId>,
507 params: Params,
508 ) -> anyhow::Result<()> {
509 let core = self.core_mut();
510
511 let trader_id = core.trader_id().expect("Trader ID not set");
512 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
513 let ts_init = core.clock().timestamp_ns();
514
515 let params = if params.is_empty() {
516 None
517 } else {
518 Some(params)
519 };
520
521 let command = CancelOrder::new(
522 trader_id,
523 client_id,
524 strategy_id,
525 order.instrument_id(),
526 order.client_order_id(),
527 order.venue_order_id(),
528 UUID4::new(),
529 ts_init,
530 params,
531 );
532
533 let manager = core.order_manager();
534
535 if matches!(order.emulation_trigger(), Some(trigger) if trigger != TriggerType::NoTrigger)
536 || order.is_emulated()
537 {
538 manager.send_emulator_command(TradingCommand::CancelOrder(command));
539 } else if let Some(algo_id) = order.exec_algorithm_id() {
540 let endpoint = format!("{algo_id}.execute");
541 msgbus::send_any(endpoint.into(), &TradingCommand::CancelOrder(command));
542 } else {
543 manager.send_exec_command(TradingCommand::CancelOrder(command));
544 }
545 Ok(())
546 }
547
548 fn cancel_orders(
555 &mut self,
556 mut orders: Vec<OrderAny>,
557 client_id: Option<ClientId>,
558 params: Option<Params>,
559 ) -> anyhow::Result<()> {
560 if orders.is_empty() {
561 anyhow::bail!("Cannot batch cancel empty order list");
562 }
563
564 let core = self.core_mut();
565 let trader_id = core.trader_id().expect("Trader ID not set");
566 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
567 let ts_init = core.clock().timestamp_ns();
568
569 let manager = core.order_manager();
570
571 let first = orders.remove(0);
572 let instrument_id = first.instrument_id();
573
574 if first.is_emulated() || first.is_active_local() {
575 anyhow::bail!("Cannot include emulated or local orders in batch cancel");
576 }
577
578 let mut cancels = Vec::with_capacity(orders.len() + 1);
579 cancels.push(CancelOrder::new(
580 trader_id,
581 client_id,
582 strategy_id,
583 instrument_id,
584 first.client_order_id(),
585 first.venue_order_id(),
586 UUID4::new(),
587 ts_init,
588 params.clone(),
589 ));
590
591 for order in orders {
592 if order.instrument_id() != instrument_id {
593 anyhow::bail!(
594 "Cannot batch cancel orders for different instruments: {} vs {}",
595 instrument_id,
596 order.instrument_id()
597 );
598 }
599
600 if order.is_emulated() || order.is_active_local() {
601 anyhow::bail!("Cannot include emulated or local orders in batch cancel");
602 }
603
604 cancels.push(CancelOrder::new(
605 trader_id,
606 client_id,
607 strategy_id,
608 instrument_id,
609 order.client_order_id(),
610 order.venue_order_id(),
611 UUID4::new(),
612 ts_init,
613 params.clone(),
614 ));
615 }
616
617 let command = BatchCancelOrders::new(
618 trader_id,
619 client_id,
620 strategy_id,
621 instrument_id,
622 cancels,
623 UUID4::new(),
624 ts_init,
625 params,
626 );
627
628 manager.send_exec_command(TradingCommand::BatchCancelOrders(command));
629 Ok(())
630 }
631
632 fn cancel_all_orders(
638 &mut self,
639 instrument_id: InstrumentId,
640 order_side: Option<OrderSide>,
641 client_id: Option<ClientId>,
642 ) -> anyhow::Result<()> {
643 self.cancel_all_orders_with_params(instrument_id, order_side, client_id, Params::new())
644 }
645
646 fn cancel_all_orders_with_params(
652 &mut self,
653 instrument_id: InstrumentId,
654 order_side: Option<OrderSide>,
655 client_id: Option<ClientId>,
656 params: Params,
657 ) -> anyhow::Result<()> {
658 let params = if params.is_empty() {
659 None
660 } else {
661 Some(params)
662 };
663 let core = self.core_mut();
664
665 let trader_id = core.trader_id().expect("Trader ID not set");
666 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
667 let ts_init = core.clock().timestamp_ns();
668 let cache = core.cache();
669
670 let open_orders = cache.orders_open(
671 None,
672 Some(&instrument_id),
673 Some(&strategy_id),
674 None,
675 order_side,
676 );
677
678 let emulated_orders = cache.orders_emulated(
679 None,
680 Some(&instrument_id),
681 Some(&strategy_id),
682 None,
683 order_side,
684 );
685
686 let inflight_orders = cache.orders_inflight(
687 None,
688 Some(&instrument_id),
689 Some(&strategy_id),
690 None,
691 order_side,
692 );
693
694 let mut exec_algorithm_ids: Vec<_> = cache.exec_algorithm_ids().into_iter().collect();
698 exec_algorithm_ids.sort();
699 let mut algo_orders = Vec::new();
700
701 for algo_id in &exec_algorithm_ids {
702 let orders = cache.orders_for_exec_algorithm(
703 algo_id,
704 None,
705 Some(&instrument_id),
706 Some(&strategy_id),
707 None,
708 order_side,
709 );
710 algo_orders.extend(orders.iter().map(|o| (*o).clone()));
711 }
712
713 let open_count = open_orders.len();
714 let emulated_count = emulated_orders.len();
715 let inflight_count = inflight_orders.len();
716 let algo_count = algo_orders.len();
717
718 drop(cache);
719
720 if open_count == 0 && emulated_count == 0 && inflight_count == 0 && algo_count == 0 {
721 let side_str = order_side.map(|s| format!(" {s}")).unwrap_or_default();
722 log::info!("No {instrument_id} open, emulated, or inflight{side_str} orders to cancel");
723 return Ok(());
724 }
725
726 let manager = core.order_manager();
727
728 let side_str = order_side.map(|s| format!(" {s}")).unwrap_or_default();
729
730 if open_count > 0 {
731 log::info!(
732 "Canceling {open_count} open{side_str} {instrument_id} order{}",
733 if open_count == 1 { "" } else { "s" }
734 );
735 }
736
737 if emulated_count > 0 {
738 log::info!(
739 "Canceling {emulated_count} emulated{side_str} {instrument_id} order{}",
740 if emulated_count == 1 { "" } else { "s" }
741 );
742 }
743
744 if inflight_count > 0 {
745 log::info!(
746 "Canceling {inflight_count} inflight{side_str} {instrument_id} order{}",
747 if inflight_count == 1 { "" } else { "s" }
748 );
749 }
750
751 if open_count > 0 || inflight_count > 0 {
752 let command = CancelAllOrders::new(
753 trader_id,
754 client_id,
755 strategy_id,
756 instrument_id,
757 order_side.unwrap_or(OrderSide::NoOrderSide),
758 UUID4::new(),
759 ts_init,
760 params.clone(),
761 );
762
763 manager.send_exec_command(TradingCommand::CancelAllOrders(command));
764 }
765
766 if emulated_count > 0 {
767 let command = CancelAllOrders::new(
768 trader_id,
769 client_id,
770 strategy_id,
771 instrument_id,
772 order_side.unwrap_or(OrderSide::NoOrderSide),
773 UUID4::new(),
774 ts_init,
775 params,
776 );
777
778 manager.send_emulator_command(TradingCommand::CancelAllOrders(command));
779 }
780
781 for order in algo_orders {
782 self.cancel_order(order, client_id)?;
783 }
784
785 Ok(())
786 }
787
788 fn close_position(
794 &mut self,
795 position: &Position,
796 client_id: Option<ClientId>,
797 tags: Option<Vec<Ustr>>,
798 time_in_force: Option<TimeInForce>,
799 reduce_only: Option<bool>,
800 quote_quantity: Option<bool>,
801 ) -> anyhow::Result<()> {
802 let core = self.core_mut();
803
804 if position.is_closed() {
805 log::warn!("Cannot close position (already closed): {}", position.id);
806 return Ok(());
807 }
808
809 let closing_side = OrderCore::closing_side(position.side);
810
811 let order = core.order_factory().market(
812 position.instrument_id,
813 closing_side,
814 position.quantity,
815 time_in_force,
816 reduce_only.or(Some(true)),
817 quote_quantity,
818 None,
819 None,
820 tags,
821 None,
822 );
823
824 self.submit_order(order, Some(position.id), client_id)
825 }
826
827 #[expect(clippy::too_many_arguments)]
833 fn close_all_positions(
834 &mut self,
835 instrument_id: InstrumentId,
836 position_side: Option<PositionSide>,
837 client_id: Option<ClientId>,
838 tags: Option<Vec<Ustr>>,
839 time_in_force: Option<TimeInForce>,
840 reduce_only: Option<bool>,
841 quote_quantity: Option<bool>,
842 ) -> anyhow::Result<()> {
843 let core = self.core_mut();
844 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
845 let cache = core.cache();
846
847 let positions_open = cache.positions_open(
848 None,
849 Some(&instrument_id),
850 Some(&strategy_id),
851 None,
852 position_side,
853 );
854
855 let side_str = position_side.map(|s| format!(" {s}")).unwrap_or_default();
856
857 if positions_open.is_empty() {
858 log::info!("No {instrument_id} open{side_str} positions to close");
859 return Ok(());
860 }
861
862 let count = positions_open.len();
863 log::info!(
864 "Closing {count} open{side_str} position{}",
865 if count == 1 { "" } else { "s" }
866 );
867
868 let positions_data: Vec<_> = positions_open
869 .iter()
870 .map(|p| (p.id, p.instrument_id, p.side, p.quantity, p.is_closed()))
871 .collect();
872
873 drop(cache);
874
875 for (pos_id, pos_instrument_id, pos_side, pos_quantity, is_closed) in positions_data {
876 if is_closed {
877 continue;
878 }
879
880 let core = self.core_mut();
881 let closing_side = OrderCore::closing_side(pos_side);
882 let order = core.order_factory().market(
883 pos_instrument_id,
884 closing_side,
885 pos_quantity,
886 time_in_force,
887 reduce_only.or(Some(true)),
888 quote_quantity,
889 None,
890 None,
891 tags.clone(),
892 None,
893 );
894
895 self.submit_order(order, Some(pos_id), client_id)?;
896 }
897
898 Ok(())
899 }
900
901 fn query_account(
910 &mut self,
911 account_id: AccountId,
912 client_id: Option<ClientId>,
913 params: Option<Params>,
914 ) -> anyhow::Result<()> {
915 let core = self.core_mut();
916
917 let trader_id = core.trader_id().expect("Trader ID not set");
918 let ts_init = core.clock().timestamp_ns();
919
920 let command = QueryAccount::new(
921 trader_id,
922 client_id,
923 account_id,
924 UUID4::new(),
925 ts_init,
926 params,
927 );
928
929 core.order_manager()
930 .send_exec_command(TradingCommand::QueryAccount(command));
931 Ok(())
932 }
933
934 fn query_order(
943 &mut self,
944 order: &OrderAny,
945 client_id: Option<ClientId>,
946 params: Option<Params>,
947 ) -> anyhow::Result<()> {
948 let core = self.core_mut();
949
950 let trader_id = core.trader_id().expect("Trader ID not set");
951 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
952 let ts_init = core.clock().timestamp_ns();
953
954 let command = QueryOrder::new(
955 trader_id,
956 client_id,
957 strategy_id,
958 order.instrument_id(),
959 order.client_order_id(),
960 order.venue_order_id(),
961 UUID4::new(),
962 ts_init,
963 params,
964 );
965
966 core.order_manager()
967 .send_exec_command(TradingCommand::QueryOrder(command));
968 Ok(())
969 }
970
971 fn handle_order_event(&mut self, event: OrderEventAny) {
973 let state = {
974 let core = self.core_mut();
975 let id = &core.actor.actor_id;
976 let is_warning = matches!(
977 &event,
978 OrderEventAny::Denied(_)
979 | OrderEventAny::Rejected(_)
980 | OrderEventAny::CancelRejected(_)
981 | OrderEventAny::ModifyRejected(_)
982 );
983
984 if is_warning {
985 log::warn!("{id} {RECV}{EVT} {event}");
986 } else if core.config.log_events {
987 log::info!("{id} {RECV}{EVT} {event}");
988 }
989
990 core.actor.state()
991 };
992
993 let client_order_id = event.client_order_id();
994 let is_terminal = matches!(
995 &event,
996 OrderEventAny::Filled(_)
997 | OrderEventAny::Canceled(_)
998 | OrderEventAny::Rejected(_)
999 | OrderEventAny::Expired(_)
1000 | OrderEventAny::Denied(_)
1001 );
1002
1003 if is_terminal {
1006 self.cancel_gtd_expiry(&client_order_id);
1007 }
1008
1009 if state != ComponentState::Running {
1012 return;
1013 }
1014
1015 {
1018 let core = self.core_mut();
1019 if let Some(manager) = &mut core.order_manager {
1020 manager.handle_event(&event);
1021 }
1022 }
1023
1024 match &event {
1025 OrderEventAny::Initialized(e) => self.on_order_initialized(e.clone()),
1026 OrderEventAny::Denied(e) => self.on_order_denied(*e),
1027 OrderEventAny::Emulated(e) => self.on_order_emulated(*e),
1028 OrderEventAny::Released(e) => self.on_order_released(*e),
1029 OrderEventAny::Submitted(e) => self.on_order_submitted(*e),
1030 OrderEventAny::Rejected(e) => self.on_order_rejected(*e),
1031 OrderEventAny::Accepted(e) => self.on_order_accepted(*e),
1032 OrderEventAny::Canceled(e) => {
1033 let _ = DataActor::on_order_canceled(self, e);
1034 }
1035 OrderEventAny::Expired(e) => self.on_order_expired(*e),
1036 OrderEventAny::Triggered(e) => self.on_order_triggered(*e),
1037 OrderEventAny::PendingUpdate(e) => self.on_order_pending_update(*e),
1038 OrderEventAny::PendingCancel(e) => self.on_order_pending_cancel(*e),
1039 OrderEventAny::ModifyRejected(e) => self.on_order_modify_rejected(*e),
1040 OrderEventAny::CancelRejected(e) => self.on_order_cancel_rejected(*e),
1041 OrderEventAny::Updated(e) => self.on_order_updated(*e),
1042 OrderEventAny::Filled(e) => {
1043 let _ = DataActor::on_order_filled(self, e);
1044 }
1045 }
1046 }
1047
1048 fn handle_position_event(&mut self, event: PositionEvent) {
1050 let state = {
1051 let core = self.core_mut();
1052
1053 if core.config.log_events {
1054 let id = &core.actor.actor_id;
1055 log::info!("{id} {RECV}{EVT} {event:?}");
1056 }
1057
1058 core.actor.state()
1059 };
1060
1061 if state != ComponentState::Running {
1062 return;
1063 }
1064
1065 match event {
1066 PositionEvent::PositionOpened(e) => self.on_position_opened(e),
1067 PositionEvent::PositionChanged(e) => self.on_position_changed(e),
1068 PositionEvent::PositionClosed(e) => self.on_position_closed(e),
1069 PositionEvent::PositionAdjusted(_) => {
1070 }
1072 }
1073 }
1074
1075 fn on_start(&mut self) -> anyhow::Result<()> {
1086 let core = self.core_mut();
1087 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
1088 log::info!("Starting {strategy_id}");
1089
1090 if core.config.manage_gtd_expiry {
1091 self.reactivate_gtd_timers();
1092 }
1093
1094 Ok(())
1095 }
1096
1097 fn on_time_event(&mut self, event: &TimeEvent) -> anyhow::Result<()> {
1106 if event.name.starts_with("GTD-EXPIRY:") {
1107 self.expire_gtd_order(event.clone());
1108 } else if event.name.starts_with("MARKET_EXIT_CHECK:") {
1109 self.check_market_exit(event.clone());
1110 }
1111 Ok(())
1112 }
1113
1114 #[allow(unused_variables)]
1120 fn on_order_initialized(&mut self, event: OrderInitialized) {}
1121
1122 #[allow(unused_variables)]
1126 fn on_order_denied(&mut self, event: OrderDenied) {}
1127
1128 #[allow(unused_variables)]
1132 fn on_order_emulated(&mut self, event: OrderEmulated) {}
1133
1134 #[allow(unused_variables)]
1138 fn on_order_released(&mut self, event: OrderReleased) {}
1139
1140 #[allow(unused_variables)]
1144 fn on_order_submitted(&mut self, event: OrderSubmitted) {}
1145
1146 #[allow(unused_variables)]
1150 fn on_order_rejected(&mut self, event: OrderRejected) {}
1151
1152 #[allow(unused_variables)]
1156 fn on_order_accepted(&mut self, event: OrderAccepted) {}
1157
1158 #[allow(unused_variables)]
1162 fn on_order_expired(&mut self, event: OrderExpired) {}
1163
1164 #[allow(unused_variables)]
1168 fn on_order_triggered(&mut self, event: OrderTriggered) {}
1169
1170 #[allow(unused_variables)]
1174 fn on_order_pending_update(&mut self, event: OrderPendingUpdate) {}
1175
1176 #[allow(unused_variables)]
1180 fn on_order_pending_cancel(&mut self, event: OrderPendingCancel) {}
1181
1182 #[allow(unused_variables)]
1186 fn on_order_modify_rejected(&mut self, event: OrderModifyRejected) {}
1187
1188 #[allow(unused_variables)]
1192 fn on_order_cancel_rejected(&mut self, event: OrderCancelRejected) {}
1193
1194 #[allow(unused_variables)]
1198 fn on_order_updated(&mut self, event: OrderUpdated) {}
1199
1200 #[allow(unused_variables)]
1206 fn on_position_opened(&mut self, event: PositionOpened) {}
1207
1208 #[allow(unused_variables)]
1212 fn on_position_changed(&mut self, event: PositionChanged) {}
1213
1214 #[allow(unused_variables)]
1218 fn on_position_closed(&mut self, event: PositionClosed) {}
1219
1220 fn on_market_exit(&mut self) {}
1224
1225 fn post_market_exit(&mut self) {}
1229
1230 fn is_exiting(&self) -> bool {
1234 self.core().is_exiting
1235 }
1236
1237 fn market_exit(&mut self) -> anyhow::Result<()> {
1253 let core = self.core_mut();
1254 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
1255
1256 if core.actor.state() != ComponentState::Running {
1257 log::warn!("{strategy_id} Cannot market exit: strategy is not running");
1258 return Ok(());
1259 }
1260
1261 if core.is_exiting {
1262 log::warn!("{strategy_id} Market exit called when already in progress");
1263 return Ok(());
1264 }
1265
1266 core.is_exiting = true;
1267 core.market_exit_attempts = 0;
1268 let time_in_force = core.config.market_exit_time_in_force;
1269 let reduce_only = core.config.market_exit_reduce_only;
1270
1271 log::info!("{strategy_id} Initiating market exit...");
1272
1273 self.on_market_exit();
1274
1275 let core = self.core_mut();
1276 let cache = core.cache();
1277
1278 let open_orders = cache.orders_open(None, None, Some(&strategy_id), None, None);
1279 let inflight_orders = cache.orders_inflight(None, None, Some(&strategy_id), None, None);
1280 let open_positions = cache.positions_open(None, None, Some(&strategy_id), None, None);
1281
1282 let mut instruments: AHashSet<InstrumentId> = AHashSet::new();
1283
1284 for order in &open_orders {
1285 instruments.insert(order.instrument_id());
1286 }
1287
1288 for order in &inflight_orders {
1289 instruments.insert(order.instrument_id());
1290 }
1291
1292 for position in &open_positions {
1293 instruments.insert(position.instrument_id);
1294 }
1295
1296 let market_exit_tag = core.market_exit_tag;
1297 let mut instruments: Vec<_> = instruments.into_iter().collect();
1301 instruments.sort();
1302 drop(cache);
1303
1304 for instrument_id in instruments {
1305 if let Err(e) = self.cancel_all_orders(instrument_id, None, None) {
1306 log::error!("Error canceling orders for {instrument_id}: {e}");
1307 }
1308
1309 if let Err(e) = self.close_all_positions(
1310 instrument_id,
1311 None,
1312 None,
1313 Some(vec![market_exit_tag]),
1314 Some(time_in_force),
1315 Some(reduce_only),
1316 None,
1317 ) {
1318 log::error!("Error closing positions for {instrument_id}: {e}");
1319 }
1320 }
1321
1322 let core = self.core_mut();
1323 let interval_ms = core.config.market_exit_interval_ms;
1324 let timer_name = core.market_exit_timer_name;
1325
1326 log::info!("{strategy_id} Setting market exit timer at {interval_ms}ms intervals");
1327
1328 let interval_ns = interval_ms * 1_000_000;
1329 let result = core.clock().set_timer_ns(
1330 timer_name.as_str(),
1331 interval_ns,
1332 None,
1333 None,
1334 None,
1335 None,
1336 None,
1337 );
1338
1339 if let Err(e) = result {
1340 core.is_exiting = false;
1342 core.market_exit_attempts = 0;
1343 return Err(e);
1344 }
1345
1346 Ok(())
1347 }
1348
1349 fn check_market_exit(&mut self, _event: TimeEvent) {
1353 if !self.is_exiting() {
1355 return;
1356 }
1357
1358 let core = self.core_mut();
1359 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
1360
1361 core.market_exit_attempts += 1;
1362 let attempts = core.market_exit_attempts;
1363 let max_attempts = core.config.market_exit_max_attempts;
1364
1365 log::debug!(
1366 "{strategy_id} Market exit check triggered (attempt {attempts}/{max_attempts})"
1367 );
1368
1369 if attempts >= max_attempts {
1370 let cache = core.cache();
1371 let open_orders_count = cache
1372 .orders_open(None, None, Some(&strategy_id), None, None)
1373 .len();
1374 let inflight_orders_count = cache
1375 .orders_inflight(None, None, Some(&strategy_id), None, None)
1376 .len();
1377 let open_positions_count = cache
1378 .positions_open(None, None, Some(&strategy_id), None, None)
1379 .len();
1380 drop(cache);
1381
1382 log::warn!(
1383 "{strategy_id} Market exit max attempts ({max_attempts}) reached, \
1384 completing with open orders: {open_orders_count}, \
1385 inflight orders: {inflight_orders_count}, \
1386 open positions: {open_positions_count}"
1387 );
1388
1389 self.finalize_market_exit();
1390 return;
1391 }
1392
1393 let cache = core.cache();
1394 let open_orders = cache.orders_open(None, None, Some(&strategy_id), None, None);
1395 let inflight_orders = cache.orders_inflight(None, None, Some(&strategy_id), None, None);
1396
1397 if !open_orders.is_empty() || !inflight_orders.is_empty() {
1398 return;
1399 }
1400
1401 let open_positions = cache.positions_open(None, None, Some(&strategy_id), None, None);
1402
1403 if !open_positions.is_empty() {
1404 let positions_data: Vec<_> = open_positions
1406 .iter()
1407 .map(|p| (p.id, p.instrument_id, p.side, p.quantity, p.is_closed()))
1408 .collect();
1409
1410 drop(cache);
1411
1412 for (pos_id, instrument_id, side, quantity, is_closed) in positions_data {
1413 if is_closed {
1414 continue;
1415 }
1416
1417 let core = self.core_mut();
1418 let time_in_force = core.config.market_exit_time_in_force;
1419 let reduce_only = core.config.market_exit_reduce_only;
1420 let market_exit_tag = core.market_exit_tag;
1421 let closing_side = OrderCore::closing_side(side);
1422 let order = core.order_factory().market(
1423 instrument_id,
1424 closing_side,
1425 quantity,
1426 Some(time_in_force),
1427 Some(reduce_only),
1428 None,
1429 None,
1430 None,
1431 Some(vec![market_exit_tag]),
1432 None,
1433 );
1434
1435 if let Err(e) = self.submit_order(order, Some(pos_id), None) {
1436 log::error!("Error re-submitting close order for position {pos_id}: {e}");
1437 }
1438 }
1439 return;
1440 }
1441
1442 drop(cache);
1443 self.finalize_market_exit();
1444 }
1445
1446 fn finalize_market_exit(&mut self) {
1451 let (strategy_id, should_stop) = {
1452 let core = self.core_mut();
1453 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
1454 let should_stop = core.pending_stop;
1455 (strategy_id, should_stop)
1456 };
1457
1458 self.cancel_market_exit();
1459
1460 let hook_result = catch_unwind(AssertUnwindSafe(|| {
1461 self.post_market_exit();
1462 }));
1463
1464 if let Err(e) = hook_result {
1465 log::error!("{strategy_id} Error in post_market_exit: {e:?}");
1466 }
1467
1468 if should_stop {
1469 log::info!("{strategy_id} Market exit complete, stopping strategy");
1470
1471 if let Err(e) = Component::stop(self) {
1472 log::error!("{strategy_id} Failed to stop: {e}");
1473 }
1474 }
1475
1476 let core = self.core_mut();
1477 debug_assert!(
1478 !(core.pending_stop
1479 && !core.is_exiting
1480 && core.actor.state() == ComponentState::Running),
1481 "INVARIANT: stuck state after finalize_market_exit"
1482 );
1483 }
1484
1485 fn cancel_market_exit(&mut self) {
1489 let core = self.core_mut();
1490 let timer_name = core.market_exit_timer_name;
1491
1492 if core.clock().timer_names().contains(&timer_name.as_str()) {
1493 core.clock().cancel_timer(timer_name.as_str());
1494 }
1495
1496 core.is_exiting = false;
1497 core.pending_stop = false;
1498 core.market_exit_attempts = 0;
1499 }
1500
1501 fn stop(&mut self) -> bool {
1513 let (manage_stop, is_exiting, should_initiate_exit) = {
1514 let core = self.core_mut();
1515 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
1516 let manage_stop = core.config.manage_stop;
1517 let state = core.actor.state();
1518 let pending_stop = core.pending_stop;
1519 let is_exiting = core.is_exiting;
1520
1521 if manage_stop {
1522 if state != ComponentState::Running {
1523 return true; }
1525
1526 if pending_stop {
1527 return false; }
1529
1530 core.pending_stop = true;
1531 let should_initiate_exit = !is_exiting;
1532
1533 if should_initiate_exit {
1534 log::info!("{strategy_id} Initiating market exit before stop");
1535 }
1536
1537 (manage_stop, is_exiting, should_initiate_exit)
1538 } else {
1539 (manage_stop, is_exiting, false)
1540 }
1541 };
1542
1543 if manage_stop {
1544 if should_initiate_exit && let Err(e) = self.market_exit() {
1545 log::warn!("Market exit failed during stop: {e}, proceeding with stop");
1546 self.core_mut().pending_stop = false;
1547 return true;
1548 }
1549 debug_assert!(
1550 self.is_exiting(),
1551 "INVARIANT: deferring stop but not exiting"
1552 );
1553 return false; }
1555
1556 if is_exiting {
1558 self.cancel_market_exit();
1559 }
1560
1561 true }
1563
1564 fn deny_order(&mut self, order: &OrderAny, reason: Ustr) {
1569 let core = self.core_mut();
1570 let trader_id = core.trader_id().expect("Trader ID not set");
1571 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
1572 let ts_now = core.clock().timestamp_ns();
1573
1574 let event = OrderDenied::new(
1575 trader_id,
1576 strategy_id,
1577 order.instrument_id(),
1578 order.client_order_id(),
1579 reason,
1580 UUID4::new(),
1581 ts_now,
1582 ts_now,
1583 );
1584
1585 log::warn!(
1586 "{strategy_id} Order {} denied: {reason}",
1587 order.client_order_id()
1588 );
1589
1590 {
1592 let cache_rc = core.cache_rc();
1593 let mut cache = cache_rc.borrow_mut();
1594 if !cache.order_exists(&order.client_order_id()) {
1595 let _ = cache.add_order(order.clone(), None, None, true);
1596 }
1597 }
1598
1599 let mut order_clone = order.clone();
1601 if let Err(e) = order_clone.apply(OrderEventAny::Denied(event)) {
1602 log::warn!("Failed to apply OrderDenied event: {e}");
1603 return;
1604 }
1605
1606 {
1607 let cache_rc = core.cache_rc();
1608 let mut cache = cache_rc.borrow_mut();
1609 let _ = cache.update_order(&order_clone);
1610 }
1611 }
1612
1613 fn deny_order_list(&mut self, orders: &[OrderAny], reason: Ustr) {
1617 for order in orders {
1618 if !order.is_closed() {
1619 self.deny_order(order, reason);
1620 }
1621 }
1622 }
1623
1624 fn set_gtd_expiry(&mut self, order: &OrderAny) -> anyhow::Result<()> {
1634 let core = self.core_mut();
1635
1636 if !core.config.manage_gtd_expiry || order.time_in_force() != TimeInForce::Gtd {
1637 return Ok(());
1638 }
1639
1640 let Some(expire_time) = order.expire_time() else {
1641 return Ok(());
1642 };
1643
1644 let client_order_id = order.client_order_id();
1645 let timer_name = format!("GTD-EXPIRY:{client_order_id}");
1646
1647 let current_time_ns = {
1648 let clock = core.clock();
1649 clock.timestamp_ns()
1650 };
1651
1652 if current_time_ns >= expire_time.as_u64() {
1653 log::info!("GTD order {client_order_id} already expired, canceling immediately");
1654 return self.cancel_order(order.clone(), None);
1655 }
1656
1657 {
1658 let mut clock = core.clock();
1659 clock.set_time_alert_ns(&timer_name, expire_time, None, None)?;
1660 }
1661
1662 core.gtd_timers
1663 .insert(client_order_id, Ustr::from(&timer_name));
1664
1665 log::debug!("Set GTD expiry timer for {client_order_id} at {expire_time}");
1666 Ok(())
1667 }
1668
1669 fn cancel_gtd_expiry(&mut self, client_order_id: &ClientOrderId) {
1671 let core = self.core_mut();
1672
1673 if let Some(timer_name) = core.gtd_timers.remove(client_order_id) {
1674 core.clock().cancel_timer(timer_name.as_str());
1675 log::debug!("Canceled GTD expiry timer for {client_order_id}");
1676 }
1677 }
1678
1679 fn has_gtd_expiry_timer(&mut self, client_order_id: &ClientOrderId) -> bool {
1681 let core = self.core_mut();
1682 core.gtd_timers.contains_key(client_order_id)
1683 }
1684
1685 fn expire_gtd_order(&mut self, event: TimeEvent) {
1689 let timer_name = event.name.to_string();
1690 let Some(client_order_id_str) = timer_name.strip_prefix("GTD-EXPIRY:") else {
1691 log::error!("Invalid GTD timer name format: {timer_name}");
1692 return;
1693 };
1694
1695 let client_order_id = ClientOrderId::from(client_order_id_str);
1696
1697 let core = self.core_mut();
1698 core.gtd_timers.remove(&client_order_id);
1699
1700 let cache = core.cache();
1701 let Some(order) = cache.order(&client_order_id) else {
1702 log::warn!("GTD order {client_order_id} not found in cache");
1703 return;
1704 };
1705
1706 let order = order.clone();
1707 drop(cache);
1708
1709 log::info!("GTD order {client_order_id} expired");
1710
1711 if let Err(e) = self.cancel_order(order, None) {
1712 log::error!("Failed to cancel expired GTD order {client_order_id}: {e}");
1713 }
1714 }
1715
1716 fn reactivate_gtd_timers(&mut self) {
1721 let core = self.core_mut();
1722 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
1723 let current_time_ns = core.clock().timestamp_ns();
1724 let cache = core.cache();
1725
1726 let open_orders = cache.orders_open(None, None, Some(&strategy_id), None, None);
1727
1728 let gtd_orders: Vec<_> = open_orders
1729 .iter()
1730 .filter(|o| o.time_in_force() == TimeInForce::Gtd)
1731 .map(|o| (*o).clone())
1732 .collect();
1733
1734 drop(cache);
1735
1736 for order in gtd_orders {
1737 let Some(expire_time) = order.expire_time() else {
1738 continue;
1739 };
1740
1741 let expire_time_ns = expire_time.as_u64();
1742 let client_order_id = order.client_order_id();
1743
1744 if current_time_ns >= expire_time_ns {
1745 log::info!("GTD order {client_order_id} already expired, canceling immediately");
1746 if let Err(e) = self.cancel_order(order, None) {
1747 log::error!("Failed to cancel expired GTD order {client_order_id}: {e}");
1748 }
1749 } else if let Err(e) = self.set_gtd_expiry(&order) {
1750 log::error!("Failed to set GTD expiry timer for {client_order_id}: {e}");
1751 }
1752 }
1753 }
1754}
1755
1756#[cfg(test)]
1757mod tests {
1758 use std::{cell::RefCell, rc::Rc};
1759
1760 use nautilus_common::{
1761 actor::DataActor,
1762 cache::Cache,
1763 clock::{Clock, TestClock},
1764 component::Component,
1765 msgbus::{
1766 self, MessagingSwitchboard,
1767 stubs::{TypedIntoMessageSavingHandler, get_typed_into_message_saving_handler},
1768 },
1769 timer::{TimeEvent, TimeEventCallback},
1770 };
1771 use nautilus_core::UnixNanos;
1772 use nautilus_model::{
1773 enums::{LiquiditySide, OrderSide, OrderType, PositionSide},
1774 events::{OrderAccepted, OrderCanceled, OrderFilled, OrderRejected},
1775 identifiers::{
1776 AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, TraderId,
1777 VenueOrderId,
1778 },
1779 orders::MarketOrder,
1780 stubs::TestDefault,
1781 types::{Currency, Money},
1782 };
1783 use nautilus_portfolio::portfolio::Portfolio;
1784 use rstest::rstest;
1785
1786 use super::*;
1787 use crate::nautilus_strategy;
1788
1789 #[derive(Debug)]
1790 struct TestStrategy {
1791 core: StrategyCore,
1792 on_order_rejected_called: bool,
1793 on_order_accepted_called: bool,
1794 on_order_canceled_called: bool,
1795 on_order_filled_called: bool,
1796 on_order_expired_called: bool,
1797 on_position_opened_called: bool,
1798 on_position_changed_called: bool,
1799 on_position_closed_called: bool,
1800 }
1801
1802 impl TestStrategy {
1803 fn new(config: StrategyConfig) -> Self {
1804 Self {
1805 core: StrategyCore::new(config),
1806 on_order_rejected_called: false,
1807 on_order_accepted_called: false,
1808 on_order_canceled_called: false,
1809 on_order_filled_called: false,
1810 on_order_expired_called: false,
1811 on_position_opened_called: false,
1812 on_position_changed_called: false,
1813 on_position_closed_called: false,
1814 }
1815 }
1816 }
1817
1818 impl DataActor for TestStrategy {
1819 fn on_order_canceled(&mut self, _event: &OrderCanceled) -> anyhow::Result<()> {
1820 self.on_order_canceled_called = true;
1821 Ok(())
1822 }
1823
1824 fn on_order_filled(&mut self, _event: &OrderFilled) -> anyhow::Result<()> {
1825 self.on_order_filled_called = true;
1826 Ok(())
1827 }
1828 }
1829
1830 nautilus_strategy!(TestStrategy, {
1831 fn on_order_rejected(&mut self, _event: OrderRejected) {
1832 self.on_order_rejected_called = true;
1833 }
1834
1835 fn on_order_accepted(&mut self, _event: OrderAccepted) {
1836 self.on_order_accepted_called = true;
1837 }
1838
1839 fn on_order_expired(&mut self, _event: OrderExpired) {
1840 self.on_order_expired_called = true;
1841 }
1842
1843 fn on_position_opened(&mut self, _event: PositionOpened) {
1844 self.on_position_opened_called = true;
1845 }
1846
1847 fn on_position_changed(&mut self, _event: PositionChanged) {
1848 self.on_position_changed_called = true;
1849 }
1850
1851 fn on_position_closed(&mut self, _event: PositionClosed) {
1852 self.on_position_closed_called = true;
1853 }
1854 });
1855
1856 fn create_test_strategy() -> TestStrategy {
1857 let config = StrategyConfig {
1858 strategy_id: Some(StrategyId::from("TEST-001")),
1859 order_id_tag: Some("001".to_string()),
1860 ..Default::default()
1861 };
1862 TestStrategy::new(config)
1863 }
1864
1865 fn register_strategy(strategy: &mut TestStrategy) {
1866 let trader_id = TraderId::from("TRADER-001");
1867 let clock = Rc::new(RefCell::new(TestClock::new()));
1868 let cache = Rc::new(RefCell::new(Cache::default()));
1869 let portfolio = Rc::new(RefCell::new(Portfolio::new(
1870 cache.clone(),
1871 clock.clone(),
1872 None,
1873 )));
1874
1875 strategy
1876 .core
1877 .register(trader_id, clock, cache, portfolio)
1878 .unwrap();
1879 strategy.initialize().unwrap();
1880 }
1881
1882 fn start_strategy(strategy: &mut TestStrategy) {
1883 strategy.start().unwrap();
1884 }
1885
1886 fn stop_strategy(strategy: &mut TestStrategy) {
1887 Component::stop(strategy).unwrap();
1888 }
1889
1890 fn make_filled(client_order_id: ClientOrderId) -> OrderEventAny {
1891 OrderEventAny::Filled(OrderFilled {
1892 trader_id: TraderId::from("TRADER-001"),
1893 strategy_id: StrategyId::from("TEST-001"),
1894 instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
1895 client_order_id,
1896 venue_order_id: VenueOrderId::test_default(),
1897 account_id: AccountId::from("ACC-001"),
1898 trade_id: TradeId::test_default(),
1899 position_id: None,
1900 order_side: OrderSide::Buy,
1901 order_type: OrderType::Market,
1902 last_qty: Quantity::default(),
1903 last_px: Price::default(),
1904 currency: Currency::from("USD"),
1905 liquidity_side: LiquiditySide::Taker,
1906 event_id: UUID4::default(),
1907 ts_event: UnixNanos::default(),
1908 ts_init: UnixNanos::default(),
1909 reconciliation: false,
1910 commission: None,
1911 })
1912 }
1913
1914 fn make_canceled(client_order_id: ClientOrderId) -> OrderEventAny {
1915 OrderEventAny::Canceled(OrderCanceled {
1916 trader_id: TraderId::from("TRADER-001"),
1917 strategy_id: StrategyId::from("TEST-001"),
1918 instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
1919 client_order_id,
1920 venue_order_id: None,
1921 account_id: Some(AccountId::from("ACC-001")),
1922 event_id: UUID4::default(),
1923 ts_event: UnixNanos::default(),
1924 ts_init: UnixNanos::default(),
1925 reconciliation: 0,
1926 })
1927 }
1928
1929 fn make_rejected(client_order_id: ClientOrderId) -> OrderEventAny {
1930 OrderEventAny::Rejected(OrderRejected {
1931 trader_id: TraderId::from("TRADER-001"),
1932 strategy_id: StrategyId::from("TEST-001"),
1933 instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
1934 client_order_id,
1935 account_id: AccountId::from("ACC-001"),
1936 reason: "Test rejection".into(),
1937 event_id: UUID4::default(),
1938 ts_event: UnixNanos::default(),
1939 ts_init: UnixNanos::default(),
1940 reconciliation: 0,
1941 due_post_only: 0,
1942 })
1943 }
1944
1945 fn make_expired(client_order_id: ClientOrderId) -> OrderEventAny {
1946 OrderEventAny::Expired(OrderExpired {
1947 trader_id: TraderId::from("TRADER-001"),
1948 strategy_id: StrategyId::from("TEST-001"),
1949 instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
1950 client_order_id,
1951 venue_order_id: None,
1952 account_id: Some(AccountId::from("ACC-001")),
1953 event_id: UUID4::default(),
1954 ts_event: UnixNanos::default(),
1955 ts_init: UnixNanos::default(),
1956 reconciliation: 0,
1957 })
1958 }
1959
1960 fn make_accepted(client_order_id: ClientOrderId) -> OrderEventAny {
1961 OrderEventAny::Accepted(OrderAccepted {
1962 trader_id: TraderId::from("TRADER-001"),
1963 strategy_id: StrategyId::from("TEST-001"),
1964 instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
1965 client_order_id,
1966 venue_order_id: VenueOrderId::test_default(),
1967 account_id: AccountId::from("ACC-001"),
1968 event_id: UUID4::default(),
1969 ts_event: UnixNanos::default(),
1970 ts_init: UnixNanos::default(),
1971 reconciliation: 0,
1972 })
1973 }
1974
1975 fn make_position_opened() -> PositionEvent {
1976 PositionEvent::PositionOpened(PositionOpened {
1977 trader_id: TraderId::from("TRADER-001"),
1978 strategy_id: StrategyId::from("TEST-001"),
1979 instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
1980 position_id: PositionId::test_default(),
1981 account_id: AccountId::from("ACC-001"),
1982 opening_order_id: ClientOrderId::from("O-001"),
1983 entry: OrderSide::Buy,
1984 side: PositionSide::Long,
1985 signed_qty: 1.0,
1986 quantity: Quantity::default(),
1987 last_qty: Quantity::default(),
1988 last_px: Price::default(),
1989 currency: Currency::from("USD"),
1990 avg_px_open: 0.0,
1991 event_id: UUID4::default(),
1992 ts_event: UnixNanos::default(),
1993 ts_init: UnixNanos::default(),
1994 })
1995 }
1996
1997 fn make_position_changed() -> PositionEvent {
1998 let currency = Currency::from("USD");
1999 PositionEvent::PositionChanged(PositionChanged {
2000 trader_id: TraderId::from("TRADER-001"),
2001 strategy_id: StrategyId::from("TEST-001"),
2002 instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
2003 position_id: PositionId::test_default(),
2004 account_id: AccountId::from("ACC-001"),
2005 opening_order_id: ClientOrderId::from("O-001"),
2006 entry: OrderSide::Buy,
2007 side: PositionSide::Long,
2008 signed_qty: 2.0,
2009 quantity: Quantity::default(),
2010 peak_quantity: Quantity::default(),
2011 last_qty: Quantity::default(),
2012 last_px: Price::default(),
2013 currency,
2014 avg_px_open: 0.0,
2015 avg_px_close: None,
2016 realized_return: 0.0,
2017 realized_pnl: None,
2018 unrealized_pnl: Money::new(0.0, currency),
2019 event_id: UUID4::default(),
2020 ts_opened: UnixNanos::default(),
2021 ts_event: UnixNanos::default(),
2022 ts_init: UnixNanos::default(),
2023 })
2024 }
2025
2026 fn make_position_closed() -> PositionEvent {
2027 let currency = Currency::from("USD");
2028 PositionEvent::PositionClosed(PositionClosed {
2029 trader_id: TraderId::from("TRADER-001"),
2030 strategy_id: StrategyId::from("TEST-001"),
2031 instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
2032 position_id: PositionId::test_default(),
2033 account_id: AccountId::from("ACC-001"),
2034 opening_order_id: ClientOrderId::from("O-001"),
2035 closing_order_id: Some(ClientOrderId::from("O-002")),
2036 entry: OrderSide::Buy,
2037 side: PositionSide::Flat,
2038 signed_qty: 0.0,
2039 quantity: Quantity::default(),
2040 peak_quantity: Quantity::default(),
2041 last_qty: Quantity::default(),
2042 last_px: Price::default(),
2043 currency,
2044 avg_px_open: 0.0,
2045 avg_px_close: None,
2046 realized_return: 0.0,
2047 realized_pnl: None,
2048 unrealized_pnl: Money::new(0.0, currency),
2049 duration: 0,
2050 event_id: UUID4::default(),
2051 ts_opened: UnixNanos::default(),
2052 ts_closed: None,
2053 ts_event: UnixNanos::default(),
2054 ts_init: UnixNanos::default(),
2055 })
2056 }
2057
2058 #[rstest]
2059 fn test_strategy_creation() {
2060 let strategy = create_test_strategy();
2061 assert_eq!(
2062 strategy.core.config.strategy_id,
2063 Some(StrategyId::from("TEST-001"))
2064 );
2065 assert!(!strategy.on_order_rejected_called);
2066 assert!(!strategy.on_position_opened_called);
2067 }
2068
2069 #[rstest]
2070 fn test_strategy_registration() {
2071 let mut strategy = create_test_strategy();
2072 register_strategy(&mut strategy);
2073
2074 assert!(strategy.core.order_manager.is_some());
2075 assert!(strategy.core.order_factory.is_some());
2076 assert!(strategy.core.portfolio.is_some());
2077 }
2078
2079 #[rstest]
2080 fn test_handle_order_event_dispatches_to_handler() {
2081 let mut strategy = create_test_strategy();
2082 register_strategy(&mut strategy);
2083 start_strategy(&mut strategy);
2084
2085 let event = OrderEventAny::Rejected(OrderRejected {
2086 trader_id: TraderId::from("TRADER-001"),
2087 strategy_id: StrategyId::from("TEST-001"),
2088 instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
2089 client_order_id: ClientOrderId::from("O-001"),
2090 account_id: AccountId::from("ACC-001"),
2091 reason: "Test rejection".into(),
2092 event_id: UUID4::default(),
2093 ts_event: UnixNanos::default(),
2094 ts_init: UnixNanos::default(),
2095 reconciliation: 0,
2096 due_post_only: 0,
2097 });
2098
2099 strategy.handle_order_event(event);
2100
2101 assert!(strategy.on_order_rejected_called);
2102 }
2103
2104 #[rstest]
2105 #[case::opened(make_position_opened())]
2106 #[case::changed(make_position_changed())]
2107 #[case::closed(make_position_closed())]
2108 fn test_handle_position_event_dispatches_to_handler(#[case] event: PositionEvent) {
2109 let mut strategy = create_test_strategy();
2110 register_strategy(&mut strategy);
2111 start_strategy(&mut strategy);
2112
2113 let expected_opened = matches!(event, PositionEvent::PositionOpened(_));
2114 let expected_changed = matches!(event, PositionEvent::PositionChanged(_));
2115 let expected_closed = matches!(event, PositionEvent::PositionClosed(_));
2116
2117 strategy.handle_position_event(event);
2118
2119 assert_eq!(strategy.on_position_opened_called, expected_opened);
2120 assert_eq!(strategy.on_position_changed_called, expected_changed);
2121 assert_eq!(strategy.on_position_closed_called, expected_closed);
2122 }
2123
2124 #[rstest]
2125 fn test_handle_position_event_skips_dispatch_when_stopped() {
2126 let mut strategy = create_test_strategy();
2127 register_strategy(&mut strategy);
2128 start_strategy(&mut strategy);
2129 stop_strategy(&mut strategy);
2130 assert_eq!(strategy.core.actor.state(), ComponentState::Stopped);
2131
2132 strategy.handle_position_event(make_position_opened());
2133
2134 assert!(!strategy.on_position_opened_called);
2135 }
2136
2137 #[rstest]
2138 fn test_strategy_default_handlers_do_not_panic() {
2139 let mut strategy = create_test_strategy();
2140
2141 strategy.on_order_initialized(OrderInitialized::default());
2142 strategy.on_order_denied(OrderDenied::default());
2143 strategy.on_order_emulated(OrderEmulated::default());
2144 strategy.on_order_released(OrderReleased::default());
2145 strategy.on_order_submitted(OrderSubmitted::default());
2146 strategy.on_order_rejected(OrderRejected::default());
2147 let _ = DataActor::on_order_canceled(&mut strategy, &OrderCanceled::default());
2148 strategy.on_order_expired(OrderExpired::default());
2149 strategy.on_order_triggered(OrderTriggered::default());
2150 strategy.on_order_pending_update(OrderPendingUpdate::default());
2151 strategy.on_order_pending_cancel(OrderPendingCancel::default());
2152 strategy.on_order_modify_rejected(OrderModifyRejected::default());
2153 strategy.on_order_cancel_rejected(OrderCancelRejected::default());
2154 strategy.on_order_updated(OrderUpdated::default());
2155 }
2156
2157 #[rstest]
2158 fn test_modify_order_routes_non_emulated_orders_to_risk() {
2159 let mut strategy = create_test_strategy();
2160 register_strategy(&mut strategy);
2161
2162 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
2163 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
2164 msgbus::register_trading_command_endpoint(
2165 MessagingSwitchboard::risk_engine_queue_execute(),
2166 risk_handler,
2167 );
2168
2169 let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
2170 get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
2171 msgbus::register_trading_command_endpoint(
2172 MessagingSwitchboard::exec_engine_queue_execute(),
2173 exec_handler,
2174 );
2175
2176 let order = OrderAny::Market(MarketOrder::new(
2177 TraderId::from("TRADER-001"),
2178 StrategyId::from("TEST-001"),
2179 InstrumentId::from("BTCUSDT.BINANCE"),
2180 ClientOrderId::from("O-20250208-0003"),
2181 OrderSide::Buy,
2182 Quantity::from(100_000),
2183 TimeInForce::Gtc,
2184 UUID4::new(),
2185 UnixNanos::default(),
2186 false,
2187 false,
2188 None,
2189 None,
2190 None,
2191 None,
2192 None,
2193 None,
2194 None,
2195 None,
2196 ));
2197
2198 strategy
2199 .modify_order(order, Some(Quantity::from(200_000)), None, None, None)
2200 .unwrap();
2201
2202 let risk_messages = risk_messages.get_messages();
2203 let exec_messages = exec_messages.get_messages();
2204
2205 assert_eq!(risk_messages.len(), 1);
2206 assert!(matches!(
2207 risk_messages.first(),
2208 Some(TradingCommand::ModifyOrder(_))
2209 ));
2210 assert!(exec_messages.is_empty());
2211 }
2212
2213 #[rstest]
2216 fn test_has_gtd_expiry_timer_when_timer_not_set() {
2217 let mut strategy = create_test_strategy();
2218 let client_order_id = ClientOrderId::from("O-001");
2219
2220 assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
2221 }
2222
2223 #[rstest]
2224 fn test_has_gtd_expiry_timer_when_timer_set() {
2225 let mut strategy = create_test_strategy();
2226 let client_order_id = ClientOrderId::from("O-001");
2227
2228 strategy
2229 .core
2230 .gtd_timers
2231 .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
2232
2233 assert!(strategy.has_gtd_expiry_timer(&client_order_id));
2234 }
2235
2236 #[rstest]
2237 fn test_cancel_gtd_expiry_removes_timer() {
2238 let mut strategy = create_test_strategy();
2239 register_strategy(&mut strategy);
2240
2241 let client_order_id = ClientOrderId::from("O-001");
2242 strategy
2243 .core
2244 .gtd_timers
2245 .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
2246
2247 strategy.cancel_gtd_expiry(&client_order_id);
2248
2249 assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
2250 }
2251
2252 #[rstest]
2253 fn test_cancel_gtd_expiry_when_timer_not_set() {
2254 let mut strategy = create_test_strategy();
2255 register_strategy(&mut strategy);
2256
2257 let client_order_id = ClientOrderId::from("O-001");
2258
2259 strategy.cancel_gtd_expiry(&client_order_id);
2260
2261 assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
2262 }
2263
2264 #[rstest]
2265 #[case::filled(make_filled)]
2266 #[case::canceled(make_canceled)]
2267 #[case::rejected(make_rejected)]
2268 #[case::expired(make_expired)]
2269 fn test_handle_order_event_cancels_gtd_timer_for_terminal_event(
2270 #[case] make_event: fn(ClientOrderId) -> OrderEventAny,
2271 ) {
2272 let mut strategy = create_test_strategy();
2273 register_strategy(&mut strategy);
2274 start_strategy(&mut strategy);
2275
2276 let client_order_id = ClientOrderId::from("O-001");
2277 strategy
2278 .core
2279 .gtd_timers
2280 .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
2281
2282 strategy.handle_order_event(make_event(client_order_id));
2283
2284 assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
2285 }
2286
2287 #[rstest]
2288 #[case::filled(make_filled)]
2289 #[case::canceled(make_canceled)]
2290 #[case::rejected(make_rejected)]
2291 #[case::expired(make_expired)]
2292 fn test_handle_order_event_cancels_gtd_timer_when_stopped(
2293 #[case] make_event: fn(ClientOrderId) -> OrderEventAny,
2294 ) {
2295 let mut strategy = create_test_strategy();
2296 register_strategy(&mut strategy);
2297 start_strategy(&mut strategy);
2298
2299 let client_order_id = ClientOrderId::from("O-001");
2300 strategy
2301 .core
2302 .gtd_timers
2303 .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
2304
2305 stop_strategy(&mut strategy);
2306 assert_eq!(strategy.core.actor.state(), ComponentState::Stopped);
2307
2308 strategy.handle_order_event(make_event(client_order_id));
2309
2310 assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
2311 }
2312
2313 #[rstest]
2314 fn test_handle_order_event_skips_gtd_cancel_for_non_terminal() {
2315 let mut strategy = create_test_strategy();
2316 register_strategy(&mut strategy);
2317 start_strategy(&mut strategy);
2318
2319 let client_order_id = ClientOrderId::from("O-001");
2320 strategy
2321 .core
2322 .gtd_timers
2323 .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
2324
2325 strategy.handle_order_event(make_accepted(client_order_id));
2326
2327 assert!(strategy.has_gtd_expiry_timer(&client_order_id));
2328 }
2329
2330 #[rstest]
2331 fn test_handle_order_event_skips_dispatch_when_stopped() {
2332 let mut strategy = create_test_strategy();
2333 register_strategy(&mut strategy);
2334 start_strategy(&mut strategy);
2335 stop_strategy(&mut strategy);
2336 assert_eq!(strategy.core.actor.state(), ComponentState::Stopped);
2337
2338 strategy.handle_order_event(make_rejected(ClientOrderId::from("O-001")));
2339
2340 assert!(!strategy.on_order_rejected_called);
2341 }
2342
2343 #[rstest]
2344 fn test_on_start_calls_reactivate_gtd_timers_when_enabled() {
2345 let config = StrategyConfig {
2346 strategy_id: Some(StrategyId::from("TEST-001")),
2347 order_id_tag: Some("001".to_string()),
2348 manage_gtd_expiry: true,
2349 ..Default::default()
2350 };
2351 let mut strategy = TestStrategy::new(config);
2352 register_strategy(&mut strategy);
2353
2354 let result = Strategy::on_start(&mut strategy);
2355 assert!(result.is_ok());
2356 }
2357
2358 #[rstest]
2359 fn test_on_start_does_not_panic_when_gtd_disabled() {
2360 let config = StrategyConfig {
2361 strategy_id: Some(StrategyId::from("TEST-001")),
2362 order_id_tag: Some("001".to_string()),
2363 manage_gtd_expiry: false,
2364 ..Default::default()
2365 };
2366 let mut strategy = TestStrategy::new(config);
2367 register_strategy(&mut strategy);
2368
2369 let result = Strategy::on_start(&mut strategy);
2370 assert!(result.is_ok());
2371 }
2372
2373 #[rstest]
2376 fn test_query_account_when_registered() {
2377 let mut strategy = create_test_strategy();
2378 register_strategy(&mut strategy);
2379
2380 let account_id = AccountId::from("ACC-001");
2381
2382 let result = strategy.query_account(account_id, None, None);
2383
2384 assert!(result.is_ok());
2385 }
2386
2387 #[rstest]
2388 fn test_query_account_with_client_id() {
2389 let mut strategy = create_test_strategy();
2390 register_strategy(&mut strategy);
2391
2392 let account_id = AccountId::from("ACC-001");
2393 let client_id = ClientId::from("BINANCE");
2394
2395 let result = strategy.query_account(account_id, Some(client_id), None);
2396
2397 assert!(result.is_ok());
2398 }
2399
2400 #[rstest]
2401 fn test_query_order_when_registered() {
2402 let mut strategy = create_test_strategy();
2403 register_strategy(&mut strategy);
2404
2405 let order = OrderAny::Market(MarketOrder::test_default());
2406
2407 let result = strategy.query_order(&order, None, None);
2408
2409 assert!(result.is_ok());
2410 }
2411
2412 #[rstest]
2413 fn test_query_order_with_client_id() {
2414 let mut strategy = create_test_strategy();
2415 register_strategy(&mut strategy);
2416
2417 let order = OrderAny::Market(MarketOrder::test_default());
2418 let client_id = ClientId::from("BINANCE");
2419
2420 let result = strategy.query_order(&order, Some(client_id), None);
2421
2422 assert!(result.is_ok());
2423 }
2424
2425 #[rstest]
2426 fn test_is_exiting_returns_false_by_default() {
2427 let strategy = create_test_strategy();
2428 assert!(!strategy.is_exiting());
2429 }
2430
2431 #[rstest]
2432 fn test_is_exiting_returns_true_when_set_manually() {
2433 let mut strategy = create_test_strategy();
2434 register_strategy(&mut strategy);
2435
2436 strategy.core.is_exiting = true;
2438
2439 assert!(strategy.is_exiting());
2440 }
2441
2442 #[rstest]
2443 fn test_market_exit_sets_is_exiting_flag() {
2444 let mut strategy = create_test_strategy();
2446 register_strategy(&mut strategy);
2447
2448 assert!(!strategy.core.is_exiting);
2449
2450 strategy.core.is_exiting = true;
2452 strategy.core.market_exit_attempts = 0;
2453
2454 assert!(strategy.core.is_exiting);
2455 assert_eq!(strategy.core.market_exit_attempts, 0);
2456 }
2457
2458 #[rstest]
2459 fn test_market_exit_uses_config_time_in_force_and_reduce_only() {
2460 let config = StrategyConfig {
2461 strategy_id: Some(StrategyId::from("TEST-001")),
2462 order_id_tag: Some("001".to_string()),
2463 market_exit_time_in_force: TimeInForce::Ioc,
2464 market_exit_reduce_only: false,
2465 ..Default::default()
2466 };
2467 let strategy = TestStrategy::new(config);
2468
2469 assert_eq!(
2470 strategy.core.config.market_exit_time_in_force,
2471 TimeInForce::Ioc
2472 );
2473 assert!(!strategy.core.config.market_exit_reduce_only);
2474 }
2475
2476 #[rstest]
2477 fn test_market_exit_resets_attempt_counter() {
2478 let mut strategy = create_test_strategy();
2479 register_strategy(&mut strategy);
2480
2481 strategy.core.market_exit_attempts = 50;
2483
2484 strategy.core.reset_market_exit_state();
2486
2487 assert_eq!(strategy.core.market_exit_attempts, 0);
2488 }
2489
2490 #[rstest]
2491 fn test_market_exit_second_call_returns_early_when_exiting() {
2492 let mut strategy = create_test_strategy();
2493 register_strategy(&mut strategy);
2494
2495 strategy.core.is_exiting = true;
2497
2498 let result = strategy.market_exit();
2500 assert!(result.is_ok());
2501 assert!(strategy.core.is_exiting);
2502 }
2503
2504 #[rstest]
2505 fn test_finalize_market_exit_resets_state() {
2506 let mut strategy = create_test_strategy();
2507 register_strategy(&mut strategy);
2508
2509 strategy.core.is_exiting = true;
2511 strategy.core.pending_stop = true;
2512 strategy.core.market_exit_attempts = 50;
2513
2514 strategy.finalize_market_exit();
2515
2516 assert!(!strategy.core.is_exiting);
2517 assert!(!strategy.core.pending_stop);
2518 assert_eq!(strategy.core.market_exit_attempts, 0);
2519 }
2520
2521 #[rstest]
2522 fn test_market_exit_config_defaults() {
2523 let config = StrategyConfig::default();
2524
2525 assert!(!config.manage_stop);
2526 assert_eq!(config.market_exit_interval_ms, 100);
2527 assert_eq!(config.market_exit_max_attempts, 100);
2528 }
2529
2530 #[rstest]
2531 fn test_market_exit_with_custom_config() {
2532 let config = StrategyConfig {
2533 strategy_id: Some(StrategyId::from("TEST-001")),
2534 manage_stop: true,
2535 market_exit_interval_ms: 50,
2536 market_exit_max_attempts: 200,
2537 ..Default::default()
2538 };
2539 let strategy = TestStrategy::new(config);
2540
2541 assert!(strategy.core.config.manage_stop);
2542 assert_eq!(strategy.core.config.market_exit_interval_ms, 50);
2543 assert_eq!(strategy.core.config.market_exit_max_attempts, 200);
2544 }
2545
2546 #[derive(Debug)]
2547 struct MarketExitHookTrackingStrategy {
2548 core: StrategyCore,
2549 on_market_exit_called: bool,
2550 post_market_exit_called: bool,
2551 }
2552
2553 impl MarketExitHookTrackingStrategy {
2554 fn new(config: StrategyConfig) -> Self {
2555 Self {
2556 core: StrategyCore::new(config),
2557 on_market_exit_called: false,
2558 post_market_exit_called: false,
2559 }
2560 }
2561 }
2562
2563 impl DataActor for MarketExitHookTrackingStrategy {}
2564
2565 nautilus_strategy!(MarketExitHookTrackingStrategy, {
2566 fn on_market_exit(&mut self) {
2567 self.on_market_exit_called = true;
2568 }
2569
2570 fn post_market_exit(&mut self) {
2571 self.post_market_exit_called = true;
2572 }
2573 });
2574
2575 #[rstest]
2576 fn test_market_exit_calls_on_market_exit_hook() {
2577 let config = StrategyConfig {
2578 strategy_id: Some(StrategyId::from("TEST-001")),
2579 order_id_tag: Some("001".to_string()),
2580 ..Default::default()
2581 };
2582 let mut strategy = MarketExitHookTrackingStrategy::new(config);
2583
2584 let trader_id = TraderId::from("TRADER-001");
2585 let clock = Rc::new(RefCell::new(TestClock::new()));
2586 let cache = Rc::new(RefCell::new(Cache::default()));
2587 let portfolio = Rc::new(RefCell::new(Portfolio::new(
2588 cache.clone(),
2589 clock.clone(),
2590 None,
2591 )));
2592 strategy
2593 .core
2594 .register(trader_id, clock, cache, portfolio)
2595 .unwrap();
2596 strategy.initialize().unwrap();
2597 strategy.start().unwrap();
2598
2599 let _ = strategy.market_exit();
2600
2601 assert!(strategy.on_market_exit_called);
2602 }
2603
2604 #[rstest]
2605 fn test_finalize_market_exit_calls_post_market_exit_hook() {
2606 let config = StrategyConfig {
2607 strategy_id: Some(StrategyId::from("TEST-001")),
2608 order_id_tag: Some("001".to_string()),
2609 ..Default::default()
2610 };
2611 let mut strategy = MarketExitHookTrackingStrategy::new(config);
2612
2613 let trader_id = TraderId::from("TRADER-001");
2614 let clock = Rc::new(RefCell::new(TestClock::new()));
2615 let cache = Rc::new(RefCell::new(Cache::default()));
2616 let portfolio = Rc::new(RefCell::new(Portfolio::new(
2617 cache.clone(),
2618 clock.clone(),
2619 None,
2620 )));
2621 strategy
2622 .core
2623 .register(trader_id, clock, cache, portfolio)
2624 .unwrap();
2625
2626 strategy.core.is_exiting = true;
2627 strategy.finalize_market_exit();
2628
2629 assert!(strategy.post_market_exit_called);
2630 }
2631
2632 #[derive(Debug)]
2633 struct FailingPostExitStrategy {
2634 core: StrategyCore,
2635 }
2636
2637 impl FailingPostExitStrategy {
2638 fn new(config: StrategyConfig) -> Self {
2639 Self {
2640 core: StrategyCore::new(config),
2641 }
2642 }
2643 }
2644
2645 impl DataActor for FailingPostExitStrategy {}
2646
2647 nautilus_strategy!(FailingPostExitStrategy, {
2648 fn post_market_exit(&mut self) {
2649 panic!("Simulated error in post_market_exit");
2650 }
2651 });
2652
2653 #[rstest]
2654 fn test_finalize_market_exit_handles_hook_panic() {
2655 let config = StrategyConfig {
2656 strategy_id: Some(StrategyId::from("TEST-001")),
2657 order_id_tag: Some("001".to_string()),
2658 ..Default::default()
2659 };
2660 let mut strategy = FailingPostExitStrategy::new(config);
2661
2662 let trader_id = TraderId::from("TRADER-001");
2663 let clock = Rc::new(RefCell::new(TestClock::new()));
2664 let cache = Rc::new(RefCell::new(Cache::default()));
2665 let portfolio = Rc::new(RefCell::new(Portfolio::new(
2666 cache.clone(),
2667 clock.clone(),
2668 None,
2669 )));
2670 strategy
2671 .core
2672 .register(trader_id, clock, cache, portfolio)
2673 .unwrap();
2674
2675 strategy.core.is_exiting = true;
2676 strategy.core.pending_stop = true;
2677
2678 strategy.finalize_market_exit();
2680
2681 assert!(!strategy.core.is_exiting);
2683 assert!(!strategy.core.pending_stop);
2684 }
2685
2686 #[rstest]
2687 fn test_check_market_exit_increments_attempts_before_finalizing() {
2688 let mut strategy = create_test_strategy();
2689 register_strategy(&mut strategy);
2690
2691 strategy.core.is_exiting = true;
2692 assert_eq!(strategy.core.market_exit_attempts, 0);
2693
2694 let event = TimeEvent::new(
2695 Ustr::from("MARKET_EXIT_CHECK:TEST-001"),
2696 UUID4::new(),
2697 UnixNanos::default(),
2698 UnixNanos::default(),
2699 );
2700 strategy.check_market_exit(event);
2701
2702 assert!(!strategy.core.is_exiting);
2706 assert_eq!(strategy.core.market_exit_attempts, 0);
2707 }
2708
2709 #[rstest]
2710 fn test_check_market_exit_finalizes_when_max_attempts_reached() {
2711 let config = StrategyConfig {
2712 strategy_id: Some(StrategyId::from("TEST-001")),
2713 order_id_tag: Some("001".to_string()),
2714 market_exit_max_attempts: 3,
2715 ..Default::default()
2716 };
2717 let mut strategy = TestStrategy::new(config);
2718 register_strategy(&mut strategy);
2719
2720 strategy.core.is_exiting = true;
2721 strategy.core.market_exit_attempts = 2; let event = TimeEvent::new(
2724 Ustr::from("MARKET_EXIT_CHECK:TEST-001"),
2725 UUID4::new(),
2726 UnixNanos::default(),
2727 UnixNanos::default(),
2728 );
2729 strategy.check_market_exit(event);
2730
2731 assert!(!strategy.core.is_exiting);
2733 assert_eq!(strategy.core.market_exit_attempts, 0);
2734 }
2735
2736 #[rstest]
2737 fn test_check_market_exit_finalizes_when_no_orders_or_positions() {
2738 let mut strategy = create_test_strategy();
2739 register_strategy(&mut strategy);
2740
2741 strategy.core.is_exiting = true;
2742
2743 let event = TimeEvent::new(
2744 Ustr::from("MARKET_EXIT_CHECK:TEST-001"),
2745 UUID4::new(),
2746 UnixNanos::default(),
2747 UnixNanos::default(),
2748 );
2749 strategy.check_market_exit(event);
2750
2751 assert!(!strategy.core.is_exiting);
2753 }
2754
2755 #[rstest]
2756 fn test_market_exit_timer_name_format() {
2757 let config = StrategyConfig {
2758 strategy_id: Some(StrategyId::from("MY-STRATEGY-001")),
2759 ..Default::default()
2760 };
2761 let strategy = TestStrategy::new(config);
2762
2763 assert_eq!(
2764 strategy.core.market_exit_timer_name.as_str(),
2765 "MARKET_EXIT_CHECK:MY-STRATEGY-001"
2766 );
2767 }
2768
2769 #[rstest]
2770 fn test_reset_market_exit_state() {
2771 let mut strategy = create_test_strategy();
2772
2773 strategy.core.is_exiting = true;
2774 strategy.core.pending_stop = true;
2775 strategy.core.market_exit_attempts = 50;
2776
2777 strategy.core.reset_market_exit_state();
2778
2779 assert!(!strategy.core.is_exiting);
2780 assert!(!strategy.core.pending_stop);
2781 assert_eq!(strategy.core.market_exit_attempts, 0);
2782 }
2783
2784 #[rstest]
2785 fn test_cancel_market_exit_resets_state_without_hooks() {
2786 let config = StrategyConfig {
2787 strategy_id: Some(StrategyId::from("TEST-001")),
2788 order_id_tag: Some("001".to_string()),
2789 ..Default::default()
2790 };
2791 let mut strategy = MarketExitHookTrackingStrategy::new(config);
2792
2793 let trader_id = TraderId::from("TRADER-001");
2794 let clock = Rc::new(RefCell::new(TestClock::new()));
2795 let cache = Rc::new(RefCell::new(Cache::default()));
2796 let portfolio = Rc::new(RefCell::new(Portfolio::new(
2797 cache.clone(),
2798 clock.clone(),
2799 None,
2800 )));
2801 strategy
2802 .core
2803 .register(trader_id, clock, cache, portfolio)
2804 .unwrap();
2805
2806 strategy.core.is_exiting = true;
2808 strategy.core.pending_stop = true;
2809 strategy.core.market_exit_attempts = 50;
2810
2811 strategy.cancel_market_exit();
2813
2814 assert!(!strategy.core.is_exiting);
2816 assert!(!strategy.core.pending_stop);
2817 assert_eq!(strategy.core.market_exit_attempts, 0);
2818
2819 assert!(!strategy.on_market_exit_called);
2821 assert!(!strategy.post_market_exit_called);
2822 }
2823
2824 #[rstest]
2825 fn test_market_exit_returns_early_when_not_running() {
2826 let mut strategy = create_test_strategy();
2827 register_strategy(&mut strategy);
2828
2829 assert_ne!(strategy.core.actor.state(), ComponentState::Running);
2831
2832 let result = strategy.market_exit();
2833
2834 assert!(result.is_ok());
2836 assert!(!strategy.core.is_exiting);
2837 }
2838
2839 #[rstest]
2840 fn test_stop_with_manage_stop_false_cleans_up_active_exit() {
2841 let config = StrategyConfig {
2842 strategy_id: Some(StrategyId::from("TEST-001")),
2843 order_id_tag: Some("001".to_string()),
2844 manage_stop: false,
2845 ..Default::default()
2846 };
2847 let mut strategy = TestStrategy::new(config);
2848 register_strategy(&mut strategy);
2849
2850 strategy.core.is_exiting = true;
2852 strategy.core.market_exit_attempts = 5;
2853
2854 let should_proceed = Strategy::stop(&mut strategy);
2856
2857 assert!(should_proceed);
2859 assert!(!strategy.core.is_exiting);
2860 assert_eq!(strategy.core.market_exit_attempts, 0);
2861 }
2862
2863 #[rstest]
2864 fn test_stop_with_manage_stop_true_defers_when_running() {
2865 let config = StrategyConfig {
2866 strategy_id: Some(StrategyId::from("TEST-001")),
2867 order_id_tag: Some("001".to_string()),
2868 manage_stop: true,
2869 ..Default::default()
2870 };
2871 let mut strategy = TestStrategy::new(config);
2872
2873 let trader_id = TraderId::from("TRADER-001");
2875 let clock = Rc::new(RefCell::new(TestClock::new()));
2876 clock
2877 .borrow_mut()
2878 .register_default_handler(TimeEventCallback::from(|_event: TimeEvent| {}));
2879 let cache = Rc::new(RefCell::new(Cache::default()));
2880 let portfolio = Rc::new(RefCell::new(Portfolio::new(
2881 cache.clone(),
2882 clock.clone(),
2883 None,
2884 )));
2885 strategy
2886 .core
2887 .register(trader_id, clock, cache, portfolio)
2888 .unwrap();
2889 strategy.initialize().unwrap();
2890 strategy.start().unwrap();
2891
2892 let should_proceed = Strategy::stop(&mut strategy);
2893
2894 assert!(!should_proceed);
2896 assert!(strategy.core.pending_stop);
2897 }
2898
2899 #[rstest]
2900 fn test_stop_with_manage_stop_true_returns_early_if_pending() {
2901 let config = StrategyConfig {
2902 strategy_id: Some(StrategyId::from("TEST-001")),
2903 order_id_tag: Some("001".to_string()),
2904 manage_stop: true,
2905 ..Default::default()
2906 };
2907 let mut strategy = TestStrategy::new(config);
2908 register_strategy(&mut strategy);
2909 start_strategy(&mut strategy);
2910 strategy.core.pending_stop = true;
2911
2912 let should_proceed = Strategy::stop(&mut strategy);
2914
2915 assert!(!should_proceed);
2917 assert!(strategy.core.pending_stop);
2918 }
2919
2920 #[rstest]
2921 fn test_stop_with_manage_stop_true_proceeds_when_not_running() {
2922 let config = StrategyConfig {
2923 strategy_id: Some(StrategyId::from("TEST-001")),
2924 order_id_tag: Some("001".to_string()),
2925 manage_stop: true,
2926 ..Default::default()
2927 };
2928 let mut strategy = TestStrategy::new(config);
2929 register_strategy(&mut strategy);
2930
2931 assert_ne!(strategy.core.actor.state(), ComponentState::Running);
2933
2934 let should_proceed = Strategy::stop(&mut strategy);
2935
2936 assert!(should_proceed);
2938 }
2939
2940 #[rstest]
2941 fn test_finalize_market_exit_stops_strategy_when_pending() {
2942 let config = StrategyConfig {
2943 strategy_id: Some(StrategyId::from("TEST-001")),
2944 order_id_tag: Some("001".to_string()),
2945 ..Default::default()
2946 };
2947 let mut strategy = TestStrategy::new(config);
2948 register_strategy(&mut strategy);
2949 start_strategy(&mut strategy);
2950
2951 strategy.core.is_exiting = true;
2953 strategy.core.pending_stop = true;
2954
2955 strategy.finalize_market_exit();
2956
2957 assert_eq!(strategy.core.actor.state(), ComponentState::Stopped);
2959 assert!(!strategy.core.is_exiting);
2960 assert!(!strategy.core.pending_stop);
2961 }
2962
2963 #[rstest]
2964 fn test_finalize_market_exit_stays_running_when_not_pending() {
2965 let config = StrategyConfig {
2966 strategy_id: Some(StrategyId::from("TEST-001")),
2967 order_id_tag: Some("001".to_string()),
2968 ..Default::default()
2969 };
2970 let mut strategy = TestStrategy::new(config);
2971 register_strategy(&mut strategy);
2972 start_strategy(&mut strategy);
2973
2974 strategy.core.is_exiting = true;
2976 strategy.core.pending_stop = false;
2977
2978 strategy.finalize_market_exit();
2979
2980 assert_eq!(strategy.core.actor.state(), ComponentState::Running);
2982 assert!(!strategy.core.is_exiting);
2983 }
2984
2985 #[rstest]
2986 fn test_submit_order_denied_during_market_exit_when_not_reduce_only() {
2987 let mut strategy = create_test_strategy();
2988 register_strategy(&mut strategy);
2989 start_strategy(&mut strategy);
2990 strategy.core.is_exiting = true;
2991
2992 let order = OrderAny::Market(MarketOrder::new(
2993 TraderId::from("TRADER-001"),
2994 StrategyId::from("TEST-001"),
2995 InstrumentId::from("BTCUSDT.BINANCE"),
2996 ClientOrderId::from("O-20250208-0001"),
2997 OrderSide::Buy,
2998 Quantity::from(100_000),
2999 TimeInForce::Gtc,
3000 UUID4::new(),
3001 UnixNanos::default(),
3002 false, false,
3004 None,
3005 None,
3006 None,
3007 None,
3008 None,
3009 None,
3010 None,
3011 None,
3012 ));
3013 let client_order_id = order.client_order_id();
3014 let result = strategy.submit_order(order, None, None);
3015
3016 assert!(result.is_ok());
3017 let cache = strategy.core.cache();
3018 let cached_order = cache.order(&client_order_id).unwrap();
3019 assert_eq!(cached_order.status(), OrderStatus::Denied);
3020 }
3021
3022 #[rstest]
3023 fn test_submit_order_allowed_during_market_exit_when_reduce_only() {
3024 let mut strategy = create_test_strategy();
3025 register_strategy(&mut strategy);
3026 start_strategy(&mut strategy);
3027 strategy.core.is_exiting = true;
3028
3029 let order = OrderAny::Market(MarketOrder::new(
3030 TraderId::from("TRADER-001"),
3031 StrategyId::from("TEST-001"),
3032 InstrumentId::from("BTCUSDT.BINANCE"),
3033 ClientOrderId::from("O-20250208-0001"),
3034 OrderSide::Buy,
3035 Quantity::from(100_000),
3036 TimeInForce::Gtc,
3037 UUID4::new(),
3038 UnixNanos::default(),
3039 true, false,
3041 None,
3042 None,
3043 None,
3044 None,
3045 None,
3046 None,
3047 None,
3048 None,
3049 ));
3050 let client_order_id = order.client_order_id();
3051 let result = strategy.submit_order(order, None, None);
3052
3053 assert!(result.is_ok());
3054 let cache = strategy.core.cache();
3055 let cached_order = cache.order(&client_order_id).unwrap();
3056 assert_ne!(cached_order.status(), OrderStatus::Denied);
3057 }
3058
3059 #[rstest]
3060 fn test_submit_order_allowed_during_market_exit_when_tagged() {
3061 let mut strategy = create_test_strategy();
3062 register_strategy(&mut strategy);
3063 start_strategy(&mut strategy);
3064 strategy.core.is_exiting = true;
3065
3066 let order = OrderAny::Market(MarketOrder::new(
3067 TraderId::from("TRADER-001"),
3068 StrategyId::from("TEST-001"),
3069 InstrumentId::from("BTCUSDT.BINANCE"),
3070 ClientOrderId::from("O-20250208-0002"),
3071 OrderSide::Buy,
3072 Quantity::from(100_000),
3073 TimeInForce::Gtc,
3074 UUID4::new(),
3075 UnixNanos::default(),
3076 false, false,
3078 None,
3079 None,
3080 None,
3081 None,
3082 None,
3083 None,
3084 None,
3085 Some(vec![Ustr::from("MARKET_EXIT")]),
3086 ));
3087 let client_order_id = order.client_order_id();
3088 let result = strategy.submit_order(order, None, None);
3089
3090 assert!(result.is_ok());
3091 let cache = strategy.core.cache();
3092 let cached_order = cache.order(&client_order_id).unwrap();
3093 assert_ne!(cached_order.status(), OrderStatus::Denied);
3094 }
3095
3096 #[derive(Debug)]
3097 struct MacroTestSimple {
3098 core: StrategyCore,
3099 }
3100
3101 nautilus_strategy!(MacroTestSimple);
3102
3103 impl DataActor for MacroTestSimple {}
3104
3105 #[derive(Debug)]
3106 struct MacroTestWithHooks {
3107 core: StrategyCore,
3108 }
3109
3110 nautilus_strategy!(MacroTestWithHooks, {
3111 fn on_order_rejected(&mut self, _event: OrderRejected) {}
3112 });
3113
3114 impl DataActor for MacroTestWithHooks {}
3115
3116 #[derive(Debug)]
3117 struct MacroTestCustomField {
3118 inner: StrategyCore,
3119 }
3120
3121 nautilus_strategy!(MacroTestCustomField, inner, {
3122 fn external_order_claims(&self) -> Option<Vec<InstrumentId>> {
3123 None
3124 }
3125 });
3126
3127 impl DataActor for MacroTestCustomField {}
3128
3129 #[rstest]
3130 fn test_nautilus_strategy_macro_forms() {
3131 let config = StrategyConfig {
3132 strategy_id: Some(StrategyId::from("MACRO-001")),
3133 order_id_tag: Some("001".to_string()),
3134 ..Default::default()
3135 };
3136
3137 let simple = MacroTestSimple {
3138 core: StrategyCore::new(config.clone()),
3139 };
3140 assert_eq!(simple.core().config.strategy_id, config.strategy_id);
3141
3142 let hooks = MacroTestWithHooks {
3143 core: StrategyCore::new(config.clone()),
3144 };
3145 assert_eq!(hooks.core().config.strategy_id, config.strategy_id);
3146
3147 let custom = MacroTestCustomField {
3148 inner: StrategyCore::new(config.clone()),
3149 };
3150 assert_eq!(custom.core().config.strategy_id, config.strategy_id);
3151 assert!(custom.external_order_claims().is_none());
3152 }
3153}