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