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 {
74 fn core(&self) -> &StrategyCore;
79
80 fn core_mut(&mut self) -> &mut StrategyCore;
85
86 fn external_order_claims(&self) -> Option<Vec<InstrumentId>> {
91 None
92 }
93
94 fn submit_order(
100 &mut self,
101 order: OrderAny,
102 position_id: Option<PositionId>,
103 client_id: Option<ClientId>,
104 ) -> anyhow::Result<()> {
105 self.submit_order_with_params(order, position_id, client_id, Params::new())
106 }
107
108 fn submit_order_with_params(
114 &mut self,
115 order: OrderAny,
116 position_id: Option<PositionId>,
117 client_id: Option<ClientId>,
118 params: Params,
119 ) -> anyhow::Result<()> {
120 let core = self.core_mut();
121
122 let trader_id = core.trader_id().expect("Trader ID not set");
123 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
124 let ts_init = core.clock().timestamp_ns();
125
126 let market_exit_tag = core.market_exit_tag;
127 let is_market_exit_order = order
128 .tags()
129 .is_some_and(|tags| tags.contains(&market_exit_tag));
130
131 if core.is_exiting && !order.is_reduce_only() && !is_market_exit_order {
132 self.deny_order(&order, Ustr::from("MARKET_EXIT_IN_PROGRESS"));
133 return Ok(());
134 }
135
136 let core = self.core_mut();
137 let params = if params.is_empty() {
138 None
139 } else {
140 Some(params)
141 };
142
143 {
144 let cache_rc = core.cache_rc();
145 let mut cache = cache_rc.borrow_mut();
146 cache.add_order(order.clone(), position_id, client_id, true)?;
147 }
148
149 let command = SubmitOrder::new(
150 trader_id,
151 client_id,
152 strategy_id,
153 order.instrument_id(),
154 order.client_order_id(),
155 order.init_event().clone(),
156 order.exec_algorithm_id(),
157 position_id,
158 params,
159 UUID4::new(),
160 ts_init,
161 );
162
163 let manager = core.order_manager();
164
165 if matches!(order.emulation_trigger(), Some(trigger) if trigger != TriggerType::NoTrigger) {
166 manager.send_emulator_command(TradingCommand::SubmitOrder(command));
167 } else if order.exec_algorithm_id().is_some() {
168 manager.send_algo_command(command, order.exec_algorithm_id().unwrap());
169 } else {
170 manager.send_risk_command(TradingCommand::SubmitOrder(command));
171 }
172
173 self.set_gtd_expiry(&order)?;
174 Ok(())
175 }
176
177 fn submit_order_list(
184 &mut self,
185 mut orders: Vec<OrderAny>,
186 position_id: Option<PositionId>,
187 client_id: Option<ClientId>,
188 ) -> anyhow::Result<()> {
189 let should_deny = {
190 let core = self.core_mut();
191 let tag = core.market_exit_tag;
192 core.is_exiting
193 && orders.iter().any(|o| {
194 !o.is_reduce_only() && !o.tags().is_some_and(|tags| tags.contains(&tag))
195 })
196 };
197
198 if should_deny {
199 self.deny_order_list(&orders, Ustr::from("MARKET_EXIT_IN_PROGRESS"));
200 return Ok(());
201 }
202
203 let core = self.core_mut();
204 let trader_id = core.trader_id().expect("Trader ID not set");
205 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
206 let ts_init = core.clock().timestamp_ns();
207
208 let order_list = if orders.first().is_some_and(|o| o.order_list_id().is_some()) {
210 OrderList::from_orders(&orders, ts_init)
211 } else {
212 core.order_factory().create_list(&mut orders, ts_init)
213 };
214
215 {
216 let cache_rc = core.cache_rc();
217 let cache = cache_rc.borrow();
218 if cache.order_list_exists(&order_list.id) {
219 anyhow::bail!("OrderList denied: duplicate {}", order_list.id);
220 }
221
222 for order in &orders {
223 if order.status() != OrderStatus::Initialized {
224 anyhow::bail!(
225 "Order in list denied: invalid status for {}, expected INITIALIZED",
226 order.client_order_id()
227 );
228 }
229
230 if cache.order_exists(&order.client_order_id()) {
231 anyhow::bail!(
232 "Order in list denied: duplicate {}",
233 order.client_order_id()
234 );
235 }
236 }
237 }
238
239 {
240 let cache_rc = core.cache_rc();
241 let mut cache = cache_rc.borrow_mut();
242 cache.add_order_list(order_list.clone())?;
243 for order in &orders {
244 cache.add_order(order.clone(), position_id, client_id, true)?;
245 }
246 }
247
248 let first_order = orders.first();
249 let order_inits: Vec<_> = orders.iter().map(|o| o.init_event().clone()).collect();
250 let exec_algorithm_id = first_order.and_then(|o| o.exec_algorithm_id());
251
252 let command = SubmitOrderList::new(
253 trader_id,
254 client_id,
255 strategy_id,
256 order_list,
257 order_inits,
258 exec_algorithm_id,
259 position_id,
260 None, UUID4::new(),
262 ts_init,
263 );
264
265 let has_emulated_order = orders.iter().any(|o| {
266 matches!(o.emulation_trigger(), Some(trigger) if trigger != TriggerType::NoTrigger)
267 || o.is_emulated()
268 });
269
270 let manager = core.order_manager();
271
272 if has_emulated_order {
273 manager.send_emulator_command(TradingCommand::SubmitOrderList(command));
274 } else if let Some(algo_id) = exec_algorithm_id {
275 let endpoint = format!("{algo_id}.execute");
276 msgbus::send_any(endpoint.into(), &TradingCommand::SubmitOrderList(command));
277 } else {
278 manager.send_risk_command(TradingCommand::SubmitOrderList(command));
279 }
280
281 for order in &orders {
282 self.set_gtd_expiry(order)?;
283 }
284
285 Ok(())
286 }
287
288 fn submit_order_list_with_params(
295 &mut self,
296 mut orders: Vec<OrderAny>,
297 position_id: Option<PositionId>,
298 client_id: Option<ClientId>,
299 params: Params,
300 ) -> anyhow::Result<()> {
301 let should_deny = {
302 let core = self.core_mut();
303 let tag = core.market_exit_tag;
304 core.is_exiting
305 && orders.iter().any(|o| {
306 !o.is_reduce_only() && !o.tags().is_some_and(|tags| tags.contains(&tag))
307 })
308 };
309
310 if should_deny {
311 self.deny_order_list(&orders, Ustr::from("MARKET_EXIT_IN_PROGRESS"));
312 return Ok(());
313 }
314
315 let core = self.core_mut();
316
317 let trader_id = core.trader_id().expect("Trader ID not set");
318 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
319 let ts_init = core.clock().timestamp_ns();
320
321 let order_list = if orders.first().is_some_and(|o| o.order_list_id().is_some()) {
323 OrderList::from_orders(&orders, ts_init)
324 } else {
325 core.order_factory().create_list(&mut orders, ts_init)
326 };
327
328 {
329 let cache_rc = core.cache_rc();
330 let cache = cache_rc.borrow();
331 if cache.order_list_exists(&order_list.id) {
332 anyhow::bail!("OrderList denied: duplicate {}", order_list.id);
333 }
334
335 for order in &orders {
336 if order.status() != OrderStatus::Initialized {
337 anyhow::bail!(
338 "Order in list denied: invalid status for {}, expected INITIALIZED",
339 order.client_order_id()
340 );
341 }
342
343 if cache.order_exists(&order.client_order_id()) {
344 anyhow::bail!(
345 "Order in list denied: duplicate {}",
346 order.client_order_id()
347 );
348 }
349 }
350 }
351
352 {
353 let cache_rc = core.cache_rc();
354 let mut cache = cache_rc.borrow_mut();
355 cache.add_order_list(order_list.clone())?;
356 for order in &orders {
357 cache.add_order(order.clone(), position_id, client_id, true)?;
358 }
359 }
360
361 let params_opt = if params.is_empty() {
362 None
363 } else {
364 Some(params)
365 };
366
367 let first_order = orders.first();
368 let order_inits: Vec<_> = orders.iter().map(|o| o.init_event().clone()).collect();
369 let exec_algorithm_id = first_order.and_then(|o| o.exec_algorithm_id());
370
371 let command = SubmitOrderList::new(
372 trader_id,
373 client_id,
374 strategy_id,
375 order_list,
376 order_inits,
377 exec_algorithm_id,
378 position_id,
379 params_opt,
380 UUID4::new(),
381 ts_init,
382 );
383
384 let has_emulated_order = orders.iter().any(|o| {
385 matches!(o.emulation_trigger(), Some(trigger) if trigger != TriggerType::NoTrigger)
386 || o.is_emulated()
387 });
388
389 let manager = core.order_manager();
390
391 if has_emulated_order {
392 manager.send_emulator_command(TradingCommand::SubmitOrderList(command));
393 } else if let Some(algo_id) = exec_algorithm_id {
394 let endpoint = format!("{algo_id}.execute");
395 msgbus::send_any(endpoint.into(), &TradingCommand::SubmitOrderList(command));
396 } else {
397 manager.send_risk_command(TradingCommand::SubmitOrderList(command));
398 }
399
400 for order in &orders {
401 self.set_gtd_expiry(order)?;
402 }
403
404 Ok(())
405 }
406
407 fn modify_order(
413 &mut self,
414 order: OrderAny,
415 quantity: Option<Quantity>,
416 price: Option<Price>,
417 trigger_price: Option<Price>,
418 client_id: Option<ClientId>,
419 ) -> anyhow::Result<()> {
420 self.modify_order_with_params(
421 order,
422 quantity,
423 price,
424 trigger_price,
425 client_id,
426 Params::new(),
427 )
428 }
429
430 fn modify_order_with_params(
436 &mut self,
437 order: OrderAny,
438 quantity: Option<Quantity>,
439 price: Option<Price>,
440 trigger_price: Option<Price>,
441 client_id: Option<ClientId>,
442 params: Params,
443 ) -> anyhow::Result<()> {
444 let core = self.core_mut();
445
446 let trader_id = core.trader_id().expect("Trader ID not set");
447 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
448 let ts_init = core.clock().timestamp_ns();
449
450 let params = if params.is_empty() {
451 None
452 } else {
453 Some(params)
454 };
455
456 let command = ModifyOrder::new(
457 trader_id,
458 client_id,
459 strategy_id,
460 order.instrument_id(),
461 order.client_order_id(),
462 order.venue_order_id(),
463 quantity,
464 price,
465 trigger_price,
466 UUID4::new(),
467 ts_init,
468 params,
469 );
470
471 let manager = core.order_manager();
472
473 if matches!(order.emulation_trigger(), Some(trigger) if trigger != TriggerType::NoTrigger) {
474 manager.send_emulator_command(TradingCommand::ModifyOrder(command));
475 } else if order.exec_algorithm_id().is_some() {
476 manager.send_risk_command(TradingCommand::ModifyOrder(command));
477 } else {
478 manager.send_exec_command(TradingCommand::ModifyOrder(command));
479 }
480 Ok(())
481 }
482
483 fn cancel_order(&mut self, order: OrderAny, client_id: Option<ClientId>) -> anyhow::Result<()> {
489 self.cancel_order_with_params(order, client_id, Params::new())
490 }
491
492 fn cancel_order_with_params(
498 &mut self,
499 order: OrderAny,
500 client_id: Option<ClientId>,
501 params: Params,
502 ) -> anyhow::Result<()> {
503 let core = self.core_mut();
504
505 let trader_id = core.trader_id().expect("Trader ID not set");
506 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
507 let ts_init = core.clock().timestamp_ns();
508
509 let params = if params.is_empty() {
510 None
511 } else {
512 Some(params)
513 };
514
515 let command = CancelOrder::new(
516 trader_id,
517 client_id,
518 strategy_id,
519 order.instrument_id(),
520 order.client_order_id(),
521 order.venue_order_id(),
522 UUID4::new(),
523 ts_init,
524 params,
525 );
526
527 let manager = core.order_manager();
528
529 if matches!(order.emulation_trigger(), Some(trigger) if trigger != TriggerType::NoTrigger)
530 || order.is_emulated()
531 {
532 manager.send_emulator_command(TradingCommand::CancelOrder(command));
533 } else if let Some(algo_id) = order.exec_algorithm_id() {
534 let endpoint = format!("{algo_id}.execute");
535 msgbus::send_any(endpoint.into(), &TradingCommand::CancelOrder(command));
536 } else {
537 manager.send_exec_command(TradingCommand::CancelOrder(command));
538 }
539 Ok(())
540 }
541
542 fn cancel_orders(
549 &mut self,
550 mut orders: Vec<OrderAny>,
551 client_id: Option<ClientId>,
552 params: Option<Params>,
553 ) -> anyhow::Result<()> {
554 if orders.is_empty() {
555 anyhow::bail!("Cannot batch cancel empty order list");
556 }
557
558 let core = self.core_mut();
559 let trader_id = core.trader_id().expect("Trader ID not set");
560 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
561 let ts_init = core.clock().timestamp_ns();
562
563 let manager = core.order_manager();
564
565 let first = orders.remove(0);
566 let instrument_id = first.instrument_id();
567
568 if first.is_emulated() || first.is_active_local() {
569 anyhow::bail!("Cannot include emulated or local orders in batch cancel");
570 }
571
572 let mut cancels = Vec::with_capacity(orders.len() + 1);
573 cancels.push(CancelOrder::new(
574 trader_id,
575 client_id,
576 strategy_id,
577 instrument_id,
578 first.client_order_id(),
579 first.venue_order_id(),
580 UUID4::new(),
581 ts_init,
582 params.clone(),
583 ));
584
585 for order in orders {
586 if order.instrument_id() != instrument_id {
587 anyhow::bail!(
588 "Cannot batch cancel orders for different instruments: {} vs {}",
589 instrument_id,
590 order.instrument_id()
591 );
592 }
593
594 if order.is_emulated() || order.is_active_local() {
595 anyhow::bail!("Cannot include emulated or local orders in batch cancel");
596 }
597
598 cancels.push(CancelOrder::new(
599 trader_id,
600 client_id,
601 strategy_id,
602 instrument_id,
603 order.client_order_id(),
604 order.venue_order_id(),
605 UUID4::new(),
606 ts_init,
607 params.clone(),
608 ));
609 }
610
611 let command = BatchCancelOrders::new(
612 trader_id,
613 client_id,
614 strategy_id,
615 instrument_id,
616 cancels,
617 UUID4::new(),
618 ts_init,
619 params,
620 );
621
622 manager.send_exec_command(TradingCommand::BatchCancelOrders(command));
623 Ok(())
624 }
625
626 fn cancel_all_orders(
632 &mut self,
633 instrument_id: InstrumentId,
634 order_side: Option<OrderSide>,
635 client_id: Option<ClientId>,
636 ) -> anyhow::Result<()> {
637 self.cancel_all_orders_with_params(instrument_id, order_side, client_id, Params::new())
638 }
639
640 fn cancel_all_orders_with_params(
646 &mut self,
647 instrument_id: InstrumentId,
648 order_side: Option<OrderSide>,
649 client_id: Option<ClientId>,
650 params: Params,
651 ) -> anyhow::Result<()> {
652 let params = if params.is_empty() {
653 None
654 } else {
655 Some(params)
656 };
657 let core = self.core_mut();
658
659 let trader_id = core.trader_id().expect("Trader ID not set");
660 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
661 let ts_init = core.clock().timestamp_ns();
662 let cache = core.cache();
663
664 let open_orders = cache.orders_open(
665 None,
666 Some(&instrument_id),
667 Some(&strategy_id),
668 None,
669 order_side,
670 );
671
672 let emulated_orders = cache.orders_emulated(
673 None,
674 Some(&instrument_id),
675 Some(&strategy_id),
676 None,
677 order_side,
678 );
679
680 let inflight_orders = cache.orders_inflight(
681 None,
682 Some(&instrument_id),
683 Some(&strategy_id),
684 None,
685 order_side,
686 );
687
688 let exec_algorithm_ids = cache.exec_algorithm_ids();
689 let mut algo_orders = Vec::new();
690
691 for algo_id in &exec_algorithm_ids {
692 let orders = cache.orders_for_exec_algorithm(
693 algo_id,
694 None,
695 Some(&instrument_id),
696 Some(&strategy_id),
697 None,
698 order_side,
699 );
700 algo_orders.extend(orders.iter().map(|o| (*o).clone()));
701 }
702
703 let open_count = open_orders.len();
704 let emulated_count = emulated_orders.len();
705 let inflight_count = inflight_orders.len();
706 let algo_count = algo_orders.len();
707
708 drop(cache);
709
710 if open_count == 0 && emulated_count == 0 && inflight_count == 0 && algo_count == 0 {
711 let side_str = order_side.map(|s| format!(" {s}")).unwrap_or_default();
712 log::info!("No {instrument_id} open, emulated, or inflight{side_str} orders to cancel");
713 return Ok(());
714 }
715
716 let manager = core.order_manager();
717
718 let side_str = order_side.map(|s| format!(" {s}")).unwrap_or_default();
719
720 if open_count > 0 {
721 log::info!(
722 "Canceling {open_count} open{side_str} {instrument_id} order{}",
723 if open_count == 1 { "" } else { "s" }
724 );
725 }
726
727 if emulated_count > 0 {
728 log::info!(
729 "Canceling {emulated_count} emulated{side_str} {instrument_id} order{}",
730 if emulated_count == 1 { "" } else { "s" }
731 );
732 }
733
734 if inflight_count > 0 {
735 log::info!(
736 "Canceling {inflight_count} inflight{side_str} {instrument_id} order{}",
737 if inflight_count == 1 { "" } else { "s" }
738 );
739 }
740
741 if open_count > 0 || inflight_count > 0 {
742 let command = CancelAllOrders::new(
743 trader_id,
744 client_id,
745 strategy_id,
746 instrument_id,
747 order_side.unwrap_or(OrderSide::NoOrderSide),
748 UUID4::new(),
749 ts_init,
750 params.clone(),
751 );
752
753 manager.send_exec_command(TradingCommand::CancelAllOrders(command));
754 }
755
756 if emulated_count > 0 {
757 let command = CancelAllOrders::new(
758 trader_id,
759 client_id,
760 strategy_id,
761 instrument_id,
762 order_side.unwrap_or(OrderSide::NoOrderSide),
763 UUID4::new(),
764 ts_init,
765 params,
766 );
767
768 manager.send_emulator_command(TradingCommand::CancelAllOrders(command));
769 }
770
771 for order in algo_orders {
772 self.cancel_order(order, client_id)?;
773 }
774
775 Ok(())
776 }
777
778 fn close_position(
784 &mut self,
785 position: &Position,
786 client_id: Option<ClientId>,
787 tags: Option<Vec<Ustr>>,
788 time_in_force: Option<TimeInForce>,
789 reduce_only: Option<bool>,
790 quote_quantity: Option<bool>,
791 ) -> anyhow::Result<()> {
792 let core = self.core_mut();
793
794 if position.is_closed() {
795 log::warn!("Cannot close position (already closed): {}", position.id);
796 return Ok(());
797 }
798
799 let closing_side = OrderCore::closing_side(position.side);
800
801 let order = core.order_factory().market(
802 position.instrument_id,
803 closing_side,
804 position.quantity,
805 time_in_force,
806 reduce_only.or(Some(true)),
807 quote_quantity,
808 None,
809 None,
810 tags,
811 None,
812 );
813
814 self.submit_order(order, Some(position.id), client_id)
815 }
816
817 #[allow(clippy::too_many_arguments)]
823 fn close_all_positions(
824 &mut self,
825 instrument_id: InstrumentId,
826 position_side: Option<PositionSide>,
827 client_id: Option<ClientId>,
828 tags: Option<Vec<Ustr>>,
829 time_in_force: Option<TimeInForce>,
830 reduce_only: Option<bool>,
831 quote_quantity: Option<bool>,
832 ) -> anyhow::Result<()> {
833 let core = self.core_mut();
834 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
835 let cache = core.cache();
836
837 let positions_open = cache.positions_open(
838 None,
839 Some(&instrument_id),
840 Some(&strategy_id),
841 None,
842 position_side,
843 );
844
845 let side_str = position_side.map(|s| format!(" {s}")).unwrap_or_default();
846
847 if positions_open.is_empty() {
848 log::info!("No {instrument_id} open{side_str} positions to close");
849 return Ok(());
850 }
851
852 let count = positions_open.len();
853 log::info!(
854 "Closing {count} open{side_str} position{}",
855 if count == 1 { "" } else { "s" }
856 );
857
858 let positions_data: Vec<_> = positions_open
859 .iter()
860 .map(|p| (p.id, p.instrument_id, p.side, p.quantity, p.is_closed()))
861 .collect();
862
863 drop(cache);
864
865 for (pos_id, pos_instrument_id, pos_side, pos_quantity, is_closed) in positions_data {
866 if is_closed {
867 continue;
868 }
869
870 let core = self.core_mut();
871 let closing_side = OrderCore::closing_side(pos_side);
872 let order = core.order_factory().market(
873 pos_instrument_id,
874 closing_side,
875 pos_quantity,
876 time_in_force,
877 reduce_only.or(Some(true)),
878 quote_quantity,
879 None,
880 None,
881 tags.clone(),
882 None,
883 );
884
885 self.submit_order(order, Some(pos_id), client_id)?;
886 }
887
888 Ok(())
889 }
890
891 fn query_account(
900 &mut self,
901 account_id: AccountId,
902 client_id: Option<ClientId>,
903 ) -> anyhow::Result<()> {
904 let core = self.core_mut();
905
906 let trader_id = core.trader_id().expect("Trader ID not set");
907 let ts_init = core.clock().timestamp_ns();
908
909 let command = QueryAccount::new(trader_id, client_id, account_id, UUID4::new(), ts_init);
910
911 core.order_manager()
912 .send_exec_command(TradingCommand::QueryAccount(command));
913 Ok(())
914 }
915
916 fn query_order(&mut self, order: &OrderAny, client_id: Option<ClientId>) -> anyhow::Result<()> {
925 let core = self.core_mut();
926
927 let trader_id = core.trader_id().expect("Trader ID not set");
928 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
929 let ts_init = core.clock().timestamp_ns();
930
931 let command = QueryOrder::new(
932 trader_id,
933 client_id,
934 strategy_id,
935 order.instrument_id(),
936 order.client_order_id(),
937 order.venue_order_id(),
938 UUID4::new(),
939 ts_init,
940 );
941
942 core.order_manager()
943 .send_exec_command(TradingCommand::QueryOrder(command));
944 Ok(())
945 }
946
947 fn handle_order_event(&mut self, event: OrderEventAny) {
949 {
950 let core = self.core_mut();
951 let id = &core.actor.actor_id;
952 let is_warning = matches!(
953 &event,
954 OrderEventAny::Denied(_)
955 | OrderEventAny::Rejected(_)
956 | OrderEventAny::CancelRejected(_)
957 | OrderEventAny::ModifyRejected(_)
958 );
959
960 if is_warning {
961 log::warn!("{id} {RECV}{EVT} {event}");
962 } else if core.config.log_events {
963 log::info!("{id} {RECV}{EVT} {event}");
964 }
965 }
966
967 let client_order_id = event.client_order_id();
968 let is_terminal = matches!(
969 &event,
970 OrderEventAny::Filled(_)
971 | OrderEventAny::Canceled(_)
972 | OrderEventAny::Rejected(_)
973 | OrderEventAny::Expired(_)
974 | OrderEventAny::Denied(_)
975 );
976
977 match &event {
978 OrderEventAny::Initialized(e) => self.on_order_initialized(e.clone()),
979 OrderEventAny::Denied(e) => self.on_order_denied(*e),
980 OrderEventAny::Emulated(e) => self.on_order_emulated(*e),
981 OrderEventAny::Released(e) => self.on_order_released(*e),
982 OrderEventAny::Submitted(e) => self.on_order_submitted(*e),
983 OrderEventAny::Rejected(e) => self.on_order_rejected(*e),
984 OrderEventAny::Accepted(e) => self.on_order_accepted(*e),
985 OrderEventAny::Canceled(e) => {
986 let _ = DataActor::on_order_canceled(self, e);
987 }
988 OrderEventAny::Expired(e) => self.on_order_expired(*e),
989 OrderEventAny::Triggered(e) => self.on_order_triggered(*e),
990 OrderEventAny::PendingUpdate(e) => self.on_order_pending_update(*e),
991 OrderEventAny::PendingCancel(e) => self.on_order_pending_cancel(*e),
992 OrderEventAny::ModifyRejected(e) => self.on_order_modify_rejected(*e),
993 OrderEventAny::CancelRejected(e) => self.on_order_cancel_rejected(*e),
994 OrderEventAny::Updated(e) => self.on_order_updated(*e),
995 OrderEventAny::Filled(e) => {
996 let _ = DataActor::on_order_filled(self, e);
997 }
998 }
999
1000 if is_terminal {
1001 self.cancel_gtd_expiry(&client_order_id);
1002 }
1003
1004 let core = self.core_mut();
1005 if let Some(manager) = &mut core.order_manager {
1006 manager.handle_event(event);
1007 }
1008 }
1009
1010 fn handle_position_event(&mut self, event: PositionEvent) {
1012 {
1013 let core = self.core_mut();
1014 if core.config.log_events {
1015 let id = &core.actor.actor_id;
1016 log::info!("{id} {RECV}{EVT} {event:?}");
1017 }
1018 }
1019
1020 match event {
1021 PositionEvent::PositionOpened(e) => self.on_position_opened(e),
1022 PositionEvent::PositionChanged(e) => self.on_position_changed(e),
1023 PositionEvent::PositionClosed(e) => self.on_position_closed(e),
1024 PositionEvent::PositionAdjusted(_) => {
1025 }
1027 }
1028 }
1029
1030 fn on_start(&mut self) -> anyhow::Result<()> {
1041 let core = self.core_mut();
1042 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
1043 log::info!("Starting {strategy_id}");
1044
1045 if core.config.manage_gtd_expiry {
1046 self.reactivate_gtd_timers();
1047 }
1048
1049 Ok(())
1050 }
1051
1052 fn on_time_event(&mut self, event: &TimeEvent) -> anyhow::Result<()> {
1061 if event.name.starts_with("GTD-EXPIRY:") {
1062 self.expire_gtd_order(event.clone());
1063 } else if event.name.starts_with("MARKET_EXIT_CHECK:") {
1064 self.check_market_exit(event.clone());
1065 }
1066 Ok(())
1067 }
1068
1069 #[allow(unused_variables)]
1075 fn on_order_initialized(&mut self, event: OrderInitialized) {}
1076
1077 #[allow(unused_variables)]
1081 fn on_order_denied(&mut self, event: OrderDenied) {}
1082
1083 #[allow(unused_variables)]
1087 fn on_order_emulated(&mut self, event: OrderEmulated) {}
1088
1089 #[allow(unused_variables)]
1093 fn on_order_released(&mut self, event: OrderReleased) {}
1094
1095 #[allow(unused_variables)]
1099 fn on_order_submitted(&mut self, event: OrderSubmitted) {}
1100
1101 #[allow(unused_variables)]
1105 fn on_order_rejected(&mut self, event: OrderRejected) {}
1106
1107 #[allow(unused_variables)]
1111 fn on_order_accepted(&mut self, event: OrderAccepted) {}
1112
1113 #[allow(unused_variables)]
1117 fn on_order_expired(&mut self, event: OrderExpired) {}
1118
1119 #[allow(unused_variables)]
1123 fn on_order_triggered(&mut self, event: OrderTriggered) {}
1124
1125 #[allow(unused_variables)]
1129 fn on_order_pending_update(&mut self, event: OrderPendingUpdate) {}
1130
1131 #[allow(unused_variables)]
1135 fn on_order_pending_cancel(&mut self, event: OrderPendingCancel) {}
1136
1137 #[allow(unused_variables)]
1141 fn on_order_modify_rejected(&mut self, event: OrderModifyRejected) {}
1142
1143 #[allow(unused_variables)]
1147 fn on_order_cancel_rejected(&mut self, event: OrderCancelRejected) {}
1148
1149 #[allow(unused_variables)]
1153 fn on_order_updated(&mut self, event: OrderUpdated) {}
1154
1155 #[allow(unused_variables)]
1161 fn on_position_opened(&mut self, event: PositionOpened) {}
1162
1163 #[allow(unused_variables)]
1167 fn on_position_changed(&mut self, event: PositionChanged) {}
1168
1169 #[allow(unused_variables)]
1173 fn on_position_closed(&mut self, event: PositionClosed) {}
1174
1175 fn on_market_exit(&mut self) {}
1179
1180 fn post_market_exit(&mut self) {}
1184
1185 fn is_exiting(&self) -> bool {
1189 self.core().is_exiting
1190 }
1191
1192 fn market_exit(&mut self) -> anyhow::Result<()> {
1208 let core = self.core_mut();
1209 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
1210
1211 if core.actor.state() != ComponentState::Running {
1212 log::warn!("{strategy_id} Cannot market exit: strategy is not running");
1213 return Ok(());
1214 }
1215
1216 if core.is_exiting {
1217 log::warn!("{strategy_id} Market exit called when already in progress");
1218 return Ok(());
1219 }
1220
1221 core.is_exiting = true;
1222 core.market_exit_attempts = 0;
1223 let time_in_force = core.config.market_exit_time_in_force;
1224 let reduce_only = core.config.market_exit_reduce_only;
1225
1226 log::info!("{strategy_id} Initiating market exit...");
1227
1228 self.on_market_exit();
1229
1230 let core = self.core_mut();
1231 let cache = core.cache();
1232
1233 let open_orders = cache.orders_open(None, None, Some(&strategy_id), None, None);
1234 let inflight_orders = cache.orders_inflight(None, None, Some(&strategy_id), None, None);
1235 let open_positions = cache.positions_open(None, None, Some(&strategy_id), None, None);
1236
1237 let mut instruments: AHashSet<InstrumentId> = AHashSet::new();
1238
1239 for order in &open_orders {
1240 instruments.insert(order.instrument_id());
1241 }
1242 for order in &inflight_orders {
1243 instruments.insert(order.instrument_id());
1244 }
1245 for position in &open_positions {
1246 instruments.insert(position.instrument_id);
1247 }
1248
1249 let market_exit_tag = core.market_exit_tag;
1250 let instruments: Vec<_> = instruments.into_iter().collect();
1251 drop(cache);
1252
1253 for instrument_id in instruments {
1254 if let Err(e) = self.cancel_all_orders(instrument_id, None, None) {
1255 log::error!("Error canceling orders for {instrument_id}: {e}");
1256 }
1257
1258 if let Err(e) = self.close_all_positions(
1259 instrument_id,
1260 None,
1261 None,
1262 Some(vec![market_exit_tag]),
1263 Some(time_in_force),
1264 Some(reduce_only),
1265 None,
1266 ) {
1267 log::error!("Error closing positions for {instrument_id}: {e}");
1268 }
1269 }
1270
1271 let core = self.core_mut();
1272 let interval_ms = core.config.market_exit_interval_ms;
1273 let timer_name = core.market_exit_timer_name;
1274
1275 log::info!("{strategy_id} Setting market exit timer at {interval_ms}ms intervals");
1276
1277 let interval_ns = interval_ms * 1_000_000;
1278 let result = core.clock().set_timer_ns(
1279 timer_name.as_str(),
1280 interval_ns,
1281 None,
1282 None,
1283 None,
1284 None,
1285 None,
1286 );
1287
1288 if let Err(e) = result {
1289 core.is_exiting = false;
1291 core.market_exit_attempts = 0;
1292 return Err(e);
1293 }
1294
1295 Ok(())
1296 }
1297
1298 fn check_market_exit(&mut self, _event: TimeEvent) {
1302 if !self.is_exiting() {
1304 return;
1305 }
1306
1307 let core = self.core_mut();
1308 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
1309
1310 core.market_exit_attempts += 1;
1311 let attempts = core.market_exit_attempts;
1312 let max_attempts = core.config.market_exit_max_attempts;
1313
1314 log::debug!(
1315 "{strategy_id} Market exit check triggered (attempt {attempts}/{max_attempts})"
1316 );
1317
1318 if attempts >= max_attempts {
1319 let cache = core.cache();
1320 let open_orders_count = cache
1321 .orders_open(None, None, Some(&strategy_id), None, None)
1322 .len();
1323 let inflight_orders_count = cache
1324 .orders_inflight(None, None, Some(&strategy_id), None, None)
1325 .len();
1326 let open_positions_count = cache
1327 .positions_open(None, None, Some(&strategy_id), None, None)
1328 .len();
1329 drop(cache);
1330
1331 log::warn!(
1332 "{strategy_id} Market exit max attempts ({max_attempts}) reached, \
1333 completing with open orders: {open_orders_count}, \
1334 inflight orders: {inflight_orders_count}, \
1335 open positions: {open_positions_count}"
1336 );
1337
1338 self.finalize_market_exit();
1339 return;
1340 }
1341
1342 let cache = core.cache();
1343 let open_orders = cache.orders_open(None, None, Some(&strategy_id), None, None);
1344 let inflight_orders = cache.orders_inflight(None, None, Some(&strategy_id), None, None);
1345
1346 if !open_orders.is_empty() || !inflight_orders.is_empty() {
1347 return;
1348 }
1349
1350 let open_positions = cache.positions_open(None, None, Some(&strategy_id), None, None);
1351
1352 if !open_positions.is_empty() {
1353 let positions_data: Vec<_> = open_positions
1355 .iter()
1356 .map(|p| (p.id, p.instrument_id, p.side, p.quantity, p.is_closed()))
1357 .collect();
1358
1359 drop(cache);
1360
1361 for (pos_id, instrument_id, side, quantity, is_closed) in positions_data {
1362 if is_closed {
1363 continue;
1364 }
1365
1366 let core = self.core_mut();
1367 let time_in_force = core.config.market_exit_time_in_force;
1368 let reduce_only = core.config.market_exit_reduce_only;
1369 let market_exit_tag = core.market_exit_tag;
1370 let closing_side = OrderCore::closing_side(side);
1371 let order = core.order_factory().market(
1372 instrument_id,
1373 closing_side,
1374 quantity,
1375 Some(time_in_force),
1376 Some(reduce_only),
1377 None,
1378 None,
1379 None,
1380 Some(vec![market_exit_tag]),
1381 None,
1382 );
1383
1384 if let Err(e) = self.submit_order(order, Some(pos_id), None) {
1385 log::error!("Error re-submitting close order for position {pos_id}: {e}");
1386 }
1387 }
1388 return;
1389 }
1390
1391 drop(cache);
1392 self.finalize_market_exit();
1393 }
1394
1395 fn finalize_market_exit(&mut self) {
1400 let (strategy_id, should_stop) = {
1401 let core = self.core_mut();
1402 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
1403 let should_stop = core.pending_stop;
1404 (strategy_id, should_stop)
1405 };
1406
1407 self.cancel_market_exit();
1408
1409 let hook_result = catch_unwind(AssertUnwindSafe(|| {
1410 self.post_market_exit();
1411 }));
1412
1413 if let Err(e) = hook_result {
1414 log::error!("{strategy_id} Error in post_market_exit: {e:?}");
1415 }
1416
1417 if should_stop {
1418 log::info!("{strategy_id} Market exit complete, stopping strategy");
1419
1420 if let Err(e) = Component::stop(self) {
1421 log::error!("{strategy_id} Failed to stop: {e}");
1422 }
1423 }
1424
1425 let core = self.core_mut();
1426 debug_assert!(
1427 !(core.pending_stop
1428 && !core.is_exiting
1429 && core.actor.state() == ComponentState::Running),
1430 "INVARIANT: stuck state after finalize_market_exit"
1431 );
1432 }
1433
1434 fn cancel_market_exit(&mut self) {
1438 let core = self.core_mut();
1439 let timer_name = core.market_exit_timer_name;
1440
1441 if core.clock().timer_names().contains(&timer_name.as_str()) {
1442 core.clock().cancel_timer(timer_name.as_str());
1443 }
1444
1445 core.is_exiting = false;
1446 core.pending_stop = false;
1447 core.market_exit_attempts = 0;
1448 }
1449
1450 fn stop(&mut self) -> bool {
1462 let (manage_stop, is_exiting, should_initiate_exit) = {
1463 let core = self.core_mut();
1464 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
1465 let manage_stop = core.config.manage_stop;
1466 let state = core.actor.state();
1467 let pending_stop = core.pending_stop;
1468 let is_exiting = core.is_exiting;
1469
1470 if manage_stop {
1471 if state != ComponentState::Running {
1472 return true; }
1474
1475 if pending_stop {
1476 return false; }
1478
1479 core.pending_stop = true;
1480 let should_initiate_exit = !is_exiting;
1481
1482 if should_initiate_exit {
1483 log::info!("{strategy_id} Initiating market exit before stop");
1484 }
1485
1486 (manage_stop, is_exiting, should_initiate_exit)
1487 } else {
1488 (manage_stop, is_exiting, false)
1489 }
1490 };
1491
1492 if manage_stop {
1493 if should_initiate_exit && let Err(e) = self.market_exit() {
1494 log::warn!("Market exit failed during stop: {e}, proceeding with stop");
1495 self.core_mut().pending_stop = false;
1496 return true;
1497 }
1498 debug_assert!(
1499 self.is_exiting(),
1500 "INVARIANT: deferring stop but not exiting"
1501 );
1502 return false; }
1504
1505 if is_exiting {
1507 self.cancel_market_exit();
1508 }
1509
1510 true }
1512
1513 fn deny_order(&mut self, order: &OrderAny, reason: Ustr) {
1518 let core = self.core_mut();
1519 let trader_id = core.trader_id().expect("Trader ID not set");
1520 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
1521 let ts_now = core.clock().timestamp_ns();
1522
1523 let event = OrderDenied::new(
1524 trader_id,
1525 strategy_id,
1526 order.instrument_id(),
1527 order.client_order_id(),
1528 reason,
1529 UUID4::new(),
1530 ts_now,
1531 ts_now,
1532 );
1533
1534 log::warn!(
1535 "{strategy_id} Order {} denied: {reason}",
1536 order.client_order_id()
1537 );
1538
1539 {
1541 let cache_rc = core.cache_rc();
1542 let mut cache = cache_rc.borrow_mut();
1543 if !cache.order_exists(&order.client_order_id()) {
1544 let _ = cache.add_order(order.clone(), None, None, true);
1545 }
1546 }
1547
1548 let mut order_clone = order.clone();
1550 if let Err(e) = order_clone.apply(OrderEventAny::Denied(event)) {
1551 log::warn!("Failed to apply OrderDenied event: {e}");
1552 return;
1553 }
1554
1555 {
1556 let cache_rc = core.cache_rc();
1557 let mut cache = cache_rc.borrow_mut();
1558 let _ = cache.update_order(&order_clone);
1559 }
1560 }
1561
1562 fn deny_order_list(&mut self, orders: &[OrderAny], reason: Ustr) {
1566 for order in orders {
1567 if !order.is_closed() {
1568 self.deny_order(order, reason);
1569 }
1570 }
1571 }
1572
1573 fn set_gtd_expiry(&mut self, order: &OrderAny) -> anyhow::Result<()> {
1583 let core = self.core_mut();
1584
1585 if !core.config.manage_gtd_expiry || order.time_in_force() != TimeInForce::Gtd {
1586 return Ok(());
1587 }
1588
1589 let Some(expire_time) = order.expire_time() else {
1590 return Ok(());
1591 };
1592
1593 let client_order_id = order.client_order_id();
1594 let timer_name = format!("GTD-EXPIRY:{client_order_id}");
1595
1596 let current_time_ns = {
1597 let clock = core.clock();
1598 clock.timestamp_ns()
1599 };
1600
1601 if current_time_ns >= expire_time.as_u64() {
1602 log::info!("GTD order {client_order_id} already expired, canceling immediately");
1603 return self.cancel_order(order.clone(), None);
1604 }
1605
1606 {
1607 let mut clock = core.clock();
1608 clock.set_time_alert_ns(&timer_name, expire_time, None, None)?;
1609 }
1610
1611 core.gtd_timers
1612 .insert(client_order_id, Ustr::from(&timer_name));
1613
1614 log::debug!("Set GTD expiry timer for {client_order_id} at {expire_time}");
1615 Ok(())
1616 }
1617
1618 fn cancel_gtd_expiry(&mut self, client_order_id: &ClientOrderId) {
1620 let core = self.core_mut();
1621
1622 if let Some(timer_name) = core.gtd_timers.remove(client_order_id) {
1623 core.clock().cancel_timer(timer_name.as_str());
1624 log::debug!("Canceled GTD expiry timer for {client_order_id}");
1625 }
1626 }
1627
1628 fn has_gtd_expiry_timer(&mut self, client_order_id: &ClientOrderId) -> bool {
1630 let core = self.core_mut();
1631 core.gtd_timers.contains_key(client_order_id)
1632 }
1633
1634 fn expire_gtd_order(&mut self, event: TimeEvent) {
1638 let timer_name = event.name.to_string();
1639 let Some(client_order_id_str) = timer_name.strip_prefix("GTD-EXPIRY:") else {
1640 log::error!("Invalid GTD timer name format: {timer_name}");
1641 return;
1642 };
1643
1644 let client_order_id = ClientOrderId::from(client_order_id_str);
1645
1646 let core = self.core_mut();
1647 core.gtd_timers.remove(&client_order_id);
1648
1649 let cache = core.cache();
1650 let Some(order) = cache.order(&client_order_id) else {
1651 log::warn!("GTD order {client_order_id} not found in cache");
1652 return;
1653 };
1654
1655 let order = order.clone();
1656 drop(cache);
1657
1658 log::info!("GTD order {client_order_id} expired");
1659
1660 if let Err(e) = self.cancel_order(order, None) {
1661 log::error!("Failed to cancel expired GTD order {client_order_id}: {e}");
1662 }
1663 }
1664
1665 fn reactivate_gtd_timers(&mut self) {
1670 let core = self.core_mut();
1671 let strategy_id = StrategyId::from(core.actor_id().inner().as_str());
1672 let current_time_ns = core.clock().timestamp_ns();
1673 let cache = core.cache();
1674
1675 let open_orders = cache.orders_open(None, None, Some(&strategy_id), None, None);
1676
1677 let gtd_orders: Vec<_> = open_orders
1678 .iter()
1679 .filter(|o| o.time_in_force() == TimeInForce::Gtd)
1680 .map(|o| (*o).clone())
1681 .collect();
1682
1683 drop(cache);
1684
1685 for order in gtd_orders {
1686 let Some(expire_time) = order.expire_time() else {
1687 continue;
1688 };
1689
1690 let expire_time_ns = expire_time.as_u64();
1691 let client_order_id = order.client_order_id();
1692
1693 if current_time_ns >= expire_time_ns {
1694 log::info!("GTD order {client_order_id} already expired, canceling immediately");
1695 if let Err(e) = self.cancel_order(order, None) {
1696 log::error!("Failed to cancel expired GTD order {client_order_id}: {e}");
1697 }
1698 } else if let Err(e) = self.set_gtd_expiry(&order) {
1699 log::error!("Failed to set GTD expiry timer for {client_order_id}: {e}");
1700 }
1701 }
1702 }
1703}
1704
1705#[cfg(test)]
1706mod tests {
1707 use std::{
1708 cell::RefCell,
1709 ops::{Deref, DerefMut},
1710 rc::Rc,
1711 };
1712
1713 use nautilus_common::{
1714 actor::{DataActor, DataActorCore},
1715 cache::Cache,
1716 clock::{Clock, TestClock},
1717 component::Component,
1718 timer::{TimeEvent, TimeEventCallback},
1719 };
1720 use nautilus_core::UnixNanos;
1721 use nautilus_model::{
1722 enums::{LiquiditySide, OrderSide, OrderType, PositionSide},
1723 events::{OrderCanceled, OrderFilled, OrderRejected},
1724 identifiers::{
1725 AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, TraderId,
1726 VenueOrderId,
1727 },
1728 orders::MarketOrder,
1729 stubs::TestDefault,
1730 types::Currency,
1731 };
1732 use nautilus_portfolio::portfolio::Portfolio;
1733 use rstest::rstest;
1734
1735 use super::*;
1736
1737 #[derive(Debug)]
1738 struct TestStrategy {
1739 core: StrategyCore,
1740 on_order_rejected_called: bool,
1741 on_position_opened_called: bool,
1742 }
1743
1744 impl TestStrategy {
1745 fn new(config: StrategyConfig) -> Self {
1746 Self {
1747 core: StrategyCore::new(config),
1748 on_order_rejected_called: false,
1749 on_position_opened_called: false,
1750 }
1751 }
1752 }
1753
1754 impl Deref for TestStrategy {
1755 type Target = DataActorCore;
1756 fn deref(&self) -> &Self::Target {
1757 &self.core
1758 }
1759 }
1760
1761 impl DerefMut for TestStrategy {
1762 fn deref_mut(&mut self) -> &mut Self::Target {
1763 &mut self.core
1764 }
1765 }
1766
1767 impl DataActor for TestStrategy {}
1768
1769 impl Strategy for TestStrategy {
1770 fn core(&self) -> &StrategyCore {
1771 &self.core
1772 }
1773
1774 fn core_mut(&mut self) -> &mut StrategyCore {
1775 &mut self.core
1776 }
1777
1778 fn on_order_rejected(&mut self, _event: OrderRejected) {
1779 self.on_order_rejected_called = true;
1780 }
1781
1782 fn on_position_opened(&mut self, _event: PositionOpened) {
1783 self.on_position_opened_called = true;
1784 }
1785 }
1786
1787 fn create_test_strategy() -> TestStrategy {
1788 let config = StrategyConfig {
1789 strategy_id: Some(StrategyId::from("TEST-001")),
1790 order_id_tag: Some("001".to_string()),
1791 ..Default::default()
1792 };
1793 TestStrategy::new(config)
1794 }
1795
1796 fn register_strategy(strategy: &mut TestStrategy) {
1797 let trader_id = TraderId::from("TRADER-001");
1798 let clock = Rc::new(RefCell::new(TestClock::new()));
1799 let cache = Rc::new(RefCell::new(Cache::default()));
1800 let portfolio = Rc::new(RefCell::new(Portfolio::new(
1801 cache.clone(),
1802 clock.clone(),
1803 None,
1804 )));
1805
1806 strategy
1807 .core
1808 .register(trader_id, clock, cache, portfolio)
1809 .unwrap();
1810 strategy.initialize().unwrap();
1811 }
1812
1813 fn start_strategy(strategy: &mut TestStrategy) {
1814 strategy.start().unwrap();
1815 }
1816
1817 #[rstest]
1818 fn test_strategy_creation() {
1819 let strategy = create_test_strategy();
1820 assert_eq!(
1821 strategy.core.config.strategy_id,
1822 Some(StrategyId::from("TEST-001"))
1823 );
1824 assert!(!strategy.on_order_rejected_called);
1825 assert!(!strategy.on_position_opened_called);
1826 }
1827
1828 #[rstest]
1829 fn test_strategy_registration() {
1830 let mut strategy = create_test_strategy();
1831 register_strategy(&mut strategy);
1832
1833 assert!(strategy.core.order_manager.is_some());
1834 assert!(strategy.core.order_factory.is_some());
1835 assert!(strategy.core.portfolio.is_some());
1836 }
1837
1838 #[rstest]
1839 fn test_handle_order_event_dispatches_to_handler() {
1840 let mut strategy = create_test_strategy();
1841 register_strategy(&mut strategy);
1842
1843 let event = OrderEventAny::Rejected(OrderRejected {
1844 trader_id: TraderId::from("TRADER-001"),
1845 strategy_id: StrategyId::from("TEST-001"),
1846 instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
1847 client_order_id: ClientOrderId::from("O-001"),
1848 account_id: AccountId::from("ACC-001"),
1849 reason: "Test rejection".into(),
1850 event_id: UUID4::default(),
1851 ts_event: UnixNanos::default(),
1852 ts_init: UnixNanos::default(),
1853 reconciliation: 0,
1854 due_post_only: 0,
1855 });
1856
1857 strategy.handle_order_event(event);
1858
1859 assert!(strategy.on_order_rejected_called);
1860 }
1861
1862 #[rstest]
1863 fn test_handle_position_event_dispatches_to_handler() {
1864 let mut strategy = create_test_strategy();
1865 register_strategy(&mut strategy);
1866
1867 let event = PositionEvent::PositionOpened(PositionOpened {
1868 trader_id: TraderId::from("TRADER-001"),
1869 strategy_id: StrategyId::from("TEST-001"),
1870 instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
1871 position_id: PositionId::test_default(),
1872 account_id: AccountId::from("ACC-001"),
1873 opening_order_id: ClientOrderId::from("O-001"),
1874 entry: OrderSide::Buy,
1875 side: PositionSide::Long,
1876 signed_qty: 1.0,
1877 quantity: Quantity::default(),
1878 last_qty: Quantity::default(),
1879 last_px: Price::default(),
1880 currency: Currency::from("USD"),
1881 avg_px_open: 0.0,
1882 event_id: UUID4::default(),
1883 ts_event: UnixNanos::default(),
1884 ts_init: UnixNanos::default(),
1885 });
1886
1887 strategy.handle_position_event(event);
1888
1889 assert!(strategy.on_position_opened_called);
1890 }
1891
1892 #[rstest]
1893 fn test_strategy_default_handlers_do_not_panic() {
1894 let mut strategy = create_test_strategy();
1895
1896 strategy.on_order_initialized(OrderInitialized::default());
1897 strategy.on_order_denied(OrderDenied::default());
1898 strategy.on_order_emulated(OrderEmulated::default());
1899 strategy.on_order_released(OrderReleased::default());
1900 strategy.on_order_submitted(OrderSubmitted::default());
1901 strategy.on_order_rejected(OrderRejected::default());
1902 let _ = DataActor::on_order_canceled(&mut strategy, &OrderCanceled::default());
1903 strategy.on_order_expired(OrderExpired::default());
1904 strategy.on_order_triggered(OrderTriggered::default());
1905 strategy.on_order_pending_update(OrderPendingUpdate::default());
1906 strategy.on_order_pending_cancel(OrderPendingCancel::default());
1907 strategy.on_order_modify_rejected(OrderModifyRejected::default());
1908 strategy.on_order_cancel_rejected(OrderCancelRejected::default());
1909 strategy.on_order_updated(OrderUpdated::default());
1910 }
1911
1912 #[rstest]
1915 fn test_has_gtd_expiry_timer_when_timer_not_set() {
1916 let mut strategy = create_test_strategy();
1917 let client_order_id = ClientOrderId::from("O-001");
1918
1919 assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
1920 }
1921
1922 #[rstest]
1923 fn test_has_gtd_expiry_timer_when_timer_set() {
1924 let mut strategy = create_test_strategy();
1925 let client_order_id = ClientOrderId::from("O-001");
1926
1927 strategy
1928 .core
1929 .gtd_timers
1930 .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
1931
1932 assert!(strategy.has_gtd_expiry_timer(&client_order_id));
1933 }
1934
1935 #[rstest]
1936 fn test_cancel_gtd_expiry_removes_timer() {
1937 let mut strategy = create_test_strategy();
1938 register_strategy(&mut strategy);
1939
1940 let client_order_id = ClientOrderId::from("O-001");
1941 strategy
1942 .core
1943 .gtd_timers
1944 .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
1945
1946 strategy.cancel_gtd_expiry(&client_order_id);
1947
1948 assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
1949 }
1950
1951 #[rstest]
1952 fn test_cancel_gtd_expiry_when_timer_not_set() {
1953 let mut strategy = create_test_strategy();
1954 register_strategy(&mut strategy);
1955
1956 let client_order_id = ClientOrderId::from("O-001");
1957
1958 strategy.cancel_gtd_expiry(&client_order_id);
1959
1960 assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
1961 }
1962
1963 #[rstest]
1964 fn test_handle_order_event_cancels_gtd_timer_on_filled() {
1965 let mut strategy = create_test_strategy();
1966 register_strategy(&mut strategy);
1967
1968 let client_order_id = ClientOrderId::from("O-001");
1969 strategy
1970 .core
1971 .gtd_timers
1972 .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
1973
1974 let event = OrderEventAny::Filled(OrderFilled {
1975 trader_id: TraderId::from("TRADER-001"),
1976 strategy_id: StrategyId::from("TEST-001"),
1977 instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
1978 client_order_id,
1979 venue_order_id: VenueOrderId::test_default(),
1980 account_id: AccountId::from("ACC-001"),
1981 trade_id: TradeId::test_default(),
1982 position_id: None,
1983 order_side: OrderSide::Buy,
1984 order_type: OrderType::Market,
1985 last_qty: Quantity::default(),
1986 last_px: Price::default(),
1987 currency: Currency::from("USD"),
1988 liquidity_side: LiquiditySide::Taker,
1989 event_id: UUID4::default(),
1990 ts_event: UnixNanos::default(),
1991 ts_init: UnixNanos::default(),
1992 reconciliation: false,
1993 commission: None,
1994 });
1995 strategy.handle_order_event(event);
1996
1997 assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
1998 }
1999
2000 #[rstest]
2001 fn test_handle_order_event_cancels_gtd_timer_on_canceled() {
2002 let mut strategy = create_test_strategy();
2003 register_strategy(&mut strategy);
2004
2005 let client_order_id = ClientOrderId::from("O-001");
2006 strategy
2007 .core
2008 .gtd_timers
2009 .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
2010
2011 let event = OrderEventAny::Canceled(OrderCanceled {
2012 trader_id: TraderId::from("TRADER-001"),
2013 strategy_id: StrategyId::from("TEST-001"),
2014 instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
2015 client_order_id,
2016 venue_order_id: Option::default(),
2017 account_id: Some(AccountId::from("ACC-001")),
2018 event_id: UUID4::default(),
2019 ts_event: UnixNanos::default(),
2020 ts_init: UnixNanos::default(),
2021 reconciliation: 0,
2022 });
2023 strategy.handle_order_event(event);
2024
2025 assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
2026 }
2027
2028 #[rstest]
2029 fn test_handle_order_event_cancels_gtd_timer_on_rejected() {
2030 let mut strategy = create_test_strategy();
2031 register_strategy(&mut strategy);
2032
2033 let client_order_id = ClientOrderId::from("O-001");
2034 strategy
2035 .core
2036 .gtd_timers
2037 .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
2038
2039 let event = OrderEventAny::Rejected(OrderRejected {
2040 trader_id: TraderId::from("TRADER-001"),
2041 strategy_id: StrategyId::from("TEST-001"),
2042 instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
2043 client_order_id,
2044 account_id: AccountId::from("ACC-001"),
2045 reason: "Test rejection".into(),
2046 event_id: UUID4::default(),
2047 ts_event: UnixNanos::default(),
2048 ts_init: UnixNanos::default(),
2049 reconciliation: 0,
2050 due_post_only: 0,
2051 });
2052 strategy.handle_order_event(event);
2053
2054 assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
2055 }
2056
2057 #[rstest]
2058 fn test_handle_order_event_cancels_gtd_timer_on_expired() {
2059 let mut strategy = create_test_strategy();
2060 register_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 Deref for MarketExitHookTrackingStrategy {
2306 type Target = DataActorCore;
2307 fn deref(&self) -> &Self::Target {
2308 &self.core
2309 }
2310 }
2311
2312 impl DerefMut for MarketExitHookTrackingStrategy {
2313 fn deref_mut(&mut self) -> &mut Self::Target {
2314 &mut self.core
2315 }
2316 }
2317
2318 impl DataActor for MarketExitHookTrackingStrategy {}
2319
2320 impl Strategy for MarketExitHookTrackingStrategy {
2321 fn core(&self) -> &StrategyCore {
2322 &self.core
2323 }
2324
2325 fn core_mut(&mut self) -> &mut StrategyCore {
2326 &mut self.core
2327 }
2328
2329 fn on_market_exit(&mut self) {
2330 self.on_market_exit_called = true;
2331 }
2332
2333 fn post_market_exit(&mut self) {
2334 self.post_market_exit_called = true;
2335 }
2336 }
2337
2338 #[rstest]
2339 fn test_market_exit_calls_on_market_exit_hook() {
2340 let config = StrategyConfig {
2341 strategy_id: Some(StrategyId::from("TEST-001")),
2342 order_id_tag: Some("001".to_string()),
2343 ..Default::default()
2344 };
2345 let mut strategy = MarketExitHookTrackingStrategy::new(config);
2346
2347 let trader_id = TraderId::from("TRADER-001");
2348 let clock = Rc::new(RefCell::new(TestClock::new()));
2349 let cache = Rc::new(RefCell::new(Cache::default()));
2350 let portfolio = Rc::new(RefCell::new(Portfolio::new(
2351 cache.clone(),
2352 clock.clone(),
2353 None,
2354 )));
2355 strategy
2356 .core
2357 .register(trader_id, clock, cache, portfolio)
2358 .unwrap();
2359 strategy.initialize().unwrap();
2360 strategy.start().unwrap();
2361
2362 let _ = strategy.market_exit();
2363
2364 assert!(strategy.on_market_exit_called);
2365 }
2366
2367 #[rstest]
2368 fn test_finalize_market_exit_calls_post_market_exit_hook() {
2369 let config = StrategyConfig {
2370 strategy_id: Some(StrategyId::from("TEST-001")),
2371 order_id_tag: Some("001".to_string()),
2372 ..Default::default()
2373 };
2374 let mut strategy = MarketExitHookTrackingStrategy::new(config);
2375
2376 let trader_id = TraderId::from("TRADER-001");
2377 let clock = Rc::new(RefCell::new(TestClock::new()));
2378 let cache = Rc::new(RefCell::new(Cache::default()));
2379 let portfolio = Rc::new(RefCell::new(Portfolio::new(
2380 cache.clone(),
2381 clock.clone(),
2382 None,
2383 )));
2384 strategy
2385 .core
2386 .register(trader_id, clock, cache, portfolio)
2387 .unwrap();
2388
2389 strategy.core.is_exiting = true;
2390 strategy.finalize_market_exit();
2391
2392 assert!(strategy.post_market_exit_called);
2393 }
2394
2395 #[derive(Debug)]
2396 struct FailingPostExitStrategy {
2397 core: StrategyCore,
2398 }
2399
2400 impl FailingPostExitStrategy {
2401 fn new(config: StrategyConfig) -> Self {
2402 Self {
2403 core: StrategyCore::new(config),
2404 }
2405 }
2406 }
2407
2408 impl Deref for FailingPostExitStrategy {
2409 type Target = DataActorCore;
2410 fn deref(&self) -> &Self::Target {
2411 &self.core
2412 }
2413 }
2414
2415 impl DerefMut for FailingPostExitStrategy {
2416 fn deref_mut(&mut self) -> &mut Self::Target {
2417 &mut self.core
2418 }
2419 }
2420
2421 impl DataActor for FailingPostExitStrategy {}
2422
2423 impl Strategy for FailingPostExitStrategy {
2424 fn core(&self) -> &StrategyCore {
2425 &self.core
2426 }
2427
2428 fn core_mut(&mut self) -> &mut StrategyCore {
2429 &mut self.core
2430 }
2431
2432 fn post_market_exit(&mut self) {
2433 panic!("Simulated error in post_market_exit");
2434 }
2435 }
2436
2437 #[rstest]
2438 fn test_finalize_market_exit_handles_hook_panic() {
2439 let config = StrategyConfig {
2440 strategy_id: Some(StrategyId::from("TEST-001")),
2441 order_id_tag: Some("001".to_string()),
2442 ..Default::default()
2443 };
2444 let mut strategy = FailingPostExitStrategy::new(config);
2445
2446 let trader_id = TraderId::from("TRADER-001");
2447 let clock = Rc::new(RefCell::new(TestClock::new()));
2448 let cache = Rc::new(RefCell::new(Cache::default()));
2449 let portfolio = Rc::new(RefCell::new(Portfolio::new(
2450 cache.clone(),
2451 clock.clone(),
2452 None,
2453 )));
2454 strategy
2455 .core
2456 .register(trader_id, clock, cache, portfolio)
2457 .unwrap();
2458
2459 strategy.core.is_exiting = true;
2460 strategy.core.pending_stop = true;
2461
2462 strategy.finalize_market_exit();
2464
2465 assert!(!strategy.core.is_exiting);
2467 assert!(!strategy.core.pending_stop);
2468 }
2469
2470 #[rstest]
2471 fn test_check_market_exit_increments_attempts_before_finalizing() {
2472 let mut strategy = create_test_strategy();
2473 register_strategy(&mut strategy);
2474
2475 strategy.core.is_exiting = true;
2476 assert_eq!(strategy.core.market_exit_attempts, 0);
2477
2478 let event = TimeEvent::new(
2479 Ustr::from("MARKET_EXIT_CHECK:TEST-001"),
2480 UUID4::new(),
2481 UnixNanos::default(),
2482 UnixNanos::default(),
2483 );
2484 strategy.check_market_exit(event);
2485
2486 assert!(!strategy.core.is_exiting);
2490 assert_eq!(strategy.core.market_exit_attempts, 0);
2491 }
2492
2493 #[rstest]
2494 fn test_check_market_exit_finalizes_when_max_attempts_reached() {
2495 let config = StrategyConfig {
2496 strategy_id: Some(StrategyId::from("TEST-001")),
2497 order_id_tag: Some("001".to_string()),
2498 market_exit_max_attempts: 3,
2499 ..Default::default()
2500 };
2501 let mut strategy = TestStrategy::new(config);
2502 register_strategy(&mut strategy);
2503
2504 strategy.core.is_exiting = true;
2505 strategy.core.market_exit_attempts = 2; let event = TimeEvent::new(
2508 Ustr::from("MARKET_EXIT_CHECK:TEST-001"),
2509 UUID4::new(),
2510 UnixNanos::default(),
2511 UnixNanos::default(),
2512 );
2513 strategy.check_market_exit(event);
2514
2515 assert!(!strategy.core.is_exiting);
2517 assert_eq!(strategy.core.market_exit_attempts, 0);
2518 }
2519
2520 #[rstest]
2521 fn test_check_market_exit_finalizes_when_no_orders_or_positions() {
2522 let mut strategy = create_test_strategy();
2523 register_strategy(&mut strategy);
2524
2525 strategy.core.is_exiting = true;
2526
2527 let event = TimeEvent::new(
2528 Ustr::from("MARKET_EXIT_CHECK:TEST-001"),
2529 UUID4::new(),
2530 UnixNanos::default(),
2531 UnixNanos::default(),
2532 );
2533 strategy.check_market_exit(event);
2534
2535 assert!(!strategy.core.is_exiting);
2537 }
2538
2539 #[rstest]
2540 fn test_market_exit_timer_name_format() {
2541 let config = StrategyConfig {
2542 strategy_id: Some(StrategyId::from("MY-STRATEGY-001")),
2543 ..Default::default()
2544 };
2545 let strategy = TestStrategy::new(config);
2546
2547 assert_eq!(
2548 strategy.core.market_exit_timer_name.as_str(),
2549 "MARKET_EXIT_CHECK:MY-STRATEGY-001"
2550 );
2551 }
2552
2553 #[rstest]
2554 fn test_reset_market_exit_state() {
2555 let mut strategy = create_test_strategy();
2556
2557 strategy.core.is_exiting = true;
2558 strategy.core.pending_stop = true;
2559 strategy.core.market_exit_attempts = 50;
2560
2561 strategy.core.reset_market_exit_state();
2562
2563 assert!(!strategy.core.is_exiting);
2564 assert!(!strategy.core.pending_stop);
2565 assert_eq!(strategy.core.market_exit_attempts, 0);
2566 }
2567
2568 #[rstest]
2569 fn test_cancel_market_exit_resets_state_without_hooks() {
2570 let config = StrategyConfig {
2571 strategy_id: Some(StrategyId::from("TEST-001")),
2572 order_id_tag: Some("001".to_string()),
2573 ..Default::default()
2574 };
2575 let mut strategy = MarketExitHookTrackingStrategy::new(config);
2576
2577 let trader_id = TraderId::from("TRADER-001");
2578 let clock = Rc::new(RefCell::new(TestClock::new()));
2579 let cache = Rc::new(RefCell::new(Cache::default()));
2580 let portfolio = Rc::new(RefCell::new(Portfolio::new(
2581 cache.clone(),
2582 clock.clone(),
2583 None,
2584 )));
2585 strategy
2586 .core
2587 .register(trader_id, clock, cache, portfolio)
2588 .unwrap();
2589
2590 strategy.core.is_exiting = true;
2592 strategy.core.pending_stop = true;
2593 strategy.core.market_exit_attempts = 50;
2594
2595 strategy.cancel_market_exit();
2597
2598 assert!(!strategy.core.is_exiting);
2600 assert!(!strategy.core.pending_stop);
2601 assert_eq!(strategy.core.market_exit_attempts, 0);
2602
2603 assert!(!strategy.on_market_exit_called);
2605 assert!(!strategy.post_market_exit_called);
2606 }
2607
2608 #[rstest]
2609 fn test_market_exit_returns_early_when_not_running() {
2610 let mut strategy = create_test_strategy();
2611 register_strategy(&mut strategy);
2612
2613 assert_ne!(strategy.core.actor.state(), ComponentState::Running);
2615
2616 let result = strategy.market_exit();
2617
2618 assert!(result.is_ok());
2620 assert!(!strategy.core.is_exiting);
2621 }
2622
2623 #[rstest]
2624 fn test_stop_with_manage_stop_false_cleans_up_active_exit() {
2625 let config = StrategyConfig {
2626 strategy_id: Some(StrategyId::from("TEST-001")),
2627 order_id_tag: Some("001".to_string()),
2628 manage_stop: false,
2629 ..Default::default()
2630 };
2631 let mut strategy = TestStrategy::new(config);
2632 register_strategy(&mut strategy);
2633
2634 strategy.core.is_exiting = true;
2636 strategy.core.market_exit_attempts = 5;
2637
2638 let should_proceed = Strategy::stop(&mut strategy);
2640
2641 assert!(should_proceed);
2643 assert!(!strategy.core.is_exiting);
2644 assert_eq!(strategy.core.market_exit_attempts, 0);
2645 }
2646
2647 #[rstest]
2648 fn test_stop_with_manage_stop_true_defers_when_running() {
2649 let config = StrategyConfig {
2650 strategy_id: Some(StrategyId::from("TEST-001")),
2651 order_id_tag: Some("001".to_string()),
2652 manage_stop: true,
2653 ..Default::default()
2654 };
2655 let mut strategy = TestStrategy::new(config);
2656
2657 let trader_id = TraderId::from("TRADER-001");
2659 let clock = Rc::new(RefCell::new(TestClock::new()));
2660 clock
2661 .borrow_mut()
2662 .register_default_handler(TimeEventCallback::from(|_event: TimeEvent| {}));
2663 let cache = Rc::new(RefCell::new(Cache::default()));
2664 let portfolio = Rc::new(RefCell::new(Portfolio::new(
2665 cache.clone(),
2666 clock.clone(),
2667 None,
2668 )));
2669 strategy
2670 .core
2671 .register(trader_id, clock, cache, portfolio)
2672 .unwrap();
2673 strategy.initialize().unwrap();
2674 strategy.start().unwrap();
2675
2676 let should_proceed = Strategy::stop(&mut strategy);
2677
2678 assert!(!should_proceed);
2680 assert!(strategy.core.pending_stop);
2681 }
2682
2683 #[rstest]
2684 fn test_stop_with_manage_stop_true_returns_early_if_pending() {
2685 let config = StrategyConfig {
2686 strategy_id: Some(StrategyId::from("TEST-001")),
2687 order_id_tag: Some("001".to_string()),
2688 manage_stop: true,
2689 ..Default::default()
2690 };
2691 let mut strategy = TestStrategy::new(config);
2692 register_strategy(&mut strategy);
2693 start_strategy(&mut strategy);
2694 strategy.core.pending_stop = true;
2695
2696 let should_proceed = Strategy::stop(&mut strategy);
2698
2699 assert!(!should_proceed);
2701 assert!(strategy.core.pending_stop);
2702 }
2703
2704 #[rstest]
2705 fn test_stop_with_manage_stop_true_proceeds_when_not_running() {
2706 let config = StrategyConfig {
2707 strategy_id: Some(StrategyId::from("TEST-001")),
2708 order_id_tag: Some("001".to_string()),
2709 manage_stop: true,
2710 ..Default::default()
2711 };
2712 let mut strategy = TestStrategy::new(config);
2713 register_strategy(&mut strategy);
2714
2715 assert_ne!(strategy.core.actor.state(), ComponentState::Running);
2717
2718 let should_proceed = Strategy::stop(&mut strategy);
2719
2720 assert!(should_proceed);
2722 }
2723
2724 #[rstest]
2725 fn test_finalize_market_exit_stops_strategy_when_pending() {
2726 let config = StrategyConfig {
2727 strategy_id: Some(StrategyId::from("TEST-001")),
2728 order_id_tag: Some("001".to_string()),
2729 ..Default::default()
2730 };
2731 let mut strategy = TestStrategy::new(config);
2732 register_strategy(&mut strategy);
2733 start_strategy(&mut strategy);
2734
2735 strategy.core.is_exiting = true;
2737 strategy.core.pending_stop = true;
2738
2739 strategy.finalize_market_exit();
2740
2741 assert_eq!(strategy.core.actor.state(), ComponentState::Stopped);
2743 assert!(!strategy.core.is_exiting);
2744 assert!(!strategy.core.pending_stop);
2745 }
2746
2747 #[rstest]
2748 fn test_finalize_market_exit_stays_running_when_not_pending() {
2749 let config = StrategyConfig {
2750 strategy_id: Some(StrategyId::from("TEST-001")),
2751 order_id_tag: Some("001".to_string()),
2752 ..Default::default()
2753 };
2754 let mut strategy = TestStrategy::new(config);
2755 register_strategy(&mut strategy);
2756 start_strategy(&mut strategy);
2757
2758 strategy.core.is_exiting = true;
2760 strategy.core.pending_stop = false;
2761
2762 strategy.finalize_market_exit();
2763
2764 assert_eq!(strategy.core.actor.state(), ComponentState::Running);
2766 assert!(!strategy.core.is_exiting);
2767 }
2768
2769 #[rstest]
2770 fn test_submit_order_denied_during_market_exit_when_not_reduce_only() {
2771 let mut strategy = create_test_strategy();
2772 register_strategy(&mut strategy);
2773 start_strategy(&mut strategy);
2774 strategy.core.is_exiting = true;
2775
2776 let order = OrderAny::Market(MarketOrder::new(
2777 TraderId::from("TRADER-001"),
2778 StrategyId::from("TEST-001"),
2779 InstrumentId::from("BTCUSDT.BINANCE"),
2780 ClientOrderId::from("O-20250208-0001"),
2781 OrderSide::Buy,
2782 Quantity::from(100_000),
2783 TimeInForce::Gtc,
2784 UUID4::new(),
2785 UnixNanos::default(),
2786 false, false,
2788 None,
2789 None,
2790 None,
2791 None,
2792 None,
2793 None,
2794 None,
2795 None,
2796 ));
2797 let client_order_id = order.client_order_id();
2798 let result = strategy.submit_order(order, None, None);
2799
2800 assert!(result.is_ok());
2801 let cache = strategy.core.cache();
2802 let cached_order = cache.order(&client_order_id).unwrap();
2803 assert_eq!(cached_order.status(), OrderStatus::Denied);
2804 }
2805
2806 #[rstest]
2807 fn test_submit_order_allowed_during_market_exit_when_reduce_only() {
2808 let mut strategy = create_test_strategy();
2809 register_strategy(&mut strategy);
2810 start_strategy(&mut strategy);
2811 strategy.core.is_exiting = true;
2812
2813 let order = OrderAny::Market(MarketOrder::new(
2814 TraderId::from("TRADER-001"),
2815 StrategyId::from("TEST-001"),
2816 InstrumentId::from("BTCUSDT.BINANCE"),
2817 ClientOrderId::from("O-20250208-0001"),
2818 OrderSide::Buy,
2819 Quantity::from(100_000),
2820 TimeInForce::Gtc,
2821 UUID4::new(),
2822 UnixNanos::default(),
2823 true, false,
2825 None,
2826 None,
2827 None,
2828 None,
2829 None,
2830 None,
2831 None,
2832 None,
2833 ));
2834 let client_order_id = order.client_order_id();
2835 let result = strategy.submit_order(order, None, None);
2836
2837 assert!(result.is_ok());
2838 let cache = strategy.core.cache();
2839 let cached_order = cache.order(&client_order_id).unwrap();
2840 assert_ne!(cached_order.status(), OrderStatus::Denied);
2841 }
2842
2843 #[rstest]
2844 fn test_submit_order_allowed_during_market_exit_when_tagged() {
2845 let mut strategy = create_test_strategy();
2846 register_strategy(&mut strategy);
2847 start_strategy(&mut strategy);
2848 strategy.core.is_exiting = true;
2849
2850 let order = OrderAny::Market(MarketOrder::new(
2851 TraderId::from("TRADER-001"),
2852 StrategyId::from("TEST-001"),
2853 InstrumentId::from("BTCUSDT.BINANCE"),
2854 ClientOrderId::from("O-20250208-0002"),
2855 OrderSide::Buy,
2856 Quantity::from(100_000),
2857 TimeInForce::Gtc,
2858 UUID4::new(),
2859 UnixNanos::default(),
2860 false, false,
2862 None,
2863 None,
2864 None,
2865 None,
2866 None,
2867 None,
2868 None,
2869 Some(vec![Ustr::from("MARKET_EXIT")]),
2870 ));
2871 let client_order_id = order.client_order_id();
2872 let result = strategy.submit_order(order, None, None);
2873
2874 assert!(result.is_ok());
2875 let cache = strategy.core.cache();
2876 let cached_order = cache.order(&client_order_id).unwrap();
2877 assert_ne!(cached_order.status(), OrderStatus::Denied);
2878 }
2879}