1pub mod api;
17#[doc(hidden)]
18pub mod binding;
19pub mod config;
20pub mod core;
21
22pub use core::{StrategyCore, StrategyNative};
23use std::panic::{AssertUnwindSafe, catch_unwind};
24
25use ahash::AHashSet;
26pub use api::{OrderApi, PortfolioApi};
27use binding::StrategyBinding;
28pub use config::{ImportableStrategyConfig, StrategyConfig};
29use nautilus_common::{
30 actor::DataActor,
31 component::Component,
32 enums::ComponentState,
33 logging::{CMD, EVT, RECV, SEND},
34 messages::execution::{
35 BatchCancelOrders, BatchModifyOrders, CancelAllOrders, CancelOrder, ModifyOrder,
36 QueryAccount, QueryOrder, SubmitOrder, SubmitOrderList, TradingCommand,
37 },
38 msgbus::{self, MessagingSwitchboard},
39 timer::TimeEvent,
40};
41use nautilus_core::{DurationNanos, Params, UUID4};
42use nautilus_execution::order_manager::OrderManagerAction;
43use nautilus_model::{
44 enums::{OrderSide, OrderStatus, OrderType, PositionSide, TimeInForce},
45 events::{
46 OrderAccepted, OrderCancelDenied, OrderCancelRejected, OrderCanceled, OrderDenied,
47 OrderEmulated, OrderEventAny, OrderExpired, OrderFillVoided, OrderFilled, OrderInitialized,
48 OrderModifyDenied, OrderModifyRejected, OrderPendingCancel, OrderPendingUpdate,
49 OrderRejected, OrderReleased, OrderSubmitted, OrderTriggered, OrderUpdated,
50 PositionChanged, PositionClosed, PositionEvent, PositionOpened,
51 },
52 identifiers::{
53 AccountId, ClientId, ClientOrderId, ExecAlgorithmId, InstrumentId, PositionId, StrategyId,
54 TraderId,
55 },
56 instruments::Instrument,
57 orders::{
58 LIMIT_ORDER_TYPES, Order, OrderAny, OrderCore, OrderError, OrderList, STOP_ORDER_TYPES,
59 },
60 position::Position,
61 types::{Price, Quantity},
62};
63use ustr::Ustr;
64
65pub type BatchModifyOrder = (
67 ClientOrderId,
68 Option<Quantity>,
69 Option<Price>,
70 Option<Price>,
71);
72
73pub trait Strategy: DataActor {
109 fn external_order_instrument_ids(&self) -> Option<Vec<InstrumentId>> {
113 None
114 }
115
116 fn set_external_order_instrument_ids(
127 &mut self,
128 instrument_ids: Vec<InstrumentId>,
129 ) -> anyhow::Result<()>
130 where
131 Self: StrategyNative,
132 {
133 let core = StrategyNative::strategy_core_mut(self);
134 let strategy_id = registered_strategy_id(core)?;
135 if !core.actor.is_registered() {
136 anyhow::bail!("Strategy {strategy_id} is not registered with a trader");
137 }
138 let cache = core.cache_rc();
139 cache
140 .try_borrow_mut()
141 .map_err(|e| anyhow::anyhow!("Cannot set external order claims: {e}"))?
142 .set_external_order_claims(strategy_id, &instrument_ids)?;
143 core.config.external_order_instrument_ids = Some(instrument_ids);
144 Ok(())
145 }
146
147 fn strategy_id(&self) -> Option<StrategyId>
149 where
150 Self: StrategyNative,
151 {
152 StrategyNative::strategy_core(self).strategy_id()
153 }
154
155 fn order(&self) -> OrderApi<'_>
157 where
158 Self: StrategyNative,
159 {
160 StrategyNative::strategy_core(self).order()
161 }
162
163 fn portfolio(&self) -> PortfolioApi<'_>
165 where
166 Self: StrategyNative,
167 {
168 StrategyNative::strategy_core(self).portfolio_api()
169 }
170
171 fn submit_order(
182 &mut self,
183 order: OrderAny,
184 position_id: Option<PositionId>,
185 client_id: Option<ClientId>,
186 params: Option<Params>,
187 ) -> anyhow::Result<()>
188 where
189 Self: StrategyBinding,
190 {
191 self.binding_submit_order(order, position_id, client_id, params)
192 }
193
194 fn submit_order_list(
201 &mut self,
202 mut orders: Vec<OrderAny>,
203 position_id: Option<PositionId>,
204 client_id: Option<ClientId>,
205 params: Option<Params>,
206 ) -> anyhow::Result<()>
207 where
208 Self: StrategyNative,
209 {
210 if orders.is_empty() {
211 anyhow::bail!("OrderList denied: no orders to submit");
212 }
213
214 for order in &orders {
215 if order.status() != OrderStatus::Initialized {
216 anyhow::bail!(
217 "Order in list denied: invalid status for {}, expected INITIALIZED",
218 order.client_order_id()
219 );
220 }
221 }
222
223 let first_venue = orders[0].instrument_id().venue;
224 for order in &orders {
225 if order.instrument_id().venue != first_venue {
226 anyhow::bail!(
227 "OrderList denied: orders must share the same venue; \
228 expected {first_venue}, found {} on {}",
229 order.instrument_id().venue,
230 order.client_order_id(),
231 );
232 }
233 }
234
235 let should_deny = {
236 let core = StrategyNative::strategy_core_mut(self);
237 let tag = core.market_exit_tag;
238 core.is_exiting
239 && orders.iter().any(|o| {
240 !o.is_reduce_only() && !o.tags().is_some_and(|tags| tags.contains(&tag))
241 })
242 };
243
244 if should_deny {
245 self.deny_order_list(&orders, Ustr::from("MARKET_EXIT_IN_PROGRESS"));
246 return Ok(());
247 }
248
249 let core = StrategyNative::strategy_core_mut(self);
250
251 let trader_id = registered_trader_id(core)?;
252 let strategy_id = registered_strategy_id(core)?;
253 let ts_init = core.clock_mut().timestamp_ns();
254
255 let order_list = if orders.first().is_some_and(|o| o.order_list_id().is_some()) {
257 OrderList::from_orders(&orders, ts_init)
258 } else {
259 core.order_factory().create_list(&mut orders, ts_init)
260 };
261
262 if let Err(e) = order_list.validate() {
263 anyhow::bail!("OrderList denied: {e}");
264 }
265
266 {
267 let cache_rc = core.cache_rc();
268 let mut cache = cache_rc.try_borrow_mut().map_err(|_| {
269 anyhow::anyhow!(
270 "Cannot submit order list {}: cache is currently borrowed",
271 order_list.id
272 )
273 })?;
274
275 if cache.order_list_exists(&order_list.id) {
276 anyhow::bail!("OrderList denied: duplicate {}", order_list.id);
277 }
278
279 for order in &orders {
280 if cache.order_exists(&order.client_order_id()) {
281 anyhow::bail!(
282 "Order in list denied: duplicate {}",
283 order.client_order_id()
284 );
285 }
286 }
287
288 cache.add_order_list(order_list.clone())?;
289 for order in &orders {
290 cache.add_order(order.clone(), position_id, client_id, true)?;
291 }
292 }
293
294 for order in &orders {
295 publish_order_initialized(order);
296 }
297
298 let params = params.filter(|params| !params.is_empty());
299
300 let first_order = orders.first();
301 let order_inits: Vec<_> = orders.iter().map(|o| o.init_event().clone()).collect();
302 let exec_algorithm_id = first_order.and_then(Order::exec_algorithm_id);
303
304 let command = SubmitOrderList::new(
305 trader_id,
306 client_id,
307 strategy_id,
308 order_list,
309 order_inits,
310 exec_algorithm_id,
311 position_id,
312 params,
313 UUID4::new(),
314 ts_init,
315 None, );
317
318 let has_emulated_order = orders
319 .iter()
320 .any(|o| o.emulation_trigger().is_some() || o.is_emulated());
321
322 if has_emulated_order {
323 send_emulator_command(TradingCommand::SubmitOrderList(command));
324 } else if let Some(algo_id) = exec_algorithm_id {
325 let endpoint = format!("{algo_id}.execute");
326 msgbus::send_any(endpoint.into(), &TradingCommand::SubmitOrderList(command));
327 } else {
328 send_risk_command(TradingCommand::SubmitOrderList(command));
329 }
330
331 for order in &orders {
332 self.set_gtd_expiry(order)?;
333 }
334
335 Ok(())
336 }
337
338 fn modify_order(
346 &mut self,
347 client_order_id: ClientOrderId,
348 quantity: Option<Quantity>,
349 price: Option<Price>,
350 trigger_price: Option<Price>,
351 client_id: Option<ClientId>,
352 params: Option<Params>,
353 ) -> anyhow::Result<()>
354 where
355 Self: StrategyNative,
356 {
357 let (trader_id, strategy_id) = {
358 let core = StrategyNative::strategy_core_mut(self);
359 (registered_trader_id(core)?, registered_strategy_id(core)?)
360 };
361
362 let params = params.filter(|params| !params.is_empty());
363
364 let order = StrategyNative::strategy_core_mut(self)
366 .cache_rc()
367 .borrow()
368 .try_order_owned(&client_order_id)
369 .map_err(|e| anyhow::anyhow!("Cannot modify order: {e}"))?;
370
371 let mut updating = false;
372
373 if quantity.is_some_and(|q| q != order.quantity() || order.is_pending_update()) {
374 updating = true;
375 }
376
377 if let Some(price) = price {
378 if !LIMIT_ORDER_TYPES.contains(&order.order_type()) {
379 anyhow::bail!("{} orders do not have a LIMIT price", order.order_type());
380 }
381
382 if Some(price) != order.price() {
383 updating = true;
384 }
385 }
386
387 if let Some(trigger_price) = trigger_price {
388 if !has_trigger_price(order.order_type()) {
389 anyhow::bail!(
390 "{} orders do not have a STOP trigger price",
391 order.order_type()
392 );
393 }
394
395 if Some(trigger_price) != order.trigger_price() {
396 updating = true;
397 }
398 }
399
400 if !updating {
401 anyhow::bail!(
402 "Cannot create command ModifyOrder: quantity, price, and trigger were either None \
403 or the same as existing values"
404 );
405 }
406
407 if order.is_closed() || order.is_pending_cancel() {
408 log::warn!(
409 "Cannot create command ModifyOrder: state is {:?}, {order:?}",
410 order.status()
411 );
412 return Ok(());
413 }
414
415 let command_id = UUID4::new();
416
417 if !self.mark_order_pending_update(&order, command_id)? {
418 return Ok(());
419 }
420
421 let command = ModifyOrder::new(
422 trader_id,
423 client_id,
424 strategy_id,
425 order.instrument_id(),
426 order.client_order_id(),
427 order.venue_order_id(),
428 quantity,
429 price,
430 trigger_price,
431 command_id,
432 StrategyNative::strategy_core_mut(self)
433 .clock_mut()
434 .timestamp_ns(),
435 params,
436 None, );
438
439 if order.emulation_trigger().is_some() || order.is_emulated() {
440 send_emulator_command(TradingCommand::ModifyOrder(command));
441 } else if let Some(algo_id) = order
442 .exec_algorithm_id()
443 .filter(|_| order.is_active_local())
444 {
445 let endpoint = format!("{algo_id}.execute");
446 msgbus::send_any(endpoint.into(), &TradingCommand::ModifyOrder(command));
447 } else {
448 send_risk_command(TradingCommand::ModifyOrder(command));
449 }
450 Ok(())
451 }
452
453 fn modify_orders(
462 &mut self,
463 updates: Vec<BatchModifyOrder>,
464 client_id: Option<ClientId>,
465 params: Option<Params>,
466 ) -> anyhow::Result<()>
467 where
468 Self: StrategyNative,
469 {
470 if updates.is_empty() {
471 anyhow::bail!("Cannot batch modify empty order list");
472 }
473
474 let (trader_id, strategy_id, ts_init) = {
475 let core = StrategyNative::strategy_core_mut(self);
476 (
477 registered_trader_id(core)?,
478 registered_strategy_id(core)?,
479 core.clock_mut().timestamp_ns(),
480 )
481 };
482
483 let orders: Vec<OrderAny> = {
484 let cache_rc = StrategyNative::strategy_core_mut(self).cache_rc();
485 let cache = cache_rc.borrow();
486 updates
487 .iter()
488 .map(|(client_order_id, _, _, _)| {
489 cache
490 .try_order_owned(client_order_id)
491 .map_err(|e| anyhow::anyhow!("Cannot modify order: {e}"))
492 })
493 .collect::<Result<_, _>>()?
494 };
495
496 let instrument_id = orders[0].instrument_id();
497
498 for (order, (_, quantity, price, trigger_price)) in orders.iter().zip(updates.iter()) {
499 if order.instrument_id() != instrument_id {
500 anyhow::bail!(
501 "Cannot batch modify orders for different instruments: {} vs {}",
502 instrument_id,
503 order.instrument_id()
504 );
505 }
506
507 if order.is_emulated() || order.is_active_local() {
508 anyhow::bail!("Cannot include emulated or local orders in batch modify");
509 }
510
511 let mut updating = false;
512
513 if quantity.is_some_and(|q| q != order.quantity()) {
514 updating = true;
515 }
516
517 if let Some(price) = price {
518 if !LIMIT_ORDER_TYPES.contains(&order.order_type()) {
519 anyhow::bail!("{} orders do not have a LIMIT price", order.order_type());
520 }
521
522 if Some(*price) != order.price() {
523 updating = true;
524 }
525 }
526
527 if let Some(trigger_price) = trigger_price {
528 if !has_trigger_price(order.order_type()) {
529 anyhow::bail!(
530 "{} orders do not have a STOP trigger price",
531 order.order_type()
532 );
533 }
534
535 if Some(*trigger_price) != order.trigger_price() {
536 updating = true;
537 }
538 }
539
540 if !updating {
541 anyhow::bail!(
542 "Cannot create command BatchModifyOrders: quantity, price, and trigger were \
543 either None or the same as existing values for {}",
544 order.client_order_id()
545 );
546 }
547
548 if order.is_closed() || order.is_pending_cancel() {
549 anyhow::bail!(
550 "Cannot create command BatchModifyOrders: state is {:?}, {order:?}",
551 order.status()
552 );
553 }
554 }
555
556 let params = params.filter(|params| !params.is_empty());
557 let mut modifies = Vec::with_capacity(orders.len());
558
559 for (order, (_, quantity, price, trigger_price)) in orders.into_iter().zip(updates) {
560 let command_id = UUID4::new();
561 if !self.mark_order_pending_update(&order, command_id)? {
562 continue;
563 }
564
565 modifies.push(ModifyOrder::new(
566 trader_id,
567 client_id,
568 strategy_id,
569 instrument_id,
570 order.client_order_id(),
571 order.venue_order_id(),
572 quantity,
573 price,
574 trigger_price,
575 command_id,
576 ts_init,
577 params.clone(),
578 None, ));
580 }
581
582 if modifies.is_empty() {
583 log::warn!("Cannot send `BatchModifyOrders`, no valid modify commands");
584 return Ok(());
585 }
586
587 let command = BatchModifyOrders::new(
588 trader_id,
589 client_id,
590 strategy_id,
591 instrument_id,
592 modifies,
593 UUID4::new(),
594 ts_init,
595 params,
596 None, );
598
599 send_risk_command(TradingCommand::ModifyOrders(command));
600 Ok(())
601 }
602
603 fn cancel_order(
609 &mut self,
610 client_order_id: ClientOrderId,
611 client_id: Option<ClientId>,
612 params: Option<Params>,
613 ) -> anyhow::Result<()>
614 where
615 Self: StrategyNative,
616 {
617 let (trader_id, strategy_id, ts_init) = {
618 let core = StrategyNative::strategy_core_mut(self);
619 (
620 registered_trader_id(core)?,
621 registered_strategy_id(core)?,
622 core.clock_mut().timestamp_ns(),
623 )
624 };
625
626 let params = params.filter(|params| !params.is_empty());
627
628 let order = StrategyNative::strategy_core_mut(self)
632 .cache_rc()
633 .borrow()
634 .try_order_owned(&client_order_id)
635 .map_err(|e| anyhow::anyhow!("Cannot cancel order: {e}"))?;
636
637 let command_id = UUID4::new();
638
639 if !self.mark_order_pending_cancel(&order, command_id)? {
640 return Ok(());
641 }
642
643 let command = CancelOrder::new(
644 trader_id,
645 client_id,
646 strategy_id,
647 order.instrument_id(),
648 order.client_order_id(),
649 order.venue_order_id(),
650 command_id,
651 ts_init,
652 params,
653 None, );
655
656 if order.emulation_trigger().is_some() || order.is_emulated() {
657 send_emulator_command(TradingCommand::CancelOrder(command));
658 } else if let Some(algo_id) = order
659 .exec_algorithm_id()
660 .filter(|_| order.is_active_local())
661 {
662 let endpoint = format!("{algo_id}.execute");
663 msgbus::send_any(endpoint.into(), &TradingCommand::CancelOrder(command));
664 } else {
665 send_exec_command(TradingCommand::CancelOrder(command));
666 }
667
668 if StrategyNative::strategy_core(self).config.manage_gtd_expiry
669 && order.time_in_force() == TimeInForce::Gtd
670 && StrategyNative::strategy_core(self)
671 .cache_ref()
672 .is_order_pending_cancel_local(&order.client_order_id())
673 && self.has_gtd_expiry_timer(&order.client_order_id())
674 {
675 self.cancel_gtd_expiry(&order.client_order_id());
676 }
677
678 Ok(())
679 }
680
681 fn cancel_orders(
688 &mut self,
689 client_order_ids: Vec<ClientOrderId>,
690 client_id: Option<ClientId>,
691 params: Option<Params>,
692 ) -> anyhow::Result<()>
693 where
694 Self: StrategyNative,
695 {
696 if client_order_ids.is_empty() {
697 anyhow::bail!("Cannot batch cancel empty order list");
698 }
699
700 let (trader_id, strategy_id, ts_init) = {
701 let core = StrategyNative::strategy_core_mut(self);
702 (
703 registered_trader_id(core)?,
704 registered_strategy_id(core)?,
705 core.clock_mut().timestamp_ns(),
706 )
707 };
708
709 let orders: Vec<OrderAny> = {
711 let cache_rc = StrategyNative::strategy_core_mut(self).cache_rc();
712 let cache = cache_rc.borrow();
713 client_order_ids
714 .iter()
715 .map(|id| {
716 cache
717 .try_order_owned(id)
718 .map_err(|e| anyhow::anyhow!("Cannot cancel order: {e}"))
719 })
720 .collect::<Result<_, _>>()?
721 };
722
723 let instrument_id = orders[0].instrument_id();
724
725 for order in &orders {
726 if order.instrument_id() != instrument_id {
727 anyhow::bail!(
728 "Cannot batch cancel orders for different instruments: {} vs {}",
729 instrument_id,
730 order.instrument_id()
731 );
732 }
733
734 if order.is_emulated() || order.is_active_local() {
735 anyhow::bail!("Cannot include emulated or local orders in batch cancel");
736 }
737 }
738
739 let mut cancels = Vec::with_capacity(orders.len());
740
741 for order in orders {
742 let command_id = UUID4::new();
743 if !self.mark_order_pending_cancel(&order, command_id)? {
744 continue;
745 }
746
747 cancels.push(CancelOrder::new(
748 trader_id,
749 client_id,
750 strategy_id,
751 instrument_id,
752 order.client_order_id(),
753 order.venue_order_id(),
754 command_id,
755 ts_init,
756 params.clone(),
757 None, ));
759 }
760
761 if cancels.is_empty() {
762 log::warn!("Cannot send `BatchCancelOrders`, no valid cancel commands");
763 return Ok(());
764 }
765
766 let command = BatchCancelOrders::new(
767 trader_id,
768 client_id,
769 strategy_id,
770 instrument_id,
771 cancels,
772 UUID4::new(),
773 ts_init,
774 params,
775 None, );
777
778 send_exec_command(TradingCommand::CancelOrders(command));
779 Ok(())
780 }
781
782 fn mark_order_pending_update(
788 &mut self,
789 order: &OrderAny,
790 command_id: UUID4,
791 ) -> anyhow::Result<bool>
792 where
793 Self: StrategyNative,
794 {
795 if order.is_active_local() {
796 return Ok(true);
797 }
798
799 let strategy_id = order.strategy_id();
800 required_account_id(order, "pending update")?;
801 let event =
802 OrderEventAny::PendingUpdate(self.generate_order_pending_update(order, command_id));
803
804 {
805 let cache_rc = StrategyNative::strategy_core_mut(self).cache_rc();
806 let mut cache = cache_rc.borrow_mut();
807 match cache.update_order(&event) {
808 Ok(_) => {}
809 Err(e)
810 if matches!(
811 e.downcast_ref::<OrderError>(),
812 Some(OrderError::InvalidStateTransition)
813 ) =>
814 {
815 log::warn!("InvalidStateTrigger: {e}, did not apply pending update event");
816 return Ok(false);
817 }
818 Err(e) => return Err(e),
819 }
820 }
821
822 let topic = format!("events.order.{strategy_id}");
823 msgbus::publish_order_event(topic.into(), &event);
824 msgbus::publish_order_event(
825 msgbus::switchboard::get_order_pending_update_topic(order.instrument_id()),
826 &event,
827 );
828
829 Ok(true)
830 }
831
832 fn mark_order_pending_cancel(
838 &mut self,
839 order: &OrderAny,
840 command_id: UUID4,
841 ) -> anyhow::Result<bool>
842 where
843 Self: StrategyNative,
844 {
845 if order.is_closed() || order.is_pending_cancel() {
846 log::warn!(
847 "Cannot cancel order: state is {:?}, {order:?}",
848 order.status()
849 );
850 return Ok(false);
851 }
852
853 if order.is_active_local() {
854 return Ok(true);
855 }
856
857 let strategy_id = order.strategy_id();
858 required_account_id(order, "pending cancel")?;
859 let event =
860 OrderEventAny::PendingCancel(self.generate_order_pending_cancel(order, command_id));
861
862 {
863 let cache_rc = StrategyNative::strategy_core_mut(self).cache_rc();
864 let mut cache = cache_rc.borrow_mut();
865 match cache.update_order(&event) {
866 Ok(_) => {}
867 Err(e)
868 if matches!(
869 e.downcast_ref::<OrderError>(),
870 Some(OrderError::InvalidStateTransition)
871 ) =>
872 {
873 log::warn!("InvalidStateTrigger: {e}, did not apply pending cancel event");
874 return Ok(false);
875 }
876 Err(e) => return Err(e),
877 }
878 cache.update_order_pending_cancel_local(order);
879 }
880
881 let topic = format!("events.order.{strategy_id}");
882 msgbus::publish_order_event(topic.into(), &event);
883 msgbus::publish_order_event(
884 msgbus::switchboard::get_order_pending_cancel_topic(order.instrument_id()),
885 &event,
886 );
887
888 Ok(true)
889 }
890
891 fn generate_order_pending_update(
893 &mut self,
894 order: &OrderAny,
895 command_id: UUID4,
896 ) -> OrderPendingUpdate
897 where
898 Self: StrategyNative,
899 {
900 let ts_now = StrategyNative::strategy_core_mut(self)
901 .clock_mut()
902 .timestamp_ns();
903
904 let mut event = OrderPendingUpdate::new(
905 order.trader_id(),
906 order.strategy_id(),
907 order.instrument_id(),
908 order.client_order_id(),
909 order.account_id(),
910 UUID4::new(),
911 ts_now,
912 ts_now,
913 false,
914 order.venue_order_id(),
915 );
916 event.causation_id = Some(command_id);
917 event
918 }
919
920 fn generate_order_pending_cancel(
922 &mut self,
923 order: &OrderAny,
924 command_id: UUID4,
925 ) -> OrderPendingCancel
926 where
927 Self: StrategyNative,
928 {
929 let ts_now = StrategyNative::strategy_core_mut(self)
930 .clock_mut()
931 .timestamp_ns();
932
933 let mut event = OrderPendingCancel::new(
934 order.trader_id(),
935 order.strategy_id(),
936 order.instrument_id(),
937 order.client_order_id(),
938 order.account_id(),
939 UUID4::new(),
940 ts_now,
941 ts_now,
942 false,
943 order.venue_order_id(),
944 );
945 event.causation_id = Some(command_id);
946 event
947 }
948
949 fn cancel_all_orders(
961 &mut self,
962 instrument_id: InstrumentId,
963 order_side: Option<OrderSide>,
964 client_id: Option<ClientId>,
965 strategy_only: bool,
966 params: Option<Params>,
967 ) -> anyhow::Result<()>
968 where
969 Self: StrategyNative,
970 {
971 let params = params.filter(|params| !params.is_empty());
972 let core = StrategyNative::strategy_core_mut(self);
973
974 let trader_id = registered_trader_id(core)?;
975 let strategy_id = registered_strategy_id(core)?;
976 let ts_init = core.clock_mut().timestamp_ns();
977
978 if !strategy_only {
979 let command_id = UUID4::new();
980 let command = CancelAllOrders::new(
981 trader_id,
982 client_id,
983 strategy_id,
984 instrument_id,
985 order_side,
986 command_id,
987 ts_init,
988 params,
989 Some(command_id),
990 );
991
992 send_exec_command(TradingCommand::CancelAllOrders(command));
993 return Ok(());
994 }
995
996 let cache = core.cache_ref();
997
998 let mut open_order_ids: Vec<ClientOrderId> = cache
999 .orders_open(
1000 None,
1001 Some(&instrument_id),
1002 Some(&strategy_id),
1003 None,
1004 order_side,
1005 )
1006 .into_iter()
1007 .map(|order| order.client_order_id())
1008 .collect();
1009
1010 let mut emulated_order_ids: Vec<ClientOrderId> = cache
1011 .orders_emulated(
1012 None,
1013 Some(&instrument_id),
1014 Some(&strategy_id),
1015 None,
1016 order_side,
1017 )
1018 .into_iter()
1019 .map(|order| order.client_order_id())
1020 .collect();
1021
1022 let mut inflight_order_ids: Vec<ClientOrderId> = cache
1023 .orders_inflight(
1024 None,
1025 Some(&instrument_id),
1026 Some(&strategy_id),
1027 None,
1028 order_side,
1029 )
1030 .into_iter()
1031 .map(|order| order.client_order_id())
1032 .collect();
1033
1034 let mut local_order_ids: Vec<ClientOrderId> = cache
1037 .orders_active_local(
1038 None,
1039 Some(&instrument_id),
1040 Some(&strategy_id),
1041 None,
1042 order_side,
1043 )
1044 .into_iter()
1045 .map(|order| order.client_order_id())
1046 .collect();
1047
1048 let mut exec_algorithm_ids: Vec<_> = cache.exec_algorithm_ids().into_iter().collect();
1052 exec_algorithm_ids.sort();
1053 let mut algo_order_ids: Vec<ClientOrderId> = Vec::new();
1054
1055 for algo_id in &exec_algorithm_ids {
1056 algo_order_ids.extend(
1057 cache
1058 .orders_for_exec_algorithm(
1059 algo_id,
1060 None,
1061 Some(&instrument_id),
1062 Some(&strategy_id),
1063 None,
1064 order_side,
1065 )
1066 .into_iter()
1067 .map(|order| order.client_order_id()),
1068 );
1069 }
1070
1071 let matches_client = |client_order_id: &ClientOrderId| {
1072 client_id.is_none_or(|client_id| {
1073 cache
1074 .client_id(client_order_id)
1075 .is_none_or(|order_client_id| *order_client_id == client_id)
1076 })
1077 };
1078
1079 open_order_ids.retain(&matches_client);
1080 emulated_order_ids.retain(&matches_client);
1081 inflight_order_ids.retain(&matches_client);
1082 local_order_ids.retain(&matches_client);
1083 algo_order_ids.retain(&matches_client);
1084
1085 let open_count = open_order_ids.len();
1086 let emulated_count = emulated_order_ids.len();
1087 let inflight_count = inflight_order_ids.len();
1088 let local_count = local_order_ids.len();
1089 let algo_count = algo_order_ids.len();
1090
1091 let mut cancel_routes: Vec<_> = open_order_ids
1092 .iter()
1093 .chain(&emulated_order_ids)
1094 .chain(&inflight_order_ids)
1095 .chain(&local_order_ids)
1096 .chain(&algo_order_ids)
1097 .map(|client_order_id| {
1098 (
1099 *client_order_id,
1100 client_id.or_else(|| cache.client_id(client_order_id).copied()),
1101 )
1102 })
1103 .collect();
1104 cancel_routes.sort_by_key(|(client_order_id, _)| *client_order_id);
1105 cancel_routes.dedup_by_key(|(client_order_id, _)| *client_order_id);
1106
1107 drop(cache);
1108
1109 if open_count == 0
1110 && emulated_count == 0
1111 && inflight_count == 0
1112 && local_count == 0
1113 && algo_count == 0
1114 {
1115 let side_str = order_side.map(|s| format!(" {s}")).unwrap_or_default();
1116 log::info!("No {instrument_id} open, emulated, or inflight{side_str} orders to cancel");
1117 return Ok(());
1118 }
1119
1120 let side_str = order_side.map(|s| format!(" {s}")).unwrap_or_default();
1121
1122 if open_count > 0 {
1123 log::info!(
1124 "Canceling {open_count} open{side_str} {instrument_id} order{}",
1125 if open_count == 1 { "" } else { "s" }
1126 );
1127 }
1128
1129 if emulated_count > 0 {
1130 log::info!(
1131 "Canceling {emulated_count} emulated{side_str} {instrument_id} order{}",
1132 if emulated_count == 1 { "" } else { "s" }
1133 );
1134 }
1135
1136 if inflight_count > 0 {
1137 log::info!(
1138 "Canceling {inflight_count} inflight{side_str} {instrument_id} order{}",
1139 if inflight_count == 1 { "" } else { "s" }
1140 );
1141 }
1142
1143 let mut error = None;
1144
1145 for (client_order_id, client_id) in cancel_routes {
1146 if let Err(e) = self.cancel_order(client_order_id, client_id, params.clone()) {
1147 error = Some(match error {
1148 Some(previous) => anyhow::anyhow!("{previous}; {e}"),
1149 None => e,
1150 });
1151 }
1152 }
1153
1154 error.map_or(Ok(()), Err)
1155 }
1156
1157 #[expect(clippy::too_many_arguments)]
1167 fn close_position(
1168 &mut self,
1169 position: &Position,
1170 client_id: Option<ClientId>,
1171 tags: Option<Vec<Ustr>>,
1172 time_in_force: Option<TimeInForce>,
1173 reduce_only: Option<bool>,
1174 quote_quantity: Option<bool>,
1175 params: Option<Params>,
1176 ) -> anyhow::Result<()>
1177 where
1178 Self: StrategyNative,
1179 {
1180 let core = StrategyNative::strategy_core_mut(self);
1181
1182 if position.is_closed() {
1183 log::warn!("Cannot close position (already closed): {}", position.id);
1184 return Ok(());
1185 }
1186
1187 let Some(closing_side) = OrderCore::closing_side(position.side) else {
1188 log::warn!("Cannot close flat position: {}", position.id);
1189 return Ok(());
1190 };
1191
1192 let Some(quantity) = closing_qty(core, position.instrument_id, position.quantity)? else {
1193 return Ok(());
1194 };
1195
1196 let order = core.order_factory().market(
1197 position.instrument_id,
1198 closing_side,
1199 quantity,
1200 time_in_force,
1201 reduce_only.or(Some(true)),
1202 quote_quantity,
1203 None,
1204 None,
1205 tags,
1206 None,
1207 );
1208
1209 self.submit_order(order, Some(position.id), client_id, params)
1210 }
1211
1212 #[expect(clippy::too_many_arguments)]
1222 fn close_all_positions(
1223 &mut self,
1224 instrument_id: InstrumentId,
1225 position_side: Option<PositionSide>,
1226 client_id: Option<ClientId>,
1227 tags: Option<Vec<Ustr>>,
1228 time_in_force: Option<TimeInForce>,
1229 reduce_only: Option<bool>,
1230 quote_quantity: Option<bool>,
1231 params: Option<Params>,
1232 ) -> anyhow::Result<()>
1233 where
1234 Self: StrategyNative,
1235 {
1236 let core = StrategyNative::strategy_core_mut(self);
1237 let strategy_id = registered_strategy_id(core)?;
1238 let cache = core.cache_ref();
1239
1240 let positions_open = cache.positions_open(
1241 None,
1242 Some(&instrument_id),
1243 Some(&strategy_id),
1244 None,
1245 position_side,
1246 );
1247
1248 let side_str = position_side.map(|s| format!(" {s}")).unwrap_or_default();
1249
1250 if positions_open.is_empty() {
1251 log::info!("No {instrument_id} open{side_str} positions to close");
1252 return Ok(());
1253 }
1254
1255 let count = positions_open.len();
1256 log::info!(
1257 "Closing {count} open{side_str} position{}",
1258 if count == 1 { "" } else { "s" }
1259 );
1260
1261 let positions_data: Vec<_> = positions_open
1262 .iter()
1263 .map(|p| (p.id, p.instrument_id, p.side, p.quantity, p.is_closed()))
1264 .collect();
1265 drop(positions_open);
1266
1267 drop(cache);
1268
1269 for (pos_id, pos_instrument_id, pos_side, pos_quantity, is_closed) in positions_data {
1270 if is_closed {
1271 continue;
1272 }
1273
1274 let core = StrategyNative::strategy_core_mut(self);
1275 let Some(closing_side) = OrderCore::closing_side(pos_side) else {
1276 continue;
1277 };
1278
1279 let Some(quantity) = closing_qty(core, pos_instrument_id, pos_quantity)? else {
1280 continue;
1281 };
1282
1283 let order = core.order_factory().market(
1284 pos_instrument_id,
1285 closing_side,
1286 quantity,
1287 time_in_force,
1288 reduce_only.or(Some(true)),
1289 quote_quantity,
1290 None,
1291 None,
1292 tags.clone(),
1293 None,
1294 );
1295
1296 self.submit_order(order, Some(pos_id), client_id, params.clone())?;
1297 }
1298
1299 Ok(())
1300 }
1301
1302 fn query_account(
1311 &mut self,
1312 account_id: AccountId,
1313 client_id: Option<ClientId>,
1314 params: Option<Params>,
1315 ) -> anyhow::Result<()>
1316 where
1317 Self: StrategyNative,
1318 {
1319 let core = StrategyNative::strategy_core_mut(self);
1320
1321 let trader_id = registered_trader_id(core)?;
1322 let ts_init = core.clock_mut().timestamp_ns();
1323
1324 let command = QueryAccount::new(
1325 trader_id,
1326 client_id,
1327 account_id,
1328 UUID4::new(),
1329 ts_init,
1330 params,
1331 None, );
1333
1334 send_exec_command(TradingCommand::QueryAccount(command));
1335 Ok(())
1336 }
1337
1338 fn query_order(
1347 &mut self,
1348 order: &OrderAny,
1349 client_id: Option<ClientId>,
1350 params: Option<Params>,
1351 ) -> anyhow::Result<()>
1352 where
1353 Self: StrategyNative,
1354 {
1355 let core = StrategyNative::strategy_core_mut(self);
1356
1357 let trader_id = registered_trader_id(core)?;
1358 let strategy_id = registered_strategy_id(core)?;
1359 let ts_init = core.clock_mut().timestamp_ns();
1360
1361 let command = QueryOrder::new(
1362 trader_id,
1363 client_id,
1364 strategy_id,
1365 order.instrument_id(),
1366 order.client_order_id(),
1367 order.venue_order_id(),
1368 UUID4::new(),
1369 ts_init,
1370 params,
1371 None, );
1373
1374 send_exec_command(TradingCommand::QueryOrder(command));
1375 Ok(())
1376 }
1377
1378 fn handle_order_event(&mut self, event: OrderEventAny)
1380 where
1381 Self: StrategyNative,
1382 {
1383 let state = {
1384 let core = StrategyNative::strategy_core_mut(self);
1385 let id = &core.actor.actor_id;
1386
1387 let is_warning = match &event {
1388 OrderEventAny::Rejected(event) => {
1389 !event.due_post_only || core.config.log_rejected_due_post_only_as_warning
1390 }
1391 OrderEventAny::Denied(_)
1392 | OrderEventAny::ModifyDenied(_)
1393 | OrderEventAny::CancelDenied(_)
1394 | OrderEventAny::CancelRejected(_)
1395 | OrderEventAny::ModifyRejected(_) => true,
1396 _ => false,
1397 };
1398
1399 if is_warning {
1400 log::warn!("{id} {RECV}{EVT} {event}");
1401 } else if core.actor.config.log_events {
1402 log::info!("{id} {RECV}{EVT} {event}");
1403 }
1404
1405 core.actor.state()
1406 };
1407
1408 let client_order_id = event.client_order_id();
1409 let cached_order_is_closed = {
1410 let core = StrategyNative::strategy_core_mut(self);
1411 core.cache_ref()
1412 .order(&client_order_id)
1413 .map(|order| order.is_closed())
1414 };
1415 let is_terminal = match &event {
1416 OrderEventAny::FillVoided(event) => {
1417 cached_order_is_closed.unwrap_or(!event.is_reopened)
1418 }
1419 OrderEventAny::Filled(_) => cached_order_is_closed.unwrap_or(true),
1420 OrderEventAny::Canceled(_)
1421 | OrderEventAny::Rejected(_)
1422 | OrderEventAny::Expired(_)
1423 | OrderEventAny::Denied(_) => true,
1424 _ => false,
1425 };
1426
1427 if is_terminal {
1430 self.cancel_gtd_expiry(&client_order_id);
1431 }
1432
1433 if state != ComponentState::Running {
1436 return;
1437 }
1438
1439 restore_gtd_timer(self, &event);
1440
1441 let manager_actions = {
1442 let core = StrategyNative::strategy_core_mut(self);
1443 if core.config.manage_contingent_orders {
1444 core.order_manager
1445 .as_mut()
1446 .map_or_else(Vec::new, |manager| manager.handle_event(&event))
1447 } else {
1448 Vec::new()
1449 }
1450 };
1451 self.dispatch_manager_actions(manager_actions);
1452
1453 match &event {
1454 OrderEventAny::Initialized(e) => self.on_order_initialized(e.clone()),
1455 OrderEventAny::Denied(e) => self.on_order_denied(*e),
1456 OrderEventAny::Emulated(e) => self.on_order_emulated(*e),
1457 OrderEventAny::Released(e) => self.on_order_released(*e),
1458 OrderEventAny::Submitted(e) => self.on_order_submitted(*e),
1459 OrderEventAny::Rejected(e) => self.on_order_rejected(*e),
1460 OrderEventAny::Accepted(e) => self.on_order_accepted(*e),
1461 OrderEventAny::Canceled(e) => self.on_order_canceled(e),
1462 OrderEventAny::Expired(e) => self.on_order_expired(*e),
1463 OrderEventAny::Triggered(e) => self.on_order_triggered(*e),
1464 OrderEventAny::PendingUpdate(e) => self.on_order_pending_update(*e),
1465 OrderEventAny::PendingCancel(e) => self.on_order_pending_cancel(*e),
1466 OrderEventAny::ModifyDenied(e) => self.on_order_modify_denied(*e),
1467 OrderEventAny::ModifyRejected(e) => self.on_order_modify_rejected(*e),
1468 OrderEventAny::CancelDenied(e) => self.on_order_cancel_denied(*e),
1469 OrderEventAny::CancelRejected(e) => self.on_order_cancel_rejected(*e),
1470 OrderEventAny::Updated(e) => self.on_order_updated(*e),
1471 OrderEventAny::Filled(e) => self.on_order_filled(e),
1472 OrderEventAny::FillVoided(e) => self.on_order_fill_voided(e),
1473 }
1474 self.on_order_event(event);
1475 }
1476
1477 fn dispatch_manager_actions(&mut self, actions: Vec<OrderManagerAction>)
1478 where
1479 Self: StrategyNative,
1480 {
1481 for action in actions {
1482 match action {
1483 OrderManagerAction::PublishInitialized(event) => {
1484 let topic = msgbus::switchboard::get_event_order_topic(event.strategy_id());
1485 msgbus::publish_order_event(topic, &event);
1486 }
1487 OrderManagerAction::SubmitToEmulator(command) => {
1488 send_emulator_command(TradingCommand::SubmitOrder(command));
1489 }
1490 OrderManagerAction::SubmitToRisk(command) => {
1491 send_risk_command(TradingCommand::SubmitOrder(command));
1492 }
1493 OrderManagerAction::SubmitToAlgorithm {
1494 command,
1495 exec_algorithm_id,
1496 } => send_algo_command(command, exec_algorithm_id),
1497 OrderManagerAction::CancelLocal(order) => {
1498 let client_order_id = order.client_order_id();
1499 if let Err(e) = self.cancel_order(client_order_id, None, None) {
1500 log::error!(
1501 "Failed to dispatch contingent cancel for {client_order_id}: {e}"
1502 );
1503 }
1504 }
1505 OrderManagerAction::ModifyLocalQuantity { order, quantity } => {
1506 let client_order_id = order.client_order_id();
1507 if let Err(e) =
1508 self.modify_order(client_order_id, Some(quantity), None, None, None, None)
1509 {
1510 log::error!(
1511 "Failed to dispatch contingent modify for {client_order_id}: {e}"
1512 );
1513 }
1514 }
1515 }
1516 }
1517 }
1518
1519 fn handle_position_event(&mut self, event: PositionEvent)
1521 where
1522 Self: StrategyNative,
1523 {
1524 let state = {
1525 let core = StrategyNative::strategy_core_mut(self);
1526
1527 if core.actor.config.log_events {
1528 let id = &core.actor.actor_id;
1529 log::info!("{id} {RECV}{EVT} {event:?}");
1530 }
1531
1532 core.actor.state()
1533 };
1534
1535 if state != ComponentState::Running {
1536 return;
1537 }
1538
1539 match &event {
1540 PositionEvent::PositionOpened(e) => self.on_position_opened(e.clone()),
1541 PositionEvent::PositionChanged(e) => self.on_position_changed(e.clone()),
1542 PositionEvent::PositionClosed(e) => self.on_position_closed(e.clone()),
1543 PositionEvent::PositionAdjusted(_) => {
1544 return;
1545 }
1546 }
1547 self.on_position_event(event);
1548 }
1549
1550 fn on_start(&mut self) -> anyhow::Result<()>
1561 where
1562 Self: StrategyNative,
1563 {
1564 let core = StrategyNative::strategy_core_mut(self);
1565 let strategy_id = registered_strategy_id(core)?;
1566 log::info!("Starting {strategy_id}");
1567
1568 if core.config.manage_gtd_expiry {
1569 self.reactivate_gtd_timers();
1570 }
1571
1572 Ok(())
1573 }
1574
1575 fn on_time_event(&mut self, event: &TimeEvent) -> anyhow::Result<()>
1585 where
1586 Self: StrategyNative + Component,
1587 {
1588 route_time_event(self, event);
1589 Ok(())
1590 }
1591
1592 #[allow(unused_variables)]
1598 fn on_order_initialized(&mut self, event: OrderInitialized) {}
1599
1600 #[allow(unused_variables)]
1604 fn on_order_event(&mut self, event: OrderEventAny) {}
1605
1606 #[allow(unused_variables)]
1610 fn on_order_denied(&mut self, event: OrderDenied) {}
1611
1612 #[allow(unused_variables)]
1616 fn on_order_emulated(&mut self, event: OrderEmulated) {}
1617
1618 #[allow(unused_variables)]
1622 fn on_order_released(&mut self, event: OrderReleased) {}
1623
1624 #[allow(unused_variables)]
1628 fn on_order_submitted(&mut self, event: OrderSubmitted) {}
1629
1630 #[allow(unused_variables)]
1634 fn on_order_rejected(&mut self, event: OrderRejected) {}
1635
1636 #[allow(unused_variables)]
1640 fn on_order_accepted(&mut self, event: OrderAccepted) {}
1641
1642 #[allow(unused_variables)]
1646 fn on_order_expired(&mut self, event: OrderExpired) {}
1647
1648 #[allow(unused_variables)]
1652 fn on_order_triggered(&mut self, event: OrderTriggered) {}
1653
1654 #[allow(unused_variables)]
1658 fn on_order_pending_update(&mut self, event: OrderPendingUpdate) {}
1659
1660 #[allow(unused_variables)]
1664 fn on_order_pending_cancel(&mut self, event: OrderPendingCancel) {}
1665
1666 #[allow(unused_variables)]
1668 fn on_order_modify_denied(&mut self, event: OrderModifyDenied) {}
1669
1670 #[allow(unused_variables)]
1674 fn on_order_modify_rejected(&mut self, event: OrderModifyRejected) {}
1675
1676 #[allow(unused_variables)]
1678 fn on_order_cancel_denied(&mut self, event: OrderCancelDenied) {}
1679
1680 #[allow(unused_variables)]
1684 fn on_order_cancel_rejected(&mut self, event: OrderCancelRejected) {}
1685
1686 #[allow(unused_variables)]
1690 fn on_order_updated(&mut self, event: OrderUpdated) {}
1691
1692 #[allow(unused_variables)]
1696 fn on_order_canceled(&mut self, event: &OrderCanceled) {}
1697
1698 #[allow(unused_variables)]
1702 fn on_order_filled(&mut self, event: &OrderFilled) {}
1703
1704 #[allow(unused_variables)]
1706 fn on_order_fill_voided(&mut self, event: &OrderFillVoided) {}
1707
1708 #[allow(unused_variables)]
1712 fn on_position_opened(&mut self, event: PositionOpened) {}
1713
1714 #[allow(unused_variables)]
1718 fn on_position_event(&mut self, event: PositionEvent) {}
1719
1720 #[allow(unused_variables)]
1724 fn on_position_changed(&mut self, event: PositionChanged) {}
1725
1726 #[allow(unused_variables)]
1730 fn on_position_closed(&mut self, event: PositionClosed) {}
1731
1732 fn on_market_exit(&mut self) {}
1736
1737 fn post_market_exit(&mut self) {}
1741
1742 fn is_exiting(&self) -> bool
1746 where
1747 Self: StrategyNative,
1748 {
1749 StrategyNative::strategy_core(self).is_exiting
1750 }
1751
1752 fn market_exit(&mut self) -> anyhow::Result<()>
1768 where
1769 Self: StrategyNative,
1770 {
1771 let core = StrategyNative::strategy_core_mut(self);
1772 let strategy_id = registered_strategy_id(core)?;
1773
1774 if core.actor.state() != ComponentState::Running {
1775 log::warn!("{strategy_id} Cannot market exit: strategy is not running");
1776 return Ok(());
1777 }
1778
1779 if core.is_exiting {
1780 log::warn!("{strategy_id} Market exit called when already in progress");
1781 return Ok(());
1782 }
1783
1784 core.is_exiting = true;
1785 core.market_exit_attempts = 0;
1786 let time_in_force = core.config.market_exit_time_in_force;
1787 let reduce_only = core.config.market_exit_reduce_only;
1788
1789 log::info!("{strategy_id} Initiating market exit...");
1790
1791 self.on_market_exit();
1792
1793 let core = StrategyNative::strategy_core_mut(self);
1794 let cache = core.cache_ref();
1795
1796 let mut instruments: AHashSet<InstrumentId> = AHashSet::new();
1797
1798 for client_order_id in cache
1799 .iter_client_order_ids_open(None, None, Some(&strategy_id), None)
1800 .chain(cache.iter_client_order_ids_inflight(None, None, Some(&strategy_id), None))
1801 .chain(cache.iter_client_order_ids_emulated(None, None, Some(&strategy_id), None))
1802 .chain(cache.iter_client_order_ids_active_local(None, None, Some(&strategy_id), None))
1803 {
1804 if let Some(order) = cache.order(&client_order_id) {
1805 instruments.insert(order.instrument_id());
1806 }
1807 }
1808
1809 for position_id in cache.iter_position_open_ids(None, None, Some(&strategy_id), None) {
1810 if let Some(position) = cache.position(&position_id) {
1811 instruments.insert(position.instrument_id);
1812 }
1813 }
1814
1815 let market_exit_tag = core.market_exit_tag;
1816 let mut instruments: Vec<_> = instruments.into_iter().collect();
1820 instruments.sort();
1821 drop(cache);
1822
1823 for instrument_id in instruments {
1824 if let Err(e) = self.cancel_all_orders(instrument_id, None, None, true, None) {
1825 log::error!("Error canceling orders for {instrument_id}: {e}");
1826 }
1827
1828 if let Err(e) = self.close_all_positions(
1829 instrument_id,
1830 None,
1831 None,
1832 Some(vec![market_exit_tag]),
1833 Some(time_in_force),
1834 Some(reduce_only),
1835 None,
1836 None,
1837 ) {
1838 log::error!("Error closing positions for {instrument_id}: {e}");
1839 }
1840 }
1841
1842 let core = StrategyNative::strategy_core_mut(self);
1843 let interval_ms = core.config.market_exit_interval_ms;
1844 let timer_name = core.market_exit_timer_name;
1845
1846 log::info!("{strategy_id} Setting market exit timer at {interval_ms}ms intervals");
1847
1848 let Ok(interval_ns) = DurationNanos::try_from_millis(interval_ms) else {
1849 core.is_exiting = false;
1850 core.market_exit_attempts = 0;
1851 anyhow::bail!("Market exit timer interval exceeds the nanosecond range");
1852 };
1853 let result = core.clock_mut().set_timer_ns(
1854 timer_name.as_str(),
1855 interval_ns,
1856 None,
1857 None,
1858 None,
1859 None,
1860 None,
1861 );
1862
1863 if let Err(e) = result {
1864 core.is_exiting = false;
1866 core.market_exit_attempts = 0;
1867 return Err(e);
1868 }
1869
1870 Ok(())
1871 }
1872
1873 fn check_market_exit(&mut self, _event: TimeEvent)
1877 where
1878 Self: StrategyNative + Component,
1879 {
1880 if !self.is_exiting() {
1882 return;
1883 }
1884
1885 let core = StrategyNative::strategy_core_mut(self);
1886 let Some(strategy_id) = core.strategy_id() else {
1887 log::error!("Cannot check market exit: strategy_id is not set");
1888 return;
1889 };
1890
1891 core.market_exit_attempts += 1;
1892 let attempts = core.market_exit_attempts;
1893 let max_attempts = core.config.market_exit_max_attempts;
1894
1895 log::debug!(
1896 "{strategy_id} Market exit check triggered (attempt {attempts}/{max_attempts})"
1897 );
1898
1899 if attempts >= max_attempts {
1900 let cache = core.cache_ref();
1901 let open_orders_count =
1902 cache.orders_open_count(None, None, Some(&strategy_id), None, None);
1903 let inflight_orders_count =
1904 cache.orders_inflight_count(None, None, Some(&strategy_id), None, None);
1905 let emulated_orders_count =
1906 cache.orders_emulated_count(None, None, Some(&strategy_id), None, None);
1907 let local_orders_count =
1908 cache.orders_active_local_count(None, None, Some(&strategy_id), None, None);
1909 let open_positions_count =
1910 cache.positions_open_count(None, None, Some(&strategy_id), None, None);
1911
1912 drop(cache);
1913
1914 log::warn!(
1915 "{strategy_id} Market exit max attempts ({max_attempts}) reached, \
1916 completing with open orders: {open_orders_count}, \
1917 inflight orders: {inflight_orders_count}, \
1918 emulated orders: {emulated_orders_count}, \
1919 active-local orders: {local_orders_count}, \
1920 open positions: {open_positions_count}"
1921 );
1922
1923 self.finalize_market_exit();
1924 return;
1925 }
1926
1927 let cache = core.cache_ref();
1928 let has_open_orders = cache.has_orders_open(None, None, Some(&strategy_id), None, None);
1929 let has_inflight_orders =
1930 cache.has_orders_inflight(None, None, Some(&strategy_id), None, None);
1931 let has_emulated_orders =
1932 cache.has_orders_emulated(None, None, Some(&strategy_id), None, None);
1933 let has_local_orders =
1934 cache.has_orders_active_local(None, None, Some(&strategy_id), None, None);
1935
1936 if has_open_orders || has_inflight_orders || has_emulated_orders || has_local_orders {
1937 return;
1938 }
1939
1940 let mut instrument_ids: Vec<_> = cache
1941 .positions_open(None, None, Some(&strategy_id), None, None)
1942 .iter()
1943 .filter_map(|position| {
1944 match closing_qty(core, position.instrument_id, position.quantity) {
1946 Ok(None) => None,
1947 Ok(Some(_)) | Err(_) => Some(position.instrument_id),
1948 }
1949 })
1950 .collect();
1951
1952 instrument_ids.sort();
1953 instrument_ids.dedup();
1954
1955 let time_in_force = core.config.market_exit_time_in_force;
1956 let reduce_only = core.config.market_exit_reduce_only;
1957 let market_exit_tag = core.market_exit_tag;
1958 drop(cache);
1959
1960 if instrument_ids.is_empty() {
1961 self.finalize_market_exit();
1962 return;
1963 }
1964
1965 for instrument_id in instrument_ids {
1966 if let Err(e) = self.close_all_positions(
1967 instrument_id,
1968 None,
1969 None,
1970 Some(vec![market_exit_tag]),
1971 Some(time_in_force),
1972 Some(reduce_only),
1973 None,
1974 None,
1975 ) {
1976 log::error!("Error re-submitting close orders for {instrument_id}: {e}");
1977 }
1978 }
1979 }
1980
1981 fn finalize_market_exit(&mut self)
1986 where
1987 Self: StrategyNative + Component,
1988 {
1989 let (actor_id, should_stop) = {
1990 let core = StrategyNative::strategy_core_mut(self);
1991 let actor_id = core.actor_id();
1992 let should_stop = core.pending_stop;
1993 (actor_id, should_stop)
1994 };
1995
1996 self.cancel_market_exit();
1997
1998 let hook_result = catch_unwind(AssertUnwindSafe(|| {
1999 self.post_market_exit();
2000 }));
2001
2002 if let Err(e) = hook_result {
2003 log::error!("{actor_id} Error in post_market_exit: {e:?}");
2004 }
2005
2006 if should_stop {
2007 log::info!("{actor_id} Market exit complete, stopping strategy");
2008
2009 if let Err(e) = Component::stop(self) {
2010 log::error!("{actor_id} Failed to stop: {e}");
2011 }
2012 }
2013
2014 let core = StrategyNative::strategy_core_mut(self);
2015 debug_assert!(
2016 !(core.pending_stop
2017 && !core.is_exiting
2018 && core.actor.state() == ComponentState::Running),
2019 "INVARIANT: stuck state after finalize_market_exit"
2020 );
2021 }
2022
2023 fn cancel_market_exit(&mut self)
2027 where
2028 Self: StrategyNative,
2029 {
2030 let core = StrategyNative::strategy_core_mut(self);
2031 let timer_name = core.market_exit_timer_name;
2032
2033 if core
2034 .clock_mut()
2035 .timer_names()
2036 .contains(&timer_name.as_str())
2037 {
2038 core.clock_mut().cancel_timer(timer_name.as_str());
2039 }
2040
2041 core.is_exiting = false;
2042 core.pending_stop = false;
2043 core.market_exit_attempts = 0;
2044 }
2045
2046 fn stop(&mut self) -> bool
2058 where
2059 Self: StrategyNative,
2060 {
2061 let (manage_stop, is_exiting, should_initiate_exit) = {
2062 let core = StrategyNative::strategy_core_mut(self);
2063 let actor_id = core.actor_id();
2064 let manage_stop = core.config.manage_stop;
2065 let state = core.actor.state();
2066 let pending_stop = core.pending_stop;
2067 let is_exiting = core.is_exiting;
2068
2069 if manage_stop {
2070 if state != ComponentState::Running {
2071 return true; }
2073
2074 if pending_stop {
2075 return false; }
2077
2078 core.pending_stop = true;
2079 let should_initiate_exit = !is_exiting;
2080
2081 if should_initiate_exit {
2082 log::info!("{actor_id} Initiating market exit before stop");
2083 }
2084
2085 (manage_stop, is_exiting, should_initiate_exit)
2086 } else {
2087 (manage_stop, is_exiting, false)
2088 }
2089 };
2090
2091 if manage_stop {
2092 if should_initiate_exit && let Err(e) = self.market_exit() {
2093 log::warn!("Market exit failed during stop: {e}, proceeding with stop");
2094 StrategyNative::strategy_core_mut(self).pending_stop = false;
2095 return true;
2096 }
2097 debug_assert!(
2098 self.is_exiting(),
2099 "INVARIANT: deferring stop but not exiting"
2100 );
2101 return false; }
2103
2104 if is_exiting {
2106 self.cancel_market_exit();
2107 }
2108
2109 true }
2111
2112 fn deny_order(&mut self, order: &OrderAny, reason: Ustr)
2117 where
2118 Self: StrategyNative,
2119 {
2120 let core = StrategyNative::strategy_core_mut(self);
2121 let Some(trader_id) = core.trader_id() else {
2122 log::error!(
2123 "Cannot deny order {}: trader_id is not set",
2124 order.client_order_id()
2125 );
2126 return;
2127 };
2128 let Some(strategy_id) = core.strategy_id() else {
2129 log::error!(
2130 "Cannot deny order {}: strategy_id is not set",
2131 order.client_order_id()
2132 );
2133 return;
2134 };
2135 let ts_now = core.clock_mut().timestamp_ns();
2136
2137 let event = OrderDenied::new(
2138 trader_id,
2139 strategy_id,
2140 order.instrument_id(),
2141 order.client_order_id(),
2142 reason,
2143 UUID4::new(),
2144 ts_now,
2145 ts_now,
2146 );
2147
2148 log::warn!(
2149 "{strategy_id} Order {} denied: {reason}",
2150 order.client_order_id()
2151 );
2152
2153 let publish_initialized = {
2154 let cache_rc = core.cache_rc();
2155 let mut cache = cache_rc.borrow_mut();
2156 if cache.order_exists(&order.client_order_id()) {
2157 false
2158 } else {
2159 match cache.add_order(order.clone(), None, None, true) {
2160 Ok(()) => true,
2161 Err(e) => {
2162 log::warn!("Failed to add denied order to cache: {e}");
2163 false
2164 }
2165 }
2166 }
2167 };
2168
2169 if publish_initialized {
2170 publish_order_initialized(order);
2171 }
2172
2173 let event = OrderEventAny::Denied(event);
2174 let applied = {
2175 let cache_rc = core.cache_rc();
2176 let mut cache = cache_rc.borrow_mut();
2177 if let Err(e) = cache.update_order(&event) {
2178 log::warn!("Failed to apply OrderDenied event: {e}");
2179 false
2180 } else {
2181 true
2182 }
2183 };
2184
2185 if applied {
2186 let topic = format!("events.order.{strategy_id}");
2187 msgbus::publish_order_event(topic.into(), &event);
2188 }
2189 }
2190
2191 fn deny_order_list(&mut self, orders: &[OrderAny], reason: Ustr)
2195 where
2196 Self: StrategyNative,
2197 {
2198 for order in orders {
2199 if !order.is_closed() {
2200 self.deny_order(order, reason);
2201 }
2202 }
2203 }
2204
2205 fn set_gtd_expiry(&mut self, order: &OrderAny) -> anyhow::Result<()>
2215 where
2216 Self: StrategyNative,
2217 {
2218 let core = StrategyNative::strategy_core_mut(self);
2219
2220 if !core.config.manage_gtd_expiry || order.time_in_force() != TimeInForce::Gtd {
2221 return Ok(());
2222 }
2223
2224 let Some(expire_time) = order.expire_time() else {
2225 return Ok(());
2226 };
2227
2228 let client_order_id = order.client_order_id();
2229 let timer_name = format!("GTD-EXPIRY:{client_order_id}");
2230
2231 let current_time_ns = {
2232 let clock = core.clock_mut();
2233 clock.timestamp_ns()
2234 };
2235
2236 if current_time_ns >= expire_time.as_u64() {
2237 log::info!("GTD order {client_order_id} already expired, canceling immediately");
2238 return self.cancel_order(order.client_order_id(), None, None);
2239 }
2240
2241 {
2242 let mut clock = core.clock_mut();
2243 clock.set_time_alert_ns(&timer_name, expire_time, None, None)?;
2244 }
2245
2246 core.gtd_timers
2247 .insert(client_order_id, Ustr::from(&timer_name));
2248
2249 log::debug!("Set GTD expiry timer for {client_order_id} at {expire_time}");
2250 Ok(())
2251 }
2252
2253 fn cancel_gtd_expiry(&mut self, client_order_id: &ClientOrderId)
2255 where
2256 Self: StrategyNative,
2257 {
2258 let core = StrategyNative::strategy_core_mut(self);
2259
2260 if let Some(timer_name) = core.gtd_timers.remove(client_order_id) {
2261 core.clock_mut().cancel_timer(timer_name.as_str());
2262 log::debug!("Canceled GTD expiry timer for {client_order_id}");
2263 }
2264 }
2265
2266 fn has_gtd_expiry_timer(&mut self, client_order_id: &ClientOrderId) -> bool
2268 where
2269 Self: StrategyNative,
2270 {
2271 let core = StrategyNative::strategy_core_mut(self);
2272 core.gtd_timers.contains_key(client_order_id)
2273 }
2274
2275 fn expire_gtd_order(&mut self, event: TimeEvent)
2279 where
2280 Self: StrategyNative,
2281 {
2282 let timer_name = event.name;
2283 let Some(client_order_id) = timer_name
2284 .strip_prefix("GTD-EXPIRY:")
2285 .and_then(|value| ClientOrderId::new_checked(value).ok())
2286 else {
2287 log::error!("Invalid GTD timer name format: {timer_name}");
2288 return;
2289 };
2290
2291 let core = StrategyNative::strategy_core_mut(self);
2292 if core.gtd_timers.get(&client_order_id) != Some(&timer_name) {
2293 return;
2294 }
2295 core.gtd_timers.remove(&client_order_id);
2296
2297 let order = core.cache_ref().order(&client_order_id).map(|o| o.clone());
2298 let Some(order) = order else {
2299 log::warn!("GTD order {client_order_id} not found in cache");
2300 return;
2301 };
2302
2303 log::info!("GTD order {client_order_id} expired");
2304
2305 if let Err(e) = self.cancel_order(order.client_order_id(), None, None) {
2306 log::error!("Failed to cancel expired GTD order {client_order_id}: {e}");
2307 }
2308 }
2309
2310 fn reactivate_gtd_timers(&mut self)
2315 where
2316 Self: StrategyNative,
2317 {
2318 let core = StrategyNative::strategy_core_mut(self);
2319 let Some(strategy_id) = core.strategy_id() else {
2320 log::error!("Cannot reactivate GTD timers: strategy_id is not set");
2321 return;
2322 };
2323 let current_time_ns = core.clock_mut().timestamp_ns();
2324
2325 let gtd_orders: Vec<OrderAny> = core
2326 .cache_ref()
2327 .orders_open(None, None, Some(&strategy_id), None, None)
2328 .into_iter()
2329 .filter(|o| o.time_in_force() == TimeInForce::Gtd)
2330 .map(|o| o.clone())
2331 .collect();
2332
2333 for order in gtd_orders {
2334 let Some(expire_time) = order.expire_time() else {
2335 continue;
2336 };
2337
2338 let expire_time_ns = expire_time.as_u64();
2339 let client_order_id = order.client_order_id();
2340
2341 if current_time_ns >= expire_time_ns {
2342 log::info!("GTD order {client_order_id} already expired, canceling immediately");
2343 if let Err(e) = self.cancel_order(order.client_order_id(), None, None) {
2344 log::error!("Failed to cancel expired GTD order {client_order_id}: {e}");
2345 }
2346 } else if let Err(e) = self.set_gtd_expiry(&order) {
2347 log::error!("Failed to set GTD expiry timer for {client_order_id}: {e}");
2348 }
2349 }
2350 }
2351}
2352
2353pub fn route_time_event<T>(strategy: &mut T, event: &TimeEvent)
2359where
2360 T: Strategy + StrategyNative + Component + ?Sized,
2361{
2362 let (gtd_order_id, is_market_exit) = {
2363 let core = StrategyNative::strategy_core(strategy);
2364 let gtd_order_id = event
2365 .name
2366 .strip_prefix("GTD-EXPIRY:")
2367 .and_then(|value| ClientOrderId::new_checked(value).ok())
2368 .filter(|client_order_id| core.gtd_timers.get(client_order_id) == Some(&event.name));
2369 let is_market_exit = event.name == core.market_exit_timer_name;
2370 (gtd_order_id, is_market_exit)
2371 };
2372
2373 if gtd_order_id.is_none() && !is_market_exit {
2374 return;
2375 }
2376
2377 let core = StrategyNative::strategy_core_mut(strategy);
2378 if core.managed_time_event_last_id == Some(event.event_id) {
2379 return;
2380 }
2381 core.managed_time_event_last_id = Some(event.event_id);
2382
2383 if gtd_order_id.is_some() {
2384 strategy.expire_gtd_order(event.clone());
2385 } else {
2386 strategy.check_market_exit(event.clone());
2387 }
2388}
2389
2390pub(super) fn submit_order_native<T>(
2391 strategy: &mut T,
2392 order: &OrderAny,
2393 position_id: Option<PositionId>,
2394 client_id: Option<ClientId>,
2395 params: Option<Params>,
2396) -> anyhow::Result<()>
2397where
2398 T: Strategy + StrategyNative + ?Sized,
2399{
2400 let core = StrategyNative::strategy_core_mut(strategy);
2401
2402 let trader_id = registered_trader_id(core)?;
2403 let strategy_id = registered_strategy_id(core)?;
2404 let ts_init = core.clock_mut().timestamp_ns();
2405
2406 if order.status() != OrderStatus::Initialized {
2407 anyhow::bail!(
2408 "Order denied: invalid status for {}, expected INITIALIZED",
2409 order.client_order_id()
2410 );
2411 }
2412
2413 let market_exit_tag = core.market_exit_tag;
2414 let is_market_exit_order = order
2415 .tags()
2416 .is_some_and(|tags| tags.contains(&market_exit_tag));
2417 let should_deny_for_market_exit =
2418 core.is_exiting && !order.is_reduce_only() && !is_market_exit_order;
2419
2420 let params = params.filter(|params| !params.is_empty());
2421
2422 {
2423 let cache_rc = core.cache_rc();
2424 let mut cache = cache_rc.try_borrow_mut().map_err(|_| {
2425 anyhow::anyhow!(
2426 "Cannot submit order {}: cache is currently borrowed",
2427 order.client_order_id()
2428 )
2429 })?;
2430
2431 if cache
2432 .order(&order.client_order_id())
2433 .is_some_and(|cached_order| !cached_order.is_active_local())
2434 {
2435 anyhow::bail!("Order denied: duplicate {}", order.client_order_id());
2436 }
2437
2438 if !should_deny_for_market_exit {
2439 cache.add_order(order.clone(), position_id, client_id, true)?;
2440 }
2441 }
2442
2443 if should_deny_for_market_exit {
2444 strategy.deny_order(order, Ustr::from("MARKET_EXIT_IN_PROGRESS"));
2445 return Ok(());
2446 }
2447
2448 publish_order_initialized(order);
2449
2450 let command = SubmitOrder::new(
2451 trader_id,
2452 client_id,
2453 strategy_id,
2454 order.instrument_id(),
2455 order.client_order_id(),
2456 order.init_event().clone(),
2457 order.exec_algorithm_id(),
2458 position_id,
2459 params,
2460 UUID4::new(),
2461 ts_init,
2462 None, );
2464
2465 if order.emulation_trigger().is_some() {
2466 send_emulator_command(TradingCommand::SubmitOrder(command));
2467 } else if let Some(exec_algorithm_id) = order.exec_algorithm_id() {
2468 send_algo_command(command, exec_algorithm_id);
2469 } else {
2470 send_risk_command(TradingCommand::SubmitOrder(command));
2471 }
2472
2473 strategy.set_gtd_expiry(order)?;
2474 Ok(())
2475}
2476
2477fn restore_gtd_timer<T: Strategy + StrategyNative + ?Sized>(
2478 strategy: &mut T,
2479 event: &OrderEventAny,
2480) {
2481 match event {
2482 OrderEventAny::CancelDenied(_) | OrderEventAny::CancelRejected(_) => {}
2483 OrderEventAny::FillVoided(event) if event.is_reopened => {}
2484 _ => return,
2485 }
2486
2487 let client_order_id = event.client_order_id();
2488 let order = StrategyNative::strategy_core_mut(strategy)
2489 .cache_ref()
2490 .order(&client_order_id)
2491 .map(|order| order.clone());
2492
2493 let Some(order) = order else {
2494 return;
2495 };
2496
2497 match event {
2498 OrderEventAny::CancelDenied(_) | OrderEventAny::CancelRejected(_) => {
2499 if !(order.is_open() || order.is_inflight()) || order.has_pending_cancel_command() {
2500 return;
2501 }
2502 }
2503 OrderEventAny::FillVoided(event) if event.is_reopened => {
2504 if !order.is_open() {
2505 return;
2506 }
2507 }
2508 _ => return,
2509 }
2510
2511 if strategy.has_gtd_expiry_timer(&client_order_id) {
2512 return;
2513 }
2514
2515 let now = StrategyNative::strategy_core_mut(strategy)
2516 .clock_mut()
2517 .timestamp_ns();
2518
2519 if matches!(event, OrderEventAny::CancelDenied(_))
2520 && order.expire_time().is_some_and(|expiry| expiry <= now)
2521 {
2522 log::warn!("GTD cancel denied for expired order {client_order_id}; order remains open");
2523 return;
2524 }
2525
2526 if let Err(e) = strategy.set_gtd_expiry(&order) {
2527 log::error!("Failed to restore GTD expiry for {client_order_id}: {e}");
2528 }
2529}
2530
2531fn publish_order_initialized(order: &OrderAny) {
2532 let topic = msgbus::switchboard::get_event_order_topic(order.strategy_id());
2533 let event = OrderEventAny::Initialized(order.init_event().clone());
2534 msgbus::publish_order_event(topic, &event);
2535}
2536
2537fn send_emulator_command(command: TradingCommand) {
2538 log_cmd_send(&command);
2539 let endpoint = MessagingSwitchboard::order_emulator_execute();
2540 msgbus::send_trading_command(endpoint, command);
2541}
2542
2543fn send_algo_command(command: SubmitOrder, exec_algorithm_id: ExecAlgorithmId) {
2544 let id = command.strategy_id;
2545 log::info!("{id} {CMD}{SEND} {command}");
2546
2547 let endpoint = format!("{exec_algorithm_id}.execute");
2548 msgbus::send_any(endpoint.into(), &TradingCommand::SubmitOrder(command));
2549}
2550
2551fn send_risk_command(command: TradingCommand) {
2552 log_cmd_send(&command);
2553 let endpoint = MessagingSwitchboard::risk_engine_queue_execute();
2554 msgbus::send_trading_command(endpoint, command);
2555}
2556
2557fn send_exec_command(command: TradingCommand) {
2558 log_cmd_send(&command);
2559 let endpoint = MessagingSwitchboard::exec_engine_queue_execute();
2560 msgbus::send_trading_command(endpoint, command);
2561}
2562
2563fn log_cmd_send(command: &TradingCommand) {
2564 if let Some(id) = command.strategy_id() {
2565 log::info!("{id} {CMD}{SEND} {command}");
2566 } else {
2567 log::info!("{CMD}{SEND} {command}");
2568 }
2569}
2570
2571fn closing_qty(
2572 core: &StrategyCore,
2573 instrument_id: InstrumentId,
2574 quantity: Quantity,
2575) -> anyhow::Result<Option<Quantity>> {
2576 let cache = core.cache_ref();
2577
2578 let instrument = cache.instrument(&instrument_id).ok_or_else(|| {
2579 anyhow::anyhow!("Cannot close position: instrument {instrument_id} is not cached")
2580 })?;
2581
2582 let Some(rounded) = quantity.checked_floor_to_increment(instrument.size_increment()) else {
2583 anyhow::bail!("close quantity cannot be aligned to the size increment");
2584 };
2585
2586 if rounded.is_zero() {
2587 log::warn!(
2588 "Cannot close {instrument_id} position: quantity {quantity} is below the size increment"
2589 );
2590 return Ok(None);
2591 }
2592
2593 Ok(Some(rounded))
2594}
2595
2596fn registered_trader_id(core: &StrategyCore) -> anyhow::Result<TraderId> {
2597 core.trader_id()
2598 .ok_or_else(|| anyhow::anyhow!("Strategy not registered: trader_id is not set"))
2599}
2600
2601fn registered_strategy_id(core: &StrategyCore) -> anyhow::Result<StrategyId> {
2602 core.strategy_id()
2603 .ok_or_else(|| anyhow::anyhow!("Strategy not registered: strategy_id is not set"))
2604}
2605
2606fn has_trigger_price(order_type: OrderType) -> bool {
2609 STOP_ORDER_TYPES.contains(&order_type)
2610 || matches!(
2611 order_type,
2612 OrderType::TrailingStopMarket | OrderType::TrailingStopLimit
2613 )
2614}
2615
2616fn required_account_id(order: &OrderAny, operation: &str) -> anyhow::Result<AccountId> {
2617 order.account_id().ok_or_else(|| {
2618 anyhow::anyhow!(
2619 "Cannot generate {operation} event for {}: account_id is not set",
2620 order.client_order_id()
2621 )
2622 })
2623}
2624
2625#[cfg(test)]
2626mod tests {
2627 use std::{cell::RefCell, rc::Rc, sync::Mutex};
2628
2629 use nautilus_common::{
2630 actor::{
2631 DataActor,
2632 registry::{deregister_actor, try_get_actor_unchecked},
2633 },
2634 cache::{Cache, ORDER_NOT_FOUND},
2635 clock::{Clock, VirtualClock},
2636 component::{Component, deregister_component, register_component_actor},
2637 enums::ComponentState,
2638 logging::{
2639 arm_shutdown_on_error, disarm_shutdown_on_error, init_logging,
2640 take_shutdown_on_error_trigger,
2641 },
2642 msgbus::{
2643 self, MessagingSwitchboard, TypedHandler, TypedIntoHandler,
2644 stubs::{
2645 TypedIntoMessageSavingHandler, TypedMessageSavingHandler, get_any_saving_handler,
2646 get_typed_into_message_saving_handler, get_typed_message_saving_handler,
2647 },
2648 },
2649 timer::{TimeEvent, TimeEventCallback},
2650 };
2651 use nautilus_core::{DurationNanos, UnixNanos};
2652 use nautilus_execution::{engine::ExecutionEngine, order_emulator::emulator::OrderEmulator};
2653 use nautilus_model::{
2654 data::QuoteTick,
2655 enums::{
2656 ContingencyType, LiquiditySide, OmsType, OrderSide, OrderStatus, OrderType,
2657 PositionAdjustmentType, PositionSide, TrailingOffsetType, TriggerType,
2658 },
2659 events::{
2660 OrderAccepted, OrderCanceled, OrderFilled, OrderRejected, PositionAdjusted,
2661 order::spec::{
2662 OrderAcceptedSpec, OrderCancelDeniedSpec, OrderCancelRejectedSpec,
2663 OrderCanceledSpec, OrderEmulatedSpec, OrderExpiredSpec, OrderFillVoidedSpec,
2664 OrderFilledSpec, OrderModifyRejectedSpec, OrderRejectedSpec, OrderReleasedSpec,
2665 OrderUpdatedSpec,
2666 },
2667 },
2668 identifiers::{
2669 AccountId, ActorId, ClientOrderId, InstrumentId, OrderListId, PositionId, StrategyId,
2670 TradeId, TraderId, VenueOrderId,
2671 },
2672 instruments::{InstrumentAny, stubs::currency_pair_btcusdt},
2673 orderbook::own::OwnOrderBook,
2674 orders::{LimitOrder, MarketOrder, OrderTestBuilder, stubs::TestOrderEventStubs},
2675 stubs::TestDefault,
2676 types::{Currency, Money, Price},
2677 };
2678 use nautilus_portfolio::portfolio::Portfolio;
2679 use rstest::rstest;
2680 use rust_decimal::Decimal;
2681 use serde_json::Value;
2682
2683 use super::*;
2684 use crate::{cache_database::FailNthAddOrderDatabase, nautilus_strategy};
2685
2686 #[derive(Debug)]
2687 struct TestStrategy {
2688 core: StrategyCore,
2689 start_calls: usize,
2690 on_order_rejected_called: bool,
2691 on_order_event_called: bool,
2692 on_order_accepted_called: bool,
2693 on_order_canceled_called: bool,
2694 on_order_filled_called: bool,
2695 on_order_fill_voided_called: bool,
2696 on_order_expired_called: bool,
2697 order_event_timeline: Rc<RefCell<Vec<&'static str>>>,
2698 on_position_event_called: bool,
2699 on_position_opened_called: bool,
2700 on_position_changed_called: bool,
2701 on_position_closed_called: bool,
2702 }
2703
2704 #[derive(Debug)]
2705 struct ModifyRejectionStrategy {
2706 core: StrategyCore,
2707 rejections: Vec<(OrderModifyRejected, OrderAny)>,
2708 }
2709
2710 #[derive(Debug)]
2711 struct CoreFreeStrategy {
2712 started: bool,
2713 }
2714
2715 #[derive(Debug)]
2716 struct InitializedModifyStrategy {
2717 core: StrategyCore,
2718 modified_quantity: Quantity,
2719 }
2720
2721 #[derive(Debug)]
2722 struct TimerOverrideStrategy {
2723 core: StrategyCore,
2724 gtd_expiries: usize,
2725 market_exit_checks: usize,
2726 }
2727
2728 impl DataActor for ModifyRejectionStrategy {}
2729
2730 nautilus_strategy!(ModifyRejectionStrategy, {
2731 fn on_order_modify_rejected(&mut self, event: OrderModifyRejected) {
2732 let order = self.cache().order(&event.client_order_id).unwrap();
2733 self.rejections.push((event, order));
2734 }
2735 });
2736
2737 impl DataActor for CoreFreeStrategy {
2738 fn on_start(&mut self) -> anyhow::Result<()> {
2739 self.started = true;
2740 Ok(())
2741 }
2742 }
2743
2744 impl Strategy for CoreFreeStrategy {}
2745
2746 impl DataActor for InitializedModifyStrategy {}
2747
2748 nautilus_strategy!(InitializedModifyStrategy, {
2749 fn on_order_initialized(&mut self, event: OrderInitialized) {
2750 self.modify_order(
2751 event.client_order_id,
2752 Some(self.modified_quantity),
2753 None,
2754 None,
2755 None,
2756 None,
2757 )
2758 .unwrap();
2759 }
2760 });
2761
2762 impl DataActor for TimerOverrideStrategy {
2763 fn on_time_event(&mut self, event: &TimeEvent) -> anyhow::Result<()> {
2764 Strategy::on_time_event(self, event)
2765 }
2766 }
2767
2768 nautilus_strategy!(TimerOverrideStrategy, {
2769 fn check_market_exit(&mut self, _event: TimeEvent) {
2770 self.market_exit_checks += 1;
2771 }
2772
2773 fn expire_gtd_order(&mut self, _event: TimeEvent) {
2774 self.gtd_expiries += 1;
2775 }
2776 });
2777
2778 impl TestStrategy {
2779 fn new(config: StrategyConfig) -> Self {
2780 Self {
2781 core: StrategyCore::new(config),
2782 start_calls: 0,
2783 on_order_rejected_called: false,
2784 on_order_event_called: false,
2785 on_order_accepted_called: false,
2786 on_order_canceled_called: false,
2787 on_order_filled_called: false,
2788 on_order_fill_voided_called: false,
2789 on_order_expired_called: false,
2790 order_event_timeline: Rc::new(RefCell::new(Vec::new())),
2791 on_position_event_called: false,
2792 on_position_opened_called: false,
2793 on_position_changed_called: false,
2794 on_position_closed_called: false,
2795 }
2796 }
2797 }
2798
2799 impl DataActor for TestStrategy {
2800 fn on_start(&mut self) -> anyhow::Result<()> {
2801 self.start_calls += 1;
2802 Ok(())
2803 }
2804 }
2805
2806 nautilus_strategy!(TestStrategy, {
2807 fn on_order_canceled(&mut self, _event: &OrderCanceled) {
2808 self.on_order_canceled_called = true;
2809 }
2810
2811 fn on_order_filled(&mut self, _event: &OrderFilled) {
2812 self.on_order_filled_called = true;
2813 self.order_event_timeline.borrow_mut().push("specific");
2814 }
2815
2816 fn on_order_fill_voided(&mut self, _event: &OrderFillVoided) {
2817 self.on_order_fill_voided_called = true;
2818 }
2819
2820 fn on_order_rejected(&mut self, _event: OrderRejected) {
2821 self.on_order_rejected_called = true;
2822 }
2823
2824 fn on_order_event(&mut self, _event: OrderEventAny) {
2825 self.on_order_event_called = true;
2826 self.order_event_timeline.borrow_mut().push("aggregate");
2827 }
2828
2829 fn on_order_accepted(&mut self, _event: OrderAccepted) {
2830 self.on_order_accepted_called = true;
2831 }
2832
2833 fn on_order_expired(&mut self, _event: OrderExpired) {
2834 self.on_order_expired_called = true;
2835 }
2836
2837 fn on_position_opened(&mut self, _event: PositionOpened) {
2838 self.on_position_opened_called = true;
2839 }
2840
2841 fn on_position_event(&mut self, _event: PositionEvent) {
2842 self.on_position_event_called = true;
2843 }
2844
2845 fn on_position_changed(&mut self, _event: PositionChanged) {
2846 self.on_position_changed_called = true;
2847 }
2848
2849 fn on_position_closed(&mut self, _event: PositionClosed) {
2850 self.on_position_closed_called = true;
2851 }
2852 });
2853
2854 fn create_test_strategy() -> TestStrategy {
2855 let config = StrategyConfig {
2856 strategy_id: Some(StrategyId::from("TEST-001")),
2857 order_id_tag: Some("001".to_string()),
2858 ..Default::default()
2859 };
2860 TestStrategy::new(config)
2861 }
2862
2863 fn create_gtd_managed_strategy() -> TestStrategy {
2864 TestStrategy::new(StrategyConfig {
2865 strategy_id: Some(StrategyId::from("TEST-001")),
2866 order_id_tag: Some("001".to_string()),
2867 manage_gtd_expiry: true,
2868 ..Default::default()
2869 })
2870 }
2871
2872 fn register_strategy(strategy: &mut TestStrategy) {
2873 let _clock = register_strategy_with_clock(strategy);
2874 }
2875
2876 fn register_strategy_with_clock(strategy: &mut TestStrategy) -> Rc<RefCell<VirtualClock>> {
2877 let trader_id = TraderId::from("TRADER-001");
2878 let clock = Rc::new(RefCell::new(VirtualClock::new()));
2879 let cache = Rc::new(RefCell::new(Cache::default()));
2880 let portfolio = Rc::new(RefCell::new(Portfolio::new(
2881 clock.clone(),
2882 cache.clone(),
2883 None,
2884 )));
2885
2886 strategy
2887 .core
2888 .register(trader_id, clock.clone(), cache, portfolio)
2889 .unwrap();
2890 strategy.initialize().unwrap();
2891 clock
2892 }
2893
2894 fn register_gtd_strategy(strategy: &mut TestStrategy) -> Rc<RefCell<VirtualClock>> {
2895 let clock = register_strategy_with_clock(strategy);
2896 clock
2897 .borrow_mut()
2898 .register_default_handler(TimeEventCallback::from(|_event: TimeEvent| {}));
2899 clock
2900 }
2901
2902 fn start_strategy(strategy: &mut TestStrategy) {
2903 strategy.start().unwrap();
2904 }
2905
2906 fn stop_strategy(strategy: &mut TestStrategy) {
2907 Component::stop(strategy).unwrap();
2908 }
2909
2910 fn make_filled(client_order_id: ClientOrderId) -> OrderEventAny {
2911 OrderEventAny::Filled(
2912 OrderFilledSpec::builder()
2913 .trader_id(TraderId::from("TRADER-001"))
2914 .strategy_id(StrategyId::from("TEST-001"))
2915 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2916 .client_order_id(client_order_id)
2917 .venue_order_id(VenueOrderId::test_default())
2918 .account_id(AccountId::from("ACC-001"))
2919 .trade_id(TradeId::test_default())
2920 .last_qty(Quantity::default())
2921 .last_px(Price::default())
2922 .currency(Currency::from("USD"))
2923 .liquidity_side(LiquiditySide::Taker)
2924 .event_id(UUID4::default())
2925 .build(),
2926 )
2927 }
2928
2929 fn make_fill_voided(client_order_id: ClientOrderId, is_reopened: bool) -> OrderEventAny {
2930 OrderEventAny::FillVoided(
2931 OrderFillVoidedSpec::builder()
2932 .trader_id(TraderId::from("TRADER-001"))
2933 .strategy_id(StrategyId::from("TEST-001"))
2934 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2935 .client_order_id(client_order_id)
2936 .venue_order_id(VenueOrderId::test_default())
2937 .account_id(AccountId::from("ACC-001"))
2938 .is_reopened(is_reopened)
2939 .build(),
2940 )
2941 }
2942
2943 fn make_terminal_fill_voided(client_order_id: ClientOrderId) -> OrderEventAny {
2944 make_fill_voided(client_order_id, false)
2945 }
2946
2947 fn make_canceled(client_order_id: ClientOrderId) -> OrderEventAny {
2948 OrderEventAny::Canceled(
2949 OrderCanceledSpec::builder()
2950 .trader_id(TraderId::from("TRADER-001"))
2951 .strategy_id(StrategyId::from("TEST-001"))
2952 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2953 .client_order_id(client_order_id)
2954 .account_id(AccountId::from("ACC-001"))
2955 .event_id(UUID4::default())
2956 .build(),
2957 )
2958 }
2959
2960 fn make_rejected(client_order_id: ClientOrderId) -> OrderEventAny {
2961 OrderEventAny::Rejected(
2962 OrderRejectedSpec::builder()
2963 .trader_id(TraderId::from("TRADER-001"))
2964 .strategy_id(StrategyId::from("TEST-001"))
2965 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2966 .client_order_id(client_order_id)
2967 .account_id(AccountId::from("ACC-001"))
2968 .reason("Test rejection".into())
2969 .event_id(UUID4::default())
2970 .build(),
2971 )
2972 }
2973
2974 fn make_cancel_denied(client_order_id: ClientOrderId) -> OrderEventAny {
2975 OrderEventAny::CancelDenied(
2976 OrderCancelDeniedSpec::builder()
2977 .trader_id(TraderId::from("TRADER-001"))
2978 .strategy_id(StrategyId::from("TEST-001"))
2979 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2980 .client_order_id(client_order_id)
2981 .reason("Test denial".into())
2982 .venue_order_id(VenueOrderId::from(client_order_id.as_str()))
2983 .account_id(AccountId::from("ACC-001"))
2984 .build(),
2985 )
2986 }
2987
2988 fn make_cancel_rejected(client_order_id: ClientOrderId) -> OrderEventAny {
2989 OrderEventAny::CancelRejected(
2990 OrderCancelRejectedSpec::builder()
2991 .trader_id(TraderId::from("TRADER-001"))
2992 .strategy_id(StrategyId::from("TEST-001"))
2993 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
2994 .client_order_id(client_order_id)
2995 .reason("Test rejection".into())
2996 .venue_order_id(VenueOrderId::from(client_order_id.as_str()))
2997 .account_id(AccountId::from("ACC-001"))
2998 .build(),
2999 )
3000 }
3001
3002 fn make_expired(client_order_id: ClientOrderId) -> OrderEventAny {
3003 OrderEventAny::Expired(
3004 OrderExpiredSpec::builder()
3005 .trader_id(TraderId::from("TRADER-001"))
3006 .strategy_id(StrategyId::from("TEST-001"))
3007 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
3008 .client_order_id(client_order_id)
3009 .account_id(AccountId::from("ACC-001"))
3010 .event_id(UUID4::default())
3011 .build(),
3012 )
3013 }
3014
3015 fn make_accepted(client_order_id: ClientOrderId) -> OrderEventAny {
3016 OrderEventAny::Accepted(
3017 OrderAcceptedSpec::builder()
3018 .trader_id(TraderId::from("TRADER-001"))
3019 .strategy_id(StrategyId::from("TEST-001"))
3020 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
3021 .client_order_id(client_order_id)
3022 .venue_order_id(VenueOrderId::test_default())
3023 .account_id(AccountId::from("ACC-001"))
3024 .event_id(UUID4::default())
3025 .build(),
3026 )
3027 }
3028
3029 fn make_accepted_market_order(client_order_id: &str) -> OrderAny {
3030 let mut order = OrderAny::Market(MarketOrder::new(
3031 TraderId::from("TRADER-001"),
3032 StrategyId::from("TEST-001"),
3033 InstrumentId::from("BTCUSDT.BINANCE"),
3034 ClientOrderId::from(client_order_id),
3035 OrderSide::Buy,
3036 Quantity::from(100_000),
3037 TimeInForce::Gtc,
3038 UUID4::new(),
3039 UnixNanos::default(),
3040 false,
3041 false,
3042 None,
3043 None,
3044 None,
3045 None,
3046 None,
3047 None,
3048 None,
3049 None,
3050 ));
3051 let account_id = AccountId::from("ACC-001");
3052 order
3053 .apply(TestOrderEventStubs::submitted(&order, account_id))
3054 .unwrap();
3055 order
3056 .apply(TestOrderEventStubs::accepted(
3057 &order,
3058 account_id,
3059 VenueOrderId::from(client_order_id),
3062 ))
3063 .unwrap();
3064 order
3065 }
3066
3067 fn make_accepted_limit_order(client_order_id: &str) -> OrderAny {
3068 let mut order = OrderAny::Limit(LimitOrder::new(
3069 TraderId::from("TRADER-001"),
3070 StrategyId::from("TEST-001"),
3071 InstrumentId::from("BTCUSDT.BINANCE"),
3072 ClientOrderId::from(client_order_id),
3073 OrderSide::Buy,
3074 Quantity::from("1.0"),
3075 Price::from("50000.0"),
3076 TimeInForce::Gtc,
3077 None,
3078 false,
3079 false,
3080 false,
3081 None,
3082 None,
3083 None,
3084 None,
3085 None,
3086 None,
3087 None,
3088 None,
3089 None,
3090 None,
3091 None,
3092 UUID4::new(),
3093 UnixNanos::default(),
3094 ));
3095 let account_id = AccountId::from("ACC-001");
3096 order
3097 .apply(TestOrderEventStubs::submitted(&order, account_id))
3098 .unwrap();
3099 order
3100 .apply(TestOrderEventStubs::accepted(
3101 &order,
3102 account_id,
3103 VenueOrderId::from(client_order_id),
3105 ))
3106 .unwrap();
3107 order
3108 }
3109
3110 fn make_submitted_gtd_limit_order(client_order_id: &str, expire_time: UnixNanos) -> OrderAny {
3111 let mut order = OrderTestBuilder::new(OrderType::Limit)
3112 .trader_id(TraderId::from("TRADER-001"))
3113 .strategy_id(StrategyId::from("TEST-001"))
3114 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
3115 .client_order_id(ClientOrderId::from(client_order_id))
3116 .side(OrderSide::Buy)
3117 .quantity(Quantity::from("1.0"))
3118 .price(Price::from("50000.0"))
3119 .time_in_force(TimeInForce::Gtd)
3120 .expire_time(expire_time)
3121 .build();
3122 let account_id = AccountId::from("ACC-001");
3123 order
3124 .apply(TestOrderEventStubs::submitted(&order, account_id))
3125 .unwrap();
3126 order
3127 }
3128
3129 fn make_accepted_gtd_limit_order(client_order_id: &str, expire_time: UnixNanos) -> OrderAny {
3130 let mut order = make_submitted_gtd_limit_order(client_order_id, expire_time);
3131 let account_id = AccountId::from("ACC-001");
3132 order
3133 .apply(TestOrderEventStubs::accepted(
3134 &order,
3135 account_id,
3136 VenueOrderId::from(client_order_id),
3137 ))
3138 .unwrap();
3139 order
3140 }
3141
3142 fn make_initialized_market_order(client_order_id: &str) -> OrderAny {
3143 OrderAny::Market(MarketOrder::new(
3144 TraderId::from("TRADER-001"),
3145 StrategyId::from("TEST-001"),
3146 InstrumentId::from("BTCUSDT.BINANCE"),
3147 ClientOrderId::from(client_order_id),
3148 OrderSide::Buy,
3149 Quantity::from(100_000),
3150 TimeInForce::Gtc,
3151 UUID4::new(),
3152 UnixNanos::default(),
3153 false,
3154 false,
3155 None,
3156 None,
3157 None,
3158 None,
3159 None,
3160 None,
3161 None,
3162 None,
3163 ))
3164 }
3165
3166 fn make_initialized_trailing_stop_order(
3167 order_type: OrderType,
3168 client_order_id: &str,
3169 ) -> OrderAny {
3170 let mut builder = OrderTestBuilder::new(order_type);
3171 builder
3172 .trader_id(TraderId::from("TRADER-001"))
3173 .strategy_id(StrategyId::from("TEST-001"))
3174 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
3175 .client_order_id(ClientOrderId::from(client_order_id))
3176 .side(OrderSide::Sell)
3177 .quantity(Quantity::from(100_000))
3178 .trigger_price(Price::from("49000.0"))
3179 .trailing_offset(Decimal::from(100))
3180 .trailing_offset_type(TrailingOffsetType::Price);
3181
3182 if order_type == OrderType::TrailingStopLimit {
3183 builder
3184 .price(Price::from("48900.0"))
3185 .limit_offset(Decimal::from(100));
3186 }
3187
3188 builder.build()
3189 }
3190
3191 fn make_initialized_algorithm_order(client_order_id: &str) -> OrderAny {
3192 OrderAny::Market(MarketOrder::new(
3193 TraderId::from("TRADER-001"),
3194 StrategyId::from("TEST-001"),
3195 InstrumentId::from("BTCUSDT.BINANCE"),
3196 ClientOrderId::from(client_order_id),
3197 OrderSide::Buy,
3198 Quantity::from(100_000),
3199 TimeInForce::Gtc,
3200 UUID4::new(),
3201 UnixNanos::default(),
3202 false,
3203 false,
3204 None,
3205 None,
3206 None,
3207 None,
3208 Some(ExecAlgorithmId::from("TWAP")),
3209 None,
3210 Some(ClientOrderId::from(client_order_id)),
3211 None,
3212 ))
3213 }
3214
3215 fn add_order_to_cache(strategy: &TestStrategy, order: &OrderAny) {
3216 let cache_rc = strategy.core.cache_rc();
3217 let mut cache = cache_rc.borrow_mut();
3218 cache.add_order(order.clone(), None, None, true).unwrap();
3219 }
3220
3221 fn make_submit_command(order: &OrderAny) -> SubmitOrder {
3222 SubmitOrder::new(
3223 order.trader_id(),
3224 None,
3225 order.strategy_id(),
3226 order.instrument_id(),
3227 order.client_order_id(),
3228 order.init_event().clone(),
3229 order.exec_algorithm_id(),
3230 None,
3231 None,
3232 UUID4::new(),
3233 UnixNanos::default(),
3234 None, )
3236 }
3237
3238 fn add_order_to_cache_and_own_book(strategy: &TestStrategy, order: &OrderAny) {
3239 let cache_rc = strategy.core.cache_rc();
3240 let mut cache = cache_rc.borrow_mut();
3241 cache.add_order(order.clone(), None, None, true).unwrap();
3242 cache
3243 .add_own_order_book(OwnOrderBook::new(order.instrument_id()))
3244 .unwrap();
3245 cache.update_own_order_book(order);
3246 }
3247
3248 fn make_position_opened() -> PositionEvent {
3249 PositionEvent::PositionOpened(PositionOpened {
3250 trader_id: TraderId::from("TRADER-001"),
3251 strategy_id: StrategyId::from("TEST-001"),
3252 instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
3253 position_id: PositionId::test_default(),
3254 account_id: AccountId::from("ACC-001"),
3255 opening_order_id: ClientOrderId::from("O-001"),
3256 entry: OrderSide::Buy,
3257 side: PositionSide::Long,
3258 signed_qty: 1.into(),
3259 quantity: Quantity::default(),
3260 last_qty: Quantity::default(),
3261 last_px: Price::default(),
3262 currency: Currency::from("USD"),
3263 avg_px_open: 0.into(),
3264 realized_pnl: None,
3265 event_id: UUID4::default(),
3266 ts_event: UnixNanos::default(),
3267 ts_init: UnixNanos::default(),
3268 })
3269 }
3270
3271 fn make_position_changed() -> PositionEvent {
3272 let currency = Currency::from("USD");
3273 PositionEvent::PositionChanged(PositionChanged {
3274 trader_id: TraderId::from("TRADER-001"),
3275 strategy_id: StrategyId::from("TEST-001"),
3276 instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
3277 position_id: PositionId::test_default(),
3278 account_id: AccountId::from("ACC-001"),
3279 opening_order_id: ClientOrderId::from("O-001"),
3280 entry: OrderSide::Buy,
3281 side: PositionSide::Long,
3282 signed_qty: 2.into(),
3283 quantity: Quantity::default(),
3284 peak_quantity: Quantity::default(),
3285 last_qty: Quantity::default(),
3286 last_px: Price::default(),
3287 currency,
3288 avg_px_open: 0.into(),
3289 avg_px_close: None,
3290 realized_return: 0.0,
3291 realized_pnl: None,
3292 unrealized_pnl: Money::zero(currency),
3293 event_id: UUID4::default(),
3294 ts_opened: UnixNanos::default(),
3295 ts_event: UnixNanos::default(),
3296 ts_init: UnixNanos::default(),
3297 })
3298 }
3299
3300 fn make_position_closed() -> PositionEvent {
3301 let currency = Currency::from("USD");
3302 PositionEvent::PositionClosed(PositionClosed {
3303 trader_id: TraderId::from("TRADER-001"),
3304 strategy_id: StrategyId::from("TEST-001"),
3305 instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
3306 position_id: PositionId::test_default(),
3307 account_id: AccountId::from("ACC-001"),
3308 opening_order_id: ClientOrderId::from("O-001"),
3309 closing_order_id: Some(ClientOrderId::from("O-002")),
3310 entry: OrderSide::Buy,
3311 side: PositionSide::Flat,
3312 signed_qty: 0.into(),
3313 quantity: Quantity::default(),
3314 peak_quantity: Quantity::default(),
3315 last_qty: Quantity::default(),
3316 last_px: Price::default(),
3317 currency,
3318 avg_px_open: 0.into(),
3319 avg_px_close: None,
3320 realized_return: 0.0,
3321 realized_pnl: None,
3322 unrealized_pnl: Money::zero(currency),
3323 duration: DurationNanos::default(),
3324 event_id: UUID4::default(),
3325 ts_opened: UnixNanos::default(),
3326 ts_closed: None,
3327 ts_event: UnixNanos::default(),
3328 ts_init: UnixNanos::default(),
3329 })
3330 }
3331
3332 fn make_position_adjusted() -> PositionEvent {
3333 PositionEvent::PositionAdjusted(PositionAdjusted {
3334 trader_id: TraderId::from("TRADER-001"),
3335 strategy_id: StrategyId::from("TEST-001"),
3336 instrument_id: InstrumentId::from("BTCUSDT.BINANCE"),
3337 position_id: PositionId::test_default(),
3338 account_id: AccountId::from("ACC-001"),
3339 adjustment_type: PositionAdjustmentType::Funding,
3340 quantity_change: None,
3341 pnl_change: None,
3342 reason: None,
3343 event_id: UUID4::default(),
3344 ts_event: UnixNanos::default(),
3345 ts_init: UnixNanos::default(),
3346 })
3347 }
3348
3349 #[rstest]
3350 fn test_strategy_creation() {
3351 let strategy = create_test_strategy();
3352 assert_eq!(strategy.strategy_id(), Some(StrategyId::from("TEST-001")));
3353 assert!(!strategy.on_order_rejected_called);
3354 assert!(!strategy.on_position_opened_called);
3355 }
3356
3357 #[rstest]
3358 fn test_strategy_registration() {
3359 let mut strategy = create_test_strategy();
3360 register_strategy(&mut strategy);
3361
3362 assert!(strategy.is_registered());
3363 let _ = strategy.order().generate_client_order_id();
3364 let _ = strategy.portfolio().is_initialized();
3365 }
3366
3367 #[rstest]
3368 fn test_set_external_order_instrument_ids_replaces_claims_atomically() {
3369 let mut strategy = create_test_strategy();
3370 register_strategy(&mut strategy);
3371 let cache = strategy.core.cache_rc();
3372 let strategy_id = StrategyId::from("TEST-001");
3373 let other_strategy_id = StrategyId::from("OTHER-001");
3374 let audusd = InstrumentId::from("AUDUSD.SIM");
3375 let eurusd = InstrumentId::from("EURUSD.SIM");
3376 let gbpusd = InstrumentId::from("GBPUSD.SIM");
3377
3378 strategy
3379 .set_external_order_instrument_ids(vec![audusd, eurusd])
3380 .unwrap();
3381 cache
3382 .borrow_mut()
3383 .set_external_order_claims(other_strategy_id, &[gbpusd])
3384 .unwrap();
3385
3386 let result = strategy.set_external_order_instrument_ids(vec![eurusd, gbpusd]);
3387
3388 assert_eq!(
3389 result.unwrap_err().to_string(),
3390 "External order claim for GBPUSD.SIM already exists for OTHER-001"
3391 );
3392 assert_eq!(
3393 cache.borrow().external_order_claim(&audusd),
3394 Some(strategy_id)
3395 );
3396 assert_eq!(
3397 cache.borrow().external_order_claim(&eurusd),
3398 Some(strategy_id)
3399 );
3400 assert_eq!(
3401 cache.borrow().external_order_claim(&gbpusd),
3402 Some(other_strategy_id)
3403 );
3404 assert_eq!(
3405 strategy.core.config.external_order_instrument_ids,
3406 Some(vec![audusd, eurusd])
3407 );
3408
3409 strategy
3410 .set_external_order_instrument_ids(vec![eurusd])
3411 .unwrap();
3412
3413 assert_eq!(cache.borrow().external_order_claim(&audusd), None);
3414 assert_eq!(
3415 cache.borrow().external_order_claim(&eurusd),
3416 Some(strategy_id)
3417 );
3418 assert_eq!(
3419 cache.borrow().external_order_claim(&gbpusd),
3420 Some(other_strategy_id)
3421 );
3422 assert_eq!(
3423 strategy.core.config.external_order_instrument_ids,
3424 Some(vec![eurusd])
3425 );
3426 }
3427
3428 #[rstest]
3429 fn test_set_external_order_instrument_ids_rejects_unregistered_strategy() {
3430 let mut strategy = create_test_strategy();
3431
3432 let error = strategy
3433 .set_external_order_instrument_ids(vec![InstrumentId::from("AUDUSD.SIM")])
3434 .unwrap_err();
3435
3436 assert_eq!(
3437 error.to_string(),
3438 "Strategy TEST-001 is not registered with a trader"
3439 );
3440 assert!(strategy.core.config.external_order_instrument_ids.is_none());
3441 }
3442
3443 #[rstest]
3444 fn test_strategy_native_methods_are_available_on_strategy_type() {
3445 let mut strategy = create_test_strategy();
3446 register_strategy(&mut strategy);
3447
3448 drop(strategy.order_factory());
3449
3450 assert!(Rc::ptr_eq(
3451 &strategy.order_factory_rc(),
3452 strategy.core.order_factory.as_ref().unwrap()
3453 ));
3454 assert!(Rc::ptr_eq(
3455 &strategy.portfolio_rc(),
3456 strategy.core.portfolio.as_ref().unwrap()
3457 ));
3458 }
3459
3460 #[rstest]
3461 #[case(false, false, false, Some(log::Level::Warn))]
3462 #[case(false, false, true, Some(log::Level::Warn))]
3463 #[case(false, true, false, Some(log::Level::Warn))]
3464 #[case(false, true, true, Some(log::Level::Warn))]
3465 #[case(true, false, false, None)]
3466 #[case(true, false, true, Some(log::Level::Info))]
3467 #[case(true, true, false, Some(log::Level::Warn))]
3468 #[case(true, true, true, Some(log::Level::Warn))]
3469 fn test_handle_order_event_logs_and_dispatches_rejection(
3470 #[case] due_post_only: bool,
3471 #[case] log_rejected_due_post_only_as_warning: bool,
3472 #[case] log_events: bool,
3473 #[case] expected_level: Option<log::Level>,
3474 ) {
3475 static LOG_CAPTURE: OrderEventLogCapture = OrderEventLogCapture(Mutex::new(Vec::new()));
3476
3477 let mut strategy = create_test_strategy();
3478 strategy.core.config.log_rejected_due_post_only_as_warning =
3479 log_rejected_due_post_only_as_warning;
3480 strategy.core.actor.config.log_events = log_events;
3481 register_strategy(&mut strategy);
3482 start_strategy(&mut strategy);
3483
3484 let event = OrderEventAny::Rejected(
3485 OrderRejectedSpec::builder()
3486 .strategy_id(StrategyId::from("TEST-001"))
3487 .client_order_id(ClientOrderId::from("O-001"))
3488 .due_post_only(due_post_only)
3489 .build(),
3490 );
3491
3492 log::set_logger(&LOG_CAPTURE).unwrap();
3493 log::set_max_level(log::LevelFilter::Info);
3494 let expected_records: Vec<_> = expected_level
3495 .map(|level| (level, format!("TEST-001 {RECV}{EVT} {event}")))
3496 .into_iter()
3497 .collect();
3498
3499 strategy.handle_order_event(event);
3500
3501 assert_eq!(*LOG_CAPTURE.0.lock().unwrap(), expected_records);
3502 assert!(strategy.on_order_rejected_called);
3503 assert!(strategy.on_order_event_called);
3504 }
3505
3506 struct OrderEventLogCapture(Mutex<Vec<(log::Level, String)>>);
3507
3508 impl log::Log for OrderEventLogCapture {
3509 fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
3510 metadata.target() == "nautilus_trading::strategy"
3511 }
3512
3513 fn log(&self, record: &log::Record<'_>) {
3514 if self.enabled(record.metadata()) {
3515 self.0
3516 .lock()
3517 .unwrap()
3518 .push((record.level(), record.args().to_string()));
3519 }
3520 }
3521
3522 fn flush(&self) {}
3523 }
3524
3525 #[rstest]
3526 fn test_dispatch_manager_actions_routes_every_action() {
3527 let mut strategy = create_test_strategy();
3528 register_strategy(&mut strategy);
3529 let strategy_id = StrategyId::from("TEST-001");
3530 let instrument_id = InstrumentId::from("BTCUSDT.BINANCE");
3531 let initialized_order = make_initialized_market_order("O-MANAGER-INIT");
3532 let emulator_order = OrderTestBuilder::new(OrderType::StopMarket)
3533 .trader_id(TraderId::from("TRADER-001"))
3534 .strategy_id(strategy_id)
3535 .instrument_id(instrument_id)
3536 .client_order_id(ClientOrderId::from("O-MANAGER-EMULATOR"))
3537 .side(OrderSide::Buy)
3538 .trigger_price(Price::from("51000.0"))
3539 .quantity(Quantity::from(100_000))
3540 .emulation_trigger(TriggerType::BidAsk)
3541 .build();
3542 let risk_order = make_initialized_market_order("O-MANAGER-RISK");
3543 let algorithm_order = make_initialized_algorithm_order("O-MANAGER-ALGORITHM");
3544 let cancel_order = OrderTestBuilder::new(OrderType::Limit)
3545 .trader_id(TraderId::from("TRADER-001"))
3546 .strategy_id(strategy_id)
3547 .instrument_id(instrument_id)
3548 .client_order_id(ClientOrderId::from("O-MANAGER-CANCEL"))
3549 .side(OrderSide::Buy)
3550 .price(Price::from("50000.0"))
3551 .quantity(Quantity::from(100_000))
3552 .submit(true)
3553 .build();
3554 let missing_cancel_order = OrderTestBuilder::new(OrderType::Limit)
3555 .trader_id(TraderId::from("TRADER-001"))
3556 .strategy_id(strategy_id)
3557 .instrument_id(instrument_id)
3558 .client_order_id(ClientOrderId::from("O-MANAGER-MISSING-CANCEL"))
3559 .side(OrderSide::Buy)
3560 .price(Price::from("50000.0"))
3561 .quantity(Quantity::from(100_000))
3562 .submit(true)
3563 .build();
3564 let modify_order = OrderTestBuilder::new(OrderType::Limit)
3565 .trader_id(TraderId::from("TRADER-001"))
3566 .strategy_id(strategy_id)
3567 .instrument_id(instrument_id)
3568 .client_order_id(ClientOrderId::from("O-MANAGER-MODIFY"))
3569 .side(OrderSide::Buy)
3570 .price(Price::from("50000.0"))
3571 .quantity(Quantity::from(100_000))
3572 .submit(true)
3573 .build();
3574 add_order_to_cache(&strategy, &cancel_order);
3575 add_order_to_cache(&strategy, &modify_order);
3576
3577 let (order_handler, order_events) =
3578 get_typed_message_saving_handler(Some(Ustr::from("manager-action-order-events")));
3579 let order_topic = format!("events.order.{strategy_id}");
3580 msgbus::subscribe_order_events(order_topic.clone().into(), order_handler.clone(), None);
3581 let (emulator_handler, emulator_messages): (
3582 _,
3583 TypedIntoMessageSavingHandler<TradingCommand>,
3584 ) = get_typed_into_message_saving_handler(Some(Ustr::from("OrderEmulator.execute")));
3585 msgbus::register_trading_command_endpoint(
3586 MessagingSwitchboard::order_emulator_execute(),
3587 emulator_handler,
3588 );
3589 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
3590 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
3591 msgbus::register_trading_command_endpoint(
3592 MessagingSwitchboard::risk_engine_queue_execute(),
3593 risk_handler,
3594 );
3595 let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
3596 get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
3597 msgbus::register_trading_command_endpoint(
3598 MessagingSwitchboard::exec_engine_queue_execute(),
3599 exec_handler,
3600 );
3601 let (algorithm_handler, algorithm_messages) =
3602 get_any_saving_handler::<TradingCommand>(Some(Ustr::from("TWAP.execute")));
3603 msgbus::register_any("TWAP.execute".into(), algorithm_handler);
3604
3605 strategy.dispatch_manager_actions(vec![
3606 OrderManagerAction::CancelLocal(missing_cancel_order),
3607 OrderManagerAction::PublishInitialized(OrderEventAny::Initialized(
3608 initialized_order.init_event().clone(),
3609 )),
3610 OrderManagerAction::SubmitToEmulator(make_submit_command(&emulator_order)),
3611 OrderManagerAction::SubmitToRisk(make_submit_command(&risk_order)),
3612 OrderManagerAction::SubmitToAlgorithm {
3613 command: make_submit_command(&algorithm_order),
3614 exec_algorithm_id: ExecAlgorithmId::from("TWAP"),
3615 },
3616 OrderManagerAction::CancelLocal(cancel_order.clone()),
3617 OrderManagerAction::ModifyLocalQuantity {
3618 order: modify_order.clone(),
3619 quantity: Quantity::from(50_000),
3620 },
3621 OrderManagerAction::ModifyLocalQuantity {
3622 order: modify_order.clone(),
3623 quantity: Quantity::from(100_000),
3624 },
3625 ]);
3626 msgbus::unsubscribe_order_events(order_topic.into(), &order_handler);
3627
3628 let order_events = order_events.get_messages();
3629 assert_eq!(order_events.len(), 4);
3630 assert!(matches!(
3631 &order_events[0],
3632 OrderEventAny::Initialized(event)
3633 if event.client_order_id == initialized_order.client_order_id()
3634 ));
3635 assert!(matches!(
3636 &order_events[1],
3637 OrderEventAny::PendingCancel(event)
3638 if event.client_order_id == cancel_order.client_order_id()
3639 ));
3640 assert!(matches!(
3641 &order_events[2],
3642 OrderEventAny::PendingUpdate(event)
3643 if event.client_order_id == modify_order.client_order_id()
3644 ));
3645 assert!(matches!(
3646 &order_events[3],
3647 OrderEventAny::PendingUpdate(event)
3648 if event.client_order_id == modify_order.client_order_id()
3649 ));
3650
3651 let pending_ids: Vec<_> = order_events
3652 .iter()
3653 .filter_map(|event| match event {
3654 OrderEventAny::PendingUpdate(event) => event.causation_id,
3655 _ => None,
3656 })
3657 .collect();
3658
3659 let dispatched_ids: Vec<_> = risk_messages
3660 .get_messages()
3661 .iter()
3662 .filter_map(|command| match command {
3663 TradingCommand::ModifyOrder(command) => Some(command.command_id),
3664 _ => None,
3665 })
3666 .collect();
3667
3668 assert_eq!(pending_ids, dispatched_ids);
3669 assert!(matches!(
3670 emulator_messages.get_messages().as_slice(),
3671 [TradingCommand::SubmitOrder(command)]
3672 if command.client_order_id == emulator_order.client_order_id()
3673 ));
3674 assert!(matches!(
3675 risk_messages.get_messages().as_slice(),
3676 [
3677 TradingCommand::SubmitOrder(submit),
3678 TradingCommand::ModifyOrder(first_modify),
3679 TradingCommand::ModifyOrder(second_modify),
3680 ]
3681
3682 if submit.client_order_id == risk_order.client_order_id()
3683 && first_modify.client_order_id == modify_order.client_order_id()
3684 && first_modify.quantity == Some(Quantity::from(50_000))
3685 && second_modify.client_order_id == modify_order.client_order_id()
3686 && second_modify.quantity == Some(Quantity::from(100_000))
3687 ));
3688 assert!(matches!(
3689 algorithm_messages.get_messages().as_slice(),
3690 [TradingCommand::SubmitOrder(command)]
3691 if command.client_order_id == algorithm_order.client_order_id()
3692 ));
3693 assert!(matches!(
3694 exec_messages.get_messages().as_slice(),
3695 [TradingCommand::CancelOrder(command)]
3696 if command.client_order_id == cancel_order.client_order_id()
3697 ));
3698 }
3699
3700 #[rstest]
3701 #[case::disabled(false)]
3702 #[case::enabled(true)]
3703 fn test_contingent_manager_dispatch_precedes_user_handlers_and_is_idempotent(
3704 #[case] manage_contingent_orders: bool,
3705 ) {
3706 let mut strategy = TestStrategy::new(StrategyConfig {
3707 strategy_id: Some(StrategyId::from("TEST-001")),
3708 order_id_tag: Some("001".to_string()),
3709 manage_contingent_orders,
3710 ..Default::default()
3711 });
3712 register_strategy(&mut strategy);
3713 start_strategy(&mut strategy);
3714 let timeline = strategy.order_event_timeline.clone();
3715 let endpoint_timeline = timeline.clone();
3716 let exec_handler = TypedIntoHandler::from(move |command: TradingCommand| {
3717 assert!(matches!(command, TradingCommand::CancelOrder(_)));
3718 endpoint_timeline.borrow_mut().push("endpoint");
3719 });
3720 msgbus::register_trading_command_endpoint(
3721 MessagingSwitchboard::exec_engine_queue_execute(),
3722 exec_handler,
3723 );
3724 let instrument_id = InstrumentId::from("BTCUSDT.BINANCE");
3725 let parent_id = ClientOrderId::from("O-CONTINGENT-PARENT");
3726 let sibling_id = ClientOrderId::from("O-CONTINGENT-SIBLING");
3727 let parent = OrderTestBuilder::new(OrderType::Limit)
3728 .trader_id(TraderId::from("TRADER-001"))
3729 .strategy_id(StrategyId::from("TEST-001"))
3730 .instrument_id(instrument_id)
3731 .client_order_id(parent_id)
3732 .side(OrderSide::Buy)
3733 .price(Price::from("50000.0"))
3734 .quantity(Quantity::from(100_000))
3735 .contingency_type(ContingencyType::Oco)
3736 .linked_order_ids(vec![parent_id, sibling_id])
3737 .submit(true)
3738 .build();
3739 let sibling = OrderTestBuilder::new(OrderType::Limit)
3740 .trader_id(TraderId::from("TRADER-001"))
3741 .strategy_id(StrategyId::from("TEST-001"))
3742 .instrument_id(instrument_id)
3743 .client_order_id(sibling_id)
3744 .side(OrderSide::Sell)
3745 .price(Price::from("51000.0"))
3746 .quantity(Quantity::from(100_000))
3747 .submit(true)
3748 .build();
3749 add_order_to_cache(&strategy, &parent);
3750 add_order_to_cache(&strategy, &sibling);
3751 let event = OrderEventAny::Filled(
3752 OrderFilledSpec::builder()
3753 .trader_id(parent.trader_id())
3754 .strategy_id(parent.strategy_id())
3755 .instrument_id(parent.instrument_id())
3756 .client_order_id(parent.client_order_id())
3757 .venue_order_id(VenueOrderId::from("V-CONTINGENT-PARENT"))
3758 .account_id(AccountId::from("ACCOUNT-001"))
3759 .trade_id(TradeId::from("T-CONTINGENT-PARENT"))
3760 .order_side(parent.order_side())
3761 .order_type(parent.order_type())
3762 .last_qty(parent.quantity())
3763 .last_px(Price::from("50000.0"))
3764 .liquidity_side(LiquiditySide::Taker)
3765 .build(),
3766 );
3767 strategy
3768 .core
3769 .cache_rc()
3770 .borrow_mut()
3771 .update_order(&event)
3772 .unwrap();
3773
3774 strategy.handle_order_event(event.clone());
3775 strategy.handle_order_event(event);
3776
3777 let timeline = timeline.borrow();
3778 let sibling_status = strategy
3779 .core
3780 .cache_ref()
3781 .order(&sibling_id)
3782 .unwrap()
3783 .status();
3784
3785 if manage_contingent_orders {
3786 assert_eq!(
3787 timeline.as_slice(),
3788 ["endpoint", "specific", "aggregate", "specific", "aggregate"]
3789 );
3790 assert_eq!(sibling_status, OrderStatus::PendingCancel);
3791 } else {
3792 assert_eq!(
3793 timeline.as_slice(),
3794 ["specific", "aggregate", "specific", "aggregate"]
3795 );
3796 assert_eq!(sibling_status, OrderStatus::Submitted);
3797 }
3798 }
3799
3800 #[rstest]
3801 fn test_handle_order_fill_voided_dispatches_to_specific_handler() {
3802 let mut strategy = create_test_strategy();
3803 register_strategy(&mut strategy);
3804 start_strategy(&mut strategy);
3805
3806 strategy.handle_order_event(make_fill_voided(ClientOrderId::from("O-001"), false));
3807
3808 assert!(strategy.on_order_fill_voided_called);
3809 assert!(strategy.on_order_event_called);
3810 }
3811
3812 #[rstest]
3813 #[case::opened(make_position_opened())]
3814 #[case::changed(make_position_changed())]
3815 #[case::closed(make_position_closed())]
3816 fn test_handle_position_event_dispatches_to_handler(#[case] event: PositionEvent) {
3817 let mut strategy = create_test_strategy();
3818 register_strategy(&mut strategy);
3819 start_strategy(&mut strategy);
3820
3821 let expected_opened = matches!(event, PositionEvent::PositionOpened(_));
3822 let expected_changed = matches!(event, PositionEvent::PositionChanged(_));
3823 let expected_closed = matches!(event, PositionEvent::PositionClosed(_));
3824
3825 strategy.handle_position_event(event);
3826
3827 assert_eq!(strategy.on_position_opened_called, expected_opened);
3828 assert_eq!(strategy.on_position_changed_called, expected_changed);
3829 assert_eq!(strategy.on_position_closed_called, expected_closed);
3830 assert!(strategy.on_position_event_called);
3831 }
3832
3833 #[rstest]
3834 fn test_handle_position_event_skips_dispatch_when_stopped() {
3835 let mut strategy = create_test_strategy();
3836 register_strategy(&mut strategy);
3837 start_strategy(&mut strategy);
3838 stop_strategy(&mut strategy);
3839 assert_eq!(strategy.state(), ComponentState::Stopped);
3840
3841 strategy.handle_position_event(make_position_opened());
3842
3843 assert!(!strategy.on_position_event_called);
3844 assert!(!strategy.on_position_opened_called);
3845 }
3846
3847 #[rstest]
3848 fn test_handle_position_event_skips_dispatch_for_adjusted() {
3849 let mut strategy = create_test_strategy();
3850 register_strategy(&mut strategy);
3851 start_strategy(&mut strategy);
3852
3853 strategy.handle_position_event(make_position_adjusted());
3854
3855 assert!(!strategy.on_position_event_called);
3856 assert!(!strategy.on_position_opened_called);
3857 assert!(!strategy.on_position_changed_called);
3858 assert!(!strategy.on_position_closed_called);
3859 }
3860
3861 #[rstest]
3862 fn test_strategy_default_handlers_do_not_panic() {
3863 let mut strategy = create_test_strategy();
3864
3865 strategy.on_order_initialized(OrderInitialized::default());
3866 strategy.on_order_event(OrderEventAny::Accepted(OrderAccepted::default()));
3867 strategy.on_order_denied(OrderDenied::default());
3868 strategy.on_order_emulated(OrderEmulated::default());
3869 strategy.on_order_released(OrderReleased::default());
3870 strategy.on_order_submitted(OrderSubmitted::default());
3871 strategy.on_order_rejected(OrderRejected::default());
3872 strategy.on_order_canceled(&OrderCanceled::default());
3873 strategy.on_order_expired(OrderExpired::default());
3874 strategy.on_order_triggered(OrderTriggered::default());
3875 strategy.on_order_pending_update(OrderPendingUpdate::default());
3876 strategy.on_order_pending_cancel(OrderPendingCancel::default());
3877 strategy.on_order_modify_rejected(OrderModifyRejected::default());
3878 strategy.on_order_cancel_rejected(OrderCancelRejected::default());
3879 strategy.on_order_updated(OrderUpdated::default());
3880 strategy.on_order_filled(&OrderFilledSpec::builder().build());
3881 strategy.on_order_fill_voided(&OrderFillVoidedSpec::builder().build());
3882 strategy.on_position_event(make_position_opened());
3883 }
3884
3885 #[rstest]
3886 #[case::submitted(OrderStatus::Submitted, false)]
3887 #[case::accepted(OrderStatus::Accepted, false)]
3888 #[case::accepted_exiting(OrderStatus::Accepted, true)]
3889 #[case::canceled(OrderStatus::Canceled, false)]
3890 fn test_submit_order_rejects_retained_initialized_order_with_non_local_cached_order(
3891 #[case] status: OrderStatus,
3892 #[case] exiting: bool,
3893 ) {
3894 let mut strategy = create_test_strategy();
3895 register_strategy(&mut strategy);
3896 let order = make_initialized_market_order("O-RETAINED-ACCEPTED");
3897 let client_order_id = order.client_order_id();
3898 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
3899 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
3900 msgbus::register_trading_command_endpoint(
3901 MessagingSwitchboard::risk_engine_queue_execute(),
3902 risk_handler,
3903 );
3904 let topic = format!("events.order.{}", order.strategy_id());
3905 let (event_handler, event_messages): (_, TypedMessageSavingHandler<OrderEventAny>) =
3906 get_typed_message_saving_handler(Some(Ustr::from("events.order.retained")));
3907 msgbus::subscribe_order_events(topic.clone().into(), event_handler.clone(), None);
3908
3909 strategy
3910 .submit_order(order.clone(), None, None, None)
3911 .unwrap();
3912 let cache_rc = strategy.core.cache_rc();
3913 let submitted = TestOrderEventStubs::submitted(&order, AccountId::from("ACC-001"));
3914 cache_rc.borrow_mut().update_order(&submitted).unwrap();
3915 if status != OrderStatus::Submitted {
3916 cache_rc
3917 .borrow_mut()
3918 .update_order(&make_accepted(client_order_id))
3919 .unwrap();
3920 }
3921
3922 if status == OrderStatus::Canceled {
3923 cache_rc
3924 .borrow_mut()
3925 .update_order(&make_canceled(client_order_id))
3926 .unwrap();
3927 }
3928
3929 let before = cache_rc.borrow().order(&client_order_id).unwrap().clone();
3930 let snapshot = serde_json::to_value(&before).unwrap();
3931
3932 strategy.core.is_exiting = exiting;
3933 let result = strategy.submit_order(order.clone(), None, None, None);
3934
3935 msgbus::unsubscribe_order_events(topic.into(), &event_handler);
3936 let cache = cache_rc.borrow();
3937 let cached = cache.order(&client_order_id).unwrap();
3938 assert_eq!(
3939 result.unwrap_err().to_string(),
3940 format!("Order denied: duplicate {client_order_id}")
3941 );
3942 assert_eq!(risk_messages.get_messages().len(), 1);
3943 assert_eq!(
3944 event_messages.get_messages(),
3945 vec![OrderEventAny::Initialized(order.init_event().clone())]
3946 );
3947 assert_eq!(cached.status(), status);
3948 assert_eq!(cached.events(), before.events());
3949 assert_eq!(serde_json::to_value(&*cached).unwrap(), snapshot);
3950 }
3951
3952 #[rstest]
3953 #[case::initialized(OrderStatus::Initialized)]
3954 #[case::emulated(OrderStatus::Emulated)]
3955 #[case::released(OrderStatus::Released)]
3956 fn test_submit_order_replaces_active_local_cached_order(#[case] status: OrderStatus) {
3957 let mut strategy = create_test_strategy();
3958 register_strategy(&mut strategy);
3959 let client_order_id = ClientOrderId::from("O-LOCAL-REPLACE");
3960 let mut local = OrderTestBuilder::new(OrderType::StopMarket)
3961 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
3962 .strategy_id(StrategyId::from("TEST-001"))
3963 .client_order_id(client_order_id)
3964 .side(OrderSide::Sell)
3965 .quantity(Quantity::from("1.25"))
3966 .trigger_price(Price::from("51000.0"))
3967 .emulation_trigger(TriggerType::BidAsk)
3968 .build();
3969
3970 if status == OrderStatus::Emulated {
3971 let event = OrderEmulatedSpec::builder()
3972 .trader_id(local.trader_id())
3973 .strategy_id(local.strategy_id())
3974 .instrument_id(local.instrument_id())
3975 .client_order_id(client_order_id)
3976 .build();
3977 local.apply(OrderEventAny::Emulated(event)).unwrap();
3978 } else if status == OrderStatus::Released {
3979 let event = OrderReleasedSpec::builder()
3980 .trader_id(local.trader_id())
3981 .strategy_id(local.strategy_id())
3982 .instrument_id(local.instrument_id())
3983 .client_order_id(client_order_id)
3984 .released_price(Price::from("51001.0"))
3985 .build();
3986 local.apply(OrderEventAny::Released(event)).unwrap();
3987 }
3988
3989 let cache_rc = strategy.core.cache_rc();
3990 cache_rc
3991 .borrow_mut()
3992 .add_order(local.clone(), None, None, false)
3993 .unwrap();
3994 let order = OrderTestBuilder::new(OrderType::Market)
3995 .instrument_id(local.instrument_id())
3996 .strategy_id(local.strategy_id())
3997 .client_order_id(client_order_id)
3998 .side(OrderSide::Buy)
3999 .quantity(Quantity::from("2.50"))
4000 .build();
4001 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4002 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4003 msgbus::register_trading_command_endpoint(
4004 MessagingSwitchboard::risk_engine_queue_execute(),
4005 risk_handler,
4006 );
4007
4008 strategy
4009 .submit_order(order.clone(), None, None, None)
4010 .unwrap();
4011
4012 let cache = cache_rc.borrow();
4013 let cached = cache.order(&client_order_id).unwrap();
4014 assert_eq!(local.status(), status);
4015 assert_eq!(cached.status(), OrderStatus::Initialized);
4016 assert_eq!(cached.events(), order.events());
4017 assert_eq!(
4018 serde_json::to_value(&*cached).unwrap(),
4019 serde_json::to_value(&order).unwrap()
4020 );
4021 let messages = risk_messages.get_messages();
4022 assert_eq!(messages.len(), 1);
4023
4024 let TradingCommand::SubmitOrder(command) = &messages[0] else {
4025 panic!("Expected SubmitOrder");
4026 };
4027
4028 assert_eq!(command.order_init, *order.init_event());
4029 }
4030
4031 #[rstest]
4032 fn test_submit_order_publishes_order_initialized_after_cache_insert_before_send() {
4033 let mut strategy = create_test_strategy();
4034 register_strategy(&mut strategy);
4035
4036 let order = make_initialized_market_order("O-20250208-INIT-001");
4037 let client_order_id = order.client_order_id();
4038 let cache_rc = strategy.core.cache_rc();
4039 let old_instrument_id = InstrumentId::from("ETHUSDT.KRAKEN");
4040 let old_order = OrderTestBuilder::new(OrderType::Market)
4041 .instrument_id(old_instrument_id)
4042 .client_order_id(client_order_id)
4043 .strategy_id(order.strategy_id())
4044 .side(OrderSide::Buy)
4045 .quantity(Quantity::from(1))
4046 .build();
4047 cache_rc
4048 .borrow_mut()
4049 .add_order(old_order, None, None, false)
4050 .unwrap();
4051 let new_instrument_id = order.instrument_id();
4052 let timeline = Rc::new(RefCell::new(Vec::new()));
4053 let event_messages = Rc::new(RefCell::new(Vec::new()));
4054
4055 let event_handler = {
4056 let event_messages = event_messages.clone();
4057 let timeline = timeline.clone();
4058 TypedHandler::from_with_id("events.order.initialized", move |event: &OrderEventAny| {
4059 assert!(cache_rc.borrow().order_exists(&client_order_id));
4060 assert!(
4061 cache_rc
4062 .borrow()
4063 .orders(None, Some(&old_instrument_id), None, None, None)
4064 .is_empty()
4065 );
4066 assert_eq!(
4067 cache_rc
4068 .borrow()
4069 .orders(None, Some(&new_instrument_id), None, None, None)
4070 .len(),
4071 1,
4072 );
4073 assert!(matches!(event, OrderEventAny::Initialized(_)));
4074 event_messages.borrow_mut().push(event.clone());
4075 timeline.borrow_mut().push("init");
4076 })
4077 };
4078 let risk_handler = {
4079 let timeline = timeline.clone();
4080 TypedIntoHandler::from_with_id(
4081 "RiskEngine.queue_execute",
4082 move |command: TradingCommand| {
4083 assert!(matches!(command, TradingCommand::SubmitOrder(_)));
4084 timeline.borrow_mut().push("command");
4085 },
4086 )
4087 };
4088 msgbus::register_trading_command_endpoint(
4089 MessagingSwitchboard::risk_engine_queue_execute(),
4090 risk_handler,
4091 );
4092
4093 let topic = format!("events.order.{}", order.strategy_id());
4094 msgbus::subscribe_order_events(topic.clone().into(), event_handler.clone(), None);
4095
4096 strategy
4097 .submit_order(order.clone(), None, None, None)
4098 .unwrap();
4099
4100 msgbus::unsubscribe_order_events(topic.into(), &event_handler);
4101
4102 let event_messages = event_messages.borrow();
4103 assert_eq!(event_messages.len(), 1);
4104 assert_eq!(
4105 event_messages[0],
4106 OrderEventAny::Initialized(order.init_event().clone())
4107 );
4108 assert_eq!(timeline.borrow().as_slice(), &["init", "command"]);
4109 }
4110
4111 #[rstest]
4112 #[case::single("single", "1.234570", "0.00000211 BTC", "0.000001", Some("1.234567"))]
4113 #[case::all("all", "1.234570", "0.00000211 BTC", "0.000001", Some("1.234567"))]
4114 #[case::single_dust("single", "0.000001", "0.00000011 BTC", "0.000001", None)]
4115 #[case::all_dust("all", "0.000001", "0.00000011 BTC", "0.000001", None)]
4116 #[case::retry("retry", "1.234570", "0.00000211 BTC", "0.000001", Some("1.234567"))]
4117 #[case::retry_dust("retry", "0.000001", "0.00000011 BTC", "0.000001", None)]
4118 #[case::single_increment("single", "1.00", "0.001 BTC", "0.50", Some("0.50"))]
4119 #[case::all_increment("all", "1.00", "0.001 BTC", "0.50", Some("0.50"))]
4120 #[case::retry_increment("retry", "1.00", "0.001 BTC", "0.50", Some("0.50"))]
4121 #[case::single_increment_dust("single", "0.50", "0.001 BTC", "0.50", None)]
4122 #[case::all_increment_dust("all", "0.50", "0.001 BTC", "0.50", None)]
4123 #[case::retry_increment_dust("retry", "0.50", "0.001 BTC", "0.50", None)]
4124 #[case::single_increment_same_precision("single", "1.00", "0.25 BTC", "0.50", Some("0.50"))]
4125 fn test_close_positions_retains_fee_dust(
4126 #[case] operation: &str,
4127 #[case] qty: &str,
4128 #[case] fee: &str,
4129 #[case] increment: &str,
4130 #[case] expected_qty: Option<&str>,
4131 ) {
4132 let mut strategy = create_test_strategy();
4133 register_strategy(&mut strategy);
4134 let mut pair = currency_pair_btcusdt();
4135 pair.size_increment = Quantity::from(increment);
4136 pair.size_precision = pair.size_increment.precision;
4137 let instrument = InstrumentAny::CurrencyPair(pair);
4138 let fill = OrderFilledSpec::builder()
4139 .strategy_id(strategy.core.strategy_id().unwrap())
4140 .instrument_id(instrument.id())
4141 .position_id(PositionId::from("P-CLOSE-DUST"))
4142 .last_qty(Quantity::from(qty))
4143 .last_px(Price::from("10000.00"))
4144 .commission(Money::from_str_exact(fee).unwrap())
4145 .build();
4146 let position = Position::new(&instrument, fill);
4147 let cache = strategy.core.cache_rc();
4148 cache
4149 .borrow_mut()
4150 .add_instrument(instrument.clone())
4151 .unwrap();
4152 cache
4153 .borrow_mut()
4154 .add_position(&position, OmsType::Hedging)
4155 .unwrap();
4156 let (handler, messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4157 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4158 msgbus::register_trading_command_endpoint(
4159 MessagingSwitchboard::risk_engine_queue_execute(),
4160 handler,
4161 );
4162
4163 match operation {
4164 "all" => strategy
4165 .close_all_positions(instrument.id(), None, None, None, None, None, None, None)
4166 .unwrap(),
4167 "retry" => {
4168 strategy.core.is_exiting = true;
4169
4170 let event = TimeEvent::new(
4171 strategy.core.market_exit_timer_name,
4172 UUID4::new(),
4173 UnixNanos::default(),
4174 UnixNanos::default(),
4175 );
4176 strategy.check_market_exit(event);
4177 }
4178 _ => strategy
4179 .close_position(&position, None, None, None, None, None, None)
4180 .unwrap(),
4181 }
4182
4183 assert_eq!(
4184 strategy.core.is_exiting,
4185 operation == "retry" && expected_qty.is_some()
4186 );
4187 let messages = messages.get_messages();
4188
4189 if let Some(expected) = expected_qty {
4190 assert_eq!(messages.len(), 1);
4191
4192 let TradingCommand::SubmitOrder(command) = &messages[0] else {
4193 panic!("expected close order")
4194 };
4195
4196 let cache = cache.borrow();
4197 let order = cache.order(&command.client_order_id).unwrap();
4198 assert_eq!(order.quantity(), Quantity::from(expected));
4199 assert_eq!(order.order_side(), OrderSide::Sell);
4200 assert!(order.is_reduce_only());
4201 assert_eq!(command.position_id, Some(position.id));
4202 } else {
4203 assert!(messages.is_empty());
4204 }
4205
4206 assert_eq!(
4207 cache.borrow().position(&position.id).unwrap().signed_qty,
4208 position.signed_qty
4209 );
4210 }
4211
4212 #[rstest]
4213 fn test_market_exit_retry_sorts_and_deduplicates_instruments() {
4214 let mut strategy = create_test_strategy();
4215 register_strategy(&mut strategy);
4216 let cache = strategy.core.cache_rc();
4217
4218 for index in (0..12).rev() {
4219 let mut pair = currency_pair_btcusdt();
4220 pair.id = InstrumentId::from(format!("BTCUSDT.VENUE{index:02}"));
4221 let instrument = InstrumentAny::CurrencyPair(pair);
4222 cache
4223 .borrow_mut()
4224 .add_instrument(instrument.clone())
4225 .unwrap();
4226 let fill = OrderFilledSpec::builder()
4227 .strategy_id(strategy.core.strategy_id().unwrap())
4228 .instrument_id(instrument.id())
4229 .client_order_id(ClientOrderId::from(format!("O-SORT-{index:02}")))
4230 .position_id(PositionId::from(format!("P-SORT-{:02}", 11 - index)))
4231 .last_qty(Quantity::from(1))
4232 .last_px(Price::from("10000.00"))
4233 .currency(instrument.quote_currency())
4234 .commission(Money::zero(instrument.quote_currency()))
4235 .build();
4236 let position = Position::new(&instrument, fill.clone());
4237 cache
4238 .borrow_mut()
4239 .add_position(&position, OmsType::Hedging)
4240 .unwrap();
4241
4242 if index == 0 {
4243 let mut duplicate = fill;
4244 duplicate.position_id = Some(PositionId::from("Z-SORT-DUP"));
4245 duplicate.client_order_id = ClientOrderId::from("O-SORT-DUP");
4246 let position = Position::new(&instrument, duplicate);
4247 cache
4248 .borrow_mut()
4249 .add_position(&position, OmsType::Hedging)
4250 .unwrap();
4251 }
4252 }
4253
4254 let (handler, messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4255 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4256 msgbus::register_trading_command_endpoint(
4257 MessagingSwitchboard::risk_engine_queue_execute(),
4258 handler,
4259 );
4260 strategy.core.is_exiting = true;
4261
4262 let event = TimeEvent::new(
4263 strategy.core.market_exit_timer_name,
4264 UUID4::new(),
4265 UnixNanos::default(),
4266 UnixNanos::default(),
4267 );
4268
4269 strategy.check_market_exit(event);
4270
4271 let commands: Vec<_> = messages
4272 .get_messages()
4273 .iter()
4274 .map(|message| {
4275 let TradingCommand::SubmitOrder(command) = message else {
4276 panic!("expected close command")
4277 };
4278
4279 (command.instrument_id, command.position_id.unwrap())
4280 })
4281 .collect();
4282
4283 let mut expected = vec![
4284 (
4285 InstrumentId::from("BTCUSDT.VENUE00"),
4286 PositionId::from("P-SORT-11"),
4287 ),
4288 (
4289 InstrumentId::from("BTCUSDT.VENUE00"),
4290 PositionId::from("Z-SORT-DUP"),
4291 ),
4292 ];
4293 expected.extend((1..12).map(|index| {
4294 (
4295 InstrumentId::from(format!("BTCUSDT.VENUE{index:02}")),
4296 PositionId::from(format!("P-SORT-{:02}", 11 - index)),
4297 )
4298 }));
4299
4300 assert_eq!(commands, expected);
4301 }
4302
4303 #[rstest]
4304 fn test_submit_order_routes_emulated_order_to_order_emulator() {
4305 let mut strategy = create_test_strategy();
4306 register_strategy(&mut strategy);
4307 let (emulator_handler, emulator_messages): (
4308 _,
4309 TypedIntoMessageSavingHandler<TradingCommand>,
4310 ) = get_typed_into_message_saving_handler(Some(Ustr::from("OrderEmulator.execute")));
4311 msgbus::register_trading_command_endpoint(
4312 MessagingSwitchboard::order_emulator_execute(),
4313 emulator_handler,
4314 );
4315 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4316 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4317 msgbus::register_trading_command_endpoint(
4318 MessagingSwitchboard::risk_engine_queue_execute(),
4319 risk_handler,
4320 );
4321 let order = OrderTestBuilder::new(OrderType::StopMarket)
4322 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
4323 .client_order_id(ClientOrderId::from("O-20250208-EMULATED-001"))
4324 .side(OrderSide::Buy)
4325 .trigger_price(Price::from("51000.0"))
4326 .quantity(Quantity::from(100_000))
4327 .emulation_trigger(TriggerType::BidAsk)
4328 .build();
4329 let client_order_id = order.client_order_id();
4330
4331 strategy.submit_order(order, None, None, None).unwrap();
4332
4333 let emulator_messages = emulator_messages.get_messages();
4334 assert_eq!(emulator_messages.len(), 1);
4335 assert!(matches!(
4336 emulator_messages.first(),
4337 Some(TradingCommand::SubmitOrder(command))
4338 if command.client_order_id == client_order_id
4339 ));
4340 assert!(risk_messages.get_messages().is_empty());
4341 }
4342
4343 #[rstest]
4344 fn test_submit_order_errors_when_strategy_not_registered() {
4345 let mut strategy = create_test_strategy();
4346 let order = make_initialized_market_order("O-20250208-UNREGISTERED-001");
4347
4348 let err = strategy
4349 .submit_order(order, None, None, None)
4350 .unwrap_err()
4351 .to_string();
4352
4353 assert_eq!(err, "Strategy not registered: trader_id is not set");
4354 }
4355
4356 #[rstest]
4357 fn test_submit_order_uses_stored_strategy_id_when_actor_id_diverges() {
4358 let mut strategy = create_test_strategy();
4359 register_strategy(&mut strategy);
4360 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4361 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4362 msgbus::register_trading_command_endpoint(
4363 MessagingSwitchboard::risk_engine_queue_execute(),
4364 risk_handler,
4365 );
4366
4367 strategy.core.actor.actor_id = ActorId::from("Strategy");
4369 let order = make_initialized_market_order("O-20250208-DIVERGED-001");
4370
4371 strategy.submit_order(order, None, None, None).unwrap();
4372
4373 let risk_messages = risk_messages.get_messages();
4374 assert_eq!(risk_messages.len(), 1);
4375 let Some(TradingCommand::SubmitOrder(command)) = risk_messages.first() else {
4376 panic!("Expected a SubmitOrder command, was {risk_messages:?}");
4377 };
4378 assert_eq!(command.strategy_id, StrategyId::from("TEST-001"));
4379 assert_eq!(
4380 command.client_order_id,
4381 ClientOrderId::from("O-20250208-DIVERGED-001")
4382 );
4383 }
4384
4385 #[rstest]
4386 fn test_required_account_id_errors_when_missing_for_strategy_event() {
4387 let order = make_initialized_market_order("O-20250208-NO-ACCOUNT-001");
4388
4389 let err = required_account_id(&order, "pending cancel")
4390 .unwrap_err()
4391 .to_string();
4392
4393 assert_eq!(
4394 err,
4395 "Cannot generate pending cancel event for O-20250208-NO-ACCOUNT-001: \
4396 account_id is not set"
4397 );
4398 }
4399
4400 #[rstest]
4401 fn test_submit_order_rejects_non_initialized_without_events() {
4402 let mut strategy = create_test_strategy();
4403 register_strategy(&mut strategy);
4404
4405 let order = make_accepted_market_order("O-20250208-ACCEPTED-001");
4406 let topic = format!("events.order.{}", order.strategy_id());
4407 let (event_handler, event_messages): (_, TypedMessageSavingHandler<OrderEventAny>) =
4408 get_typed_message_saving_handler(Some(Ustr::from("events.order.invalid")));
4409
4410 msgbus::subscribe_order_events(topic.clone().into(), event_handler.clone(), None);
4411 let result = strategy.submit_order(order, None, None, None);
4412
4413 msgbus::unsubscribe_order_events(topic.into(), &event_handler);
4414
4415 assert!(result.is_err());
4416 assert!(
4417 result
4418 .unwrap_err()
4419 .to_string()
4420 .contains("expected INITIALIZED")
4421 );
4422 assert!(event_messages.get_messages().is_empty());
4423 }
4424
4425 #[rstest]
4426 fn test_submit_order_returns_error_when_cache_already_borrowed() {
4427 let mut strategy = create_test_strategy();
4428 register_strategy(&mut strategy);
4429
4430 let order = make_initialized_market_order("O-20250208-BORROWED-001");
4431 let cache_rc = strategy.core.cache_rc();
4432 let _cache = cache_rc.borrow();
4433
4434 let result = catch_unwind(AssertUnwindSafe(|| {
4435 strategy.submit_order(order, None, None, None)
4436 }));
4437
4438 let err = result
4439 .expect("submit_order should not panic")
4440 .unwrap_err()
4441 .to_string();
4442
4443 assert_eq!(
4444 err,
4445 "Cannot submit order O-20250208-BORROWED-001: cache is currently borrowed"
4446 );
4447 }
4448
4449 #[rstest]
4450 fn test_submit_order_list_publishes_order_initialized_after_cache_insert_before_send() {
4451 let mut strategy = create_test_strategy();
4452 register_strategy(&mut strategy);
4453
4454 let order_list_id = OrderListId::from("OL-20250208-LIST-INIT");
4455 let mut orders = vec![
4456 make_initialized_market_order("O-20250208-LIST-INIT-001"),
4457 make_initialized_market_order("O-20250208-LIST-INIT-002"),
4458 ];
4459
4460 for order in &mut orders {
4461 order.set_order_list_id(order_list_id);
4462 }
4463
4464 let client_order_id1 = orders[0].client_order_id();
4465 let client_order_id2 = orders[1].client_order_id();
4466 let cache_rc = strategy.core.cache_rc();
4467 let timeline = Rc::new(RefCell::new(Vec::new()));
4468 let event_messages = Rc::new(RefCell::new(Vec::new()));
4469
4470 let event_handler = {
4471 let event_messages = event_messages.clone();
4472 let timeline = timeline.clone();
4473 TypedHandler::from_with_id(
4474 "events.order.list_initialized",
4475 move |event: &OrderEventAny| {
4476 match event {
4477 OrderEventAny::Initialized(e) if e.client_order_id == client_order_id1 => {
4478 let cache = cache_rc.borrow();
4479 assert!(cache.order_exists(&client_order_id1));
4480 assert!(cache.order_exists(&client_order_id2));
4481 assert!(cache.order_list_exists(&order_list_id));
4482 let order_list = cache.order_list(&order_list_id).unwrap();
4483 assert_eq!(
4484 order_list.client_order_ids.as_slice(),
4485 &[client_order_id1, client_order_id2]
4486 );
4487 timeline.borrow_mut().push("init1");
4488 }
4489 OrderEventAny::Initialized(e) if e.client_order_id == client_order_id2 => {
4490 assert!(cache_rc.borrow().order_exists(&client_order_id2));
4491 timeline.borrow_mut().push("init2");
4492 }
4493 _ => panic!("unexpected order event {event:?}"),
4494 }
4495 event_messages.borrow_mut().push(event.clone());
4496 },
4497 )
4498 };
4499 let risk_handler = {
4500 let timeline = timeline.clone();
4501 TypedIntoHandler::from_with_id(
4502 "RiskEngine.queue_execute",
4503 move |command: TradingCommand| {
4504 assert!(matches!(command, TradingCommand::SubmitOrderList(_)));
4505 timeline.borrow_mut().push("command");
4506 },
4507 )
4508 };
4509 msgbus::register_trading_command_endpoint(
4510 MessagingSwitchboard::risk_engine_queue_execute(),
4511 risk_handler,
4512 );
4513
4514 let topic = format!("events.order.{}", orders[0].strategy_id());
4515 msgbus::subscribe_order_events(topic.clone().into(), event_handler.clone(), None);
4516
4517 strategy
4518 .submit_order_list(orders.clone(), None, None, None)
4519 .unwrap();
4520
4521 msgbus::unsubscribe_order_events(topic.into(), &event_handler);
4522
4523 let event_messages = event_messages.borrow();
4524 assert_eq!(event_messages.len(), 2);
4525 assert_eq!(
4526 event_messages[0],
4527 OrderEventAny::Initialized(orders[0].init_event().clone())
4528 );
4529 assert_eq!(
4530 event_messages[1],
4531 OrderEventAny::Initialized(orders[1].init_event().clone())
4532 );
4533 assert_eq!(timeline.borrow().as_slice(), &["init1", "init2", "command"]);
4534 }
4535
4536 #[rstest]
4537 fn test_submission_persistence_failure_does_not_block_dispatch(
4538 #[values(false, true)] fail_position_index: bool,
4539 #[values(false, true)] list: bool,
4540 ) {
4541 let mut strategy = create_test_strategy();
4542 register_strategy(&mut strategy);
4543 let (database, control) = FailNthAddOrderDatabase::create();
4544 let cache_rc = strategy.core.cache_rc();
4545 cache_rc.borrow_mut().set_database(Box::new(database));
4546
4547 let order_count = if list { 2 } else { 1 };
4548 if fail_position_index {
4549 control.set_fail_index_order_position_on(Some(order_count));
4550 } else {
4551 control.set_fail_add_order_on(Some(order_count));
4552 }
4553
4554 let position_id = PositionId::from("P-LIST-RETRY");
4555 let client_id = ClientId::from("EXEC-LIST-RETRY");
4556 let order_list_id = OrderListId::from("OL-LIST-RETRY");
4557 let mut orders = vec![
4558 make_initialized_market_order("O-LIST-RETRY-001"),
4559 make_initialized_market_order("O-LIST-RETRY-002"),
4560 ];
4561
4562 orders.truncate(order_count);
4563 if list {
4564 for order in &mut orders {
4565 order.set_order_list_id(order_list_id);
4566 }
4567 }
4568
4569 let topic = format!("events.order.{}", orders[0].strategy_id());
4570 let (event_handler, event_messages): (_, TypedMessageSavingHandler<OrderEventAny>) =
4571 get_typed_message_saving_handler(Some(Ustr::from("events.order.list_retry")));
4572 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4573 get_typed_into_message_saving_handler(Some(Ustr::from("risk.list_retry")));
4574 msgbus::subscribe_order_events(topic.clone().into(), event_handler.clone(), None);
4575 msgbus::register_trading_command_endpoint(
4576 MessagingSwitchboard::risk_engine_queue_execute(),
4577 risk_handler,
4578 );
4579
4580 if list {
4581 strategy
4582 .submit_order_list(orders.clone(), Some(position_id), Some(client_id), None)
4583 .unwrap();
4584 } else {
4585 strategy
4586 .submit_order(orders[0].clone(), Some(position_id), Some(client_id), None)
4587 .unwrap();
4588 }
4589
4590 msgbus::unsubscribe_order_events(topic.into(), &event_handler);
4591
4592 let cache = cache_rc.borrow();
4593 assert_eq!(control.calls(), (order_count, order_count));
4594 assert_eq!(
4595 cache.order_lists(None, None, None, None).len(),
4596 usize::from(list)
4597 );
4598
4599 if list {
4600 assert_eq!(
4601 cache.order_list(&order_list_id).unwrap().client_order_ids,
4602 orders
4603 .iter()
4604 .map(Order::client_order_id)
4605 .collect::<Vec<_>>()
4606 );
4607 }
4608
4609 for order in &orders {
4610 let cached = cache.order(&order.client_order_id()).unwrap();
4611 assert_eq!(
4612 serde_json::to_value(&*cached).unwrap(),
4613 serde_json::to_value(order).unwrap()
4614 );
4615 assert_eq!(
4616 cache.position_id(&order.client_order_id()),
4617 Some(&position_id)
4618 );
4619 assert_eq!(cache.client_id(&order.client_order_id()), Some(&client_id));
4620 }
4621
4622 assert_eq!(cache.orders_for_position(&position_id).len(), order_count);
4623 assert_eq!(
4624 event_messages.get_messages(),
4625 orders
4626 .iter()
4627 .map(|order| OrderEventAny::Initialized(order.init_event().clone()))
4628 .collect::<Vec<_>>()
4629 );
4630 let commands = risk_messages.get_messages();
4631 assert_eq!(commands.len(), 1);
4632
4633 match (&commands[0], list) {
4634 (TradingCommand::SubmitOrderList(command), true) => {
4635 assert_eq!(command.order_list.id, order_list_id);
4636 assert_eq!(command.position_id, Some(position_id));
4637 assert_eq!(command.client_id, Some(client_id));
4638 assert_eq!(
4639 command.order_inits,
4640 orders
4641 .iter()
4642 .map(|order| order.init_event().clone())
4643 .collect::<Vec<_>>()
4644 );
4645 }
4646 (TradingCommand::SubmitOrder(command), false) => {
4647 assert_eq!(command.position_id, Some(position_id));
4648 assert_eq!(command.client_id, Some(client_id));
4649 assert_eq!(command.order_init, orders[0].init_event().clone());
4650 }
4651 _ => panic!("unexpected submission command"),
4652 }
4653 }
4654
4655 #[rstest]
4656 fn test_submit_order_list_returns_error_when_cache_already_borrowed() {
4657 let mut strategy = create_test_strategy();
4658 register_strategy(&mut strategy);
4659
4660 let order_list_id = OrderListId::from("OL-20250208-BORROWED");
4661 let mut orders = vec![
4662 make_initialized_market_order("O-20250208-LIST-BORROWED-001"),
4663 make_initialized_market_order("O-20250208-LIST-BORROWED-002"),
4664 ];
4665
4666 for order in &mut orders {
4667 order.set_order_list_id(order_list_id);
4668 }
4669
4670 let cache_rc = strategy.core.cache_rc();
4671 let _cache = cache_rc.borrow();
4672
4673 let result = catch_unwind(AssertUnwindSafe(|| {
4674 strategy.submit_order_list(orders, None, None, None)
4675 }));
4676
4677 let err = result
4678 .expect("submit_order_list should not panic")
4679 .unwrap_err()
4680 .to_string();
4681
4682 assert_eq!(
4683 err,
4684 "Cannot submit order list OL-20250208-BORROWED: cache is currently borrowed"
4685 );
4686 }
4687
4688 #[rstest]
4689 fn test_submit_order_list_create_list_branch_publishes_init_after_cache_insert() {
4690 let mut strategy = create_test_strategy();
4691 register_strategy(&mut strategy);
4692
4693 let orders = vec![
4694 make_initialized_market_order("O-20250208-LIST-CREATE-001"),
4695 make_initialized_market_order("O-20250208-LIST-CREATE-002"),
4696 ];
4697
4698 let client_order_id1 = orders[0].client_order_id();
4699 let client_order_id2 = orders[1].client_order_id();
4700 let cache_rc = strategy.core.cache_rc();
4701 let timeline = Rc::new(RefCell::new(Vec::new()));
4702 let event_messages = Rc::new(RefCell::new(Vec::new()));
4703
4704 let event_handler = {
4705 let event_messages = event_messages.clone();
4706 let timeline = timeline.clone();
4707 TypedHandler::from_with_id(
4708 "events.order.list_create_initialized",
4709 move |event: &OrderEventAny| {
4710 match event {
4711 OrderEventAny::Initialized(e) if e.client_order_id == client_order_id1 => {
4712 let cache = cache_rc.borrow();
4713 let cached_order1 = cache.order(&client_order_id1).unwrap();
4714 let cached_order2 = cache.order(&client_order_id2).unwrap();
4715 let order_list_id = cached_order1.order_list_id().unwrap();
4716 assert_eq!(cached_order2.order_list_id(), Some(order_list_id));
4717 assert_eq!(e.order_list_id, Some(order_list_id));
4718 assert!(cache.order_list_exists(&order_list_id));
4719 let order_list = cache.order_list(&order_list_id).unwrap();
4720 assert_eq!(
4721 order_list.client_order_ids.as_slice(),
4722 &[client_order_id1, client_order_id2]
4723 );
4724 timeline.borrow_mut().push("init1");
4725 }
4726 OrderEventAny::Initialized(e) if e.client_order_id == client_order_id2 => {
4727 let cache = cache_rc.borrow();
4728 let cached_order = cache.order(&client_order_id2).unwrap();
4729 assert_eq!(e.order_list_id, cached_order.order_list_id());
4730 timeline.borrow_mut().push("init2");
4731 }
4732 _ => panic!("unexpected order event {event:?}"),
4733 }
4734 event_messages.borrow_mut().push(event.clone());
4735 },
4736 )
4737 };
4738 let risk_handler = {
4739 let timeline = timeline.clone();
4740 TypedIntoHandler::from_with_id(
4741 "RiskEngine.queue_execute",
4742 move |command: TradingCommand| {
4743 let TradingCommand::SubmitOrderList(command) = command else {
4744 panic!("expected SubmitOrderList command");
4745 };
4746 assert!(
4747 command
4748 .order_inits
4749 .iter()
4750 .all(|init| init.order_list_id == Some(command.order_list.id))
4751 );
4752 timeline.borrow_mut().push("command");
4753 },
4754 )
4755 };
4756 msgbus::register_trading_command_endpoint(
4757 MessagingSwitchboard::risk_engine_queue_execute(),
4758 risk_handler,
4759 );
4760
4761 let topic = format!("events.order.{}", orders[0].strategy_id());
4762 msgbus::subscribe_order_events(topic.clone().into(), event_handler.clone(), None);
4763
4764 strategy
4765 .submit_order_list(orders, None, None, None)
4766 .unwrap();
4767
4768 msgbus::unsubscribe_order_events(topic.into(), &event_handler);
4769
4770 let cache = strategy.cache();
4771 let cached_order1 = cache.order(&client_order_id1).unwrap();
4772 let cached_order2 = cache.order(&client_order_id2).unwrap();
4773 let order_list_id = cached_order1.order_list_id().unwrap();
4774 assert_eq!(cached_order2.order_list_id(), Some(order_list_id));
4775
4776 let event_messages = event_messages.borrow();
4777 assert_eq!(event_messages.len(), 2);
4778 let OrderEventAny::Initialized(init1) = &event_messages[0] else {
4779 panic!("expected first OrderInitialized event");
4780 };
4781 let OrderEventAny::Initialized(init2) = &event_messages[1] else {
4782 panic!("expected second OrderInitialized event");
4783 };
4784 assert_eq!(init1.order_list_id, Some(order_list_id));
4785 assert_eq!(init2.order_list_id, Some(order_list_id));
4786 assert_eq!(timeline.borrow().as_slice(), &["init1", "init2", "command"]);
4787
4788 let order_list = cache.order_list(&order_list_id).unwrap();
4789 assert_eq!(
4790 order_list.client_order_ids.as_slice(),
4791 &[client_order_id1, client_order_id2]
4792 );
4793 }
4794
4795 #[rstest]
4796 fn test_submit_order_list_routes_optional_params_to_risk() {
4797 let mut strategy = create_test_strategy();
4798 register_strategy(&mut strategy);
4799
4800 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4801 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4802 msgbus::register_trading_command_endpoint(
4803 MessagingSwitchboard::risk_engine_queue_execute(),
4804 risk_handler,
4805 );
4806
4807 let no_params_orders = vec![
4808 make_initialized_market_order("O-20250208-LIST-001"),
4809 make_initialized_market_order("O-20250208-LIST-002"),
4810 ];
4811 strategy
4812 .submit_order_list(no_params_orders, None, None, None)
4813 .unwrap();
4814
4815 let params = Params::new(
4816 [(
4817 "routing_hint".to_string(),
4818 Value::String("prefer_batch".to_string()),
4819 )]
4820 .into(),
4821 );
4822 let param_orders = vec![
4823 make_initialized_market_order("O-20250208-LIST-003"),
4824 make_initialized_market_order("O-20250208-LIST-004"),
4825 ];
4826 strategy
4827 .submit_order_list(param_orders, None, None, Some(params.clone()))
4828 .unwrap();
4829
4830 let risk_messages = risk_messages.get_messages();
4831 assert_eq!(risk_messages.len(), 2);
4832 let Some(TradingCommand::SubmitOrderList(no_params_command)) = risk_messages.first() else {
4833 panic!("expected SubmitOrderList command");
4834 };
4835 let Some(TradingCommand::SubmitOrderList(param_command)) = risk_messages.get(1) else {
4836 panic!("expected SubmitOrderList command");
4837 };
4838 assert!(no_params_command.params.is_none());
4839 assert_eq!(param_command.params.as_ref(), Some(¶ms));
4840 }
4841
4842 #[rstest]
4843 fn test_modify_order_routes_non_emulated_orders_to_risk() {
4844 let mut strategy = create_test_strategy();
4845 register_strategy(&mut strategy);
4846
4847 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4848 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4849 msgbus::register_trading_command_endpoint(
4850 MessagingSwitchboard::risk_engine_queue_execute(),
4851 risk_handler,
4852 );
4853
4854 let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4855 get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
4856 msgbus::register_trading_command_endpoint(
4857 MessagingSwitchboard::exec_engine_queue_execute(),
4858 exec_handler,
4859 );
4860
4861 let order = OrderAny::Market(MarketOrder::new(
4862 TraderId::from("TRADER-001"),
4863 StrategyId::from("TEST-001"),
4864 InstrumentId::from("BTCUSDT.BINANCE"),
4865 ClientOrderId::from("O-20250208-0003"),
4866 OrderSide::Buy,
4867 Quantity::from(100_000),
4868 TimeInForce::Gtc,
4869 UUID4::new(),
4870 UnixNanos::default(),
4871 false,
4872 false,
4873 None,
4874 None,
4875 None,
4876 None,
4877 None,
4878 None,
4879 None,
4880 None,
4881 ));
4882 add_order_to_cache(&strategy, &order);
4883
4884 strategy
4885 .modify_order(
4886 order.client_order_id(),
4887 Some(Quantity::from(200_000)),
4888 None,
4889 None,
4890 None,
4891 None,
4892 )
4893 .unwrap();
4894
4895 let risk_messages = risk_messages.get_messages();
4896 let exec_messages = exec_messages.get_messages();
4897
4898 assert_eq!(risk_messages.len(), 1);
4899 assert!(matches!(
4900 risk_messages.first(),
4901 Some(TradingCommand::ModifyOrder(_))
4902 ));
4903 assert!(exec_messages.is_empty());
4904 }
4905
4906 #[rstest]
4907 #[case::trailing_stop_market(OrderType::TrailingStopMarket)]
4908 #[case::trailing_stop_limit(OrderType::TrailingStopLimit)]
4909 fn test_modify_order_changes_trailing_stop_trigger(#[case] order_type: OrderType) {
4910 let mut strategy = create_test_strategy();
4911 register_strategy(&mut strategy);
4912
4913 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4914 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4915 msgbus::register_trading_command_endpoint(
4916 MessagingSwitchboard::risk_engine_queue_execute(),
4917 risk_handler,
4918 );
4919
4920 let order = make_initialized_trailing_stop_order(order_type, "O-TRAILING-MODIFY");
4921 add_order_to_cache(&strategy, &order);
4922
4923 strategy
4924 .modify_order(
4925 order.client_order_id(),
4926 None,
4927 None,
4928 Some(Price::from("49500.0")),
4929 None,
4930 None,
4931 )
4932 .unwrap();
4933
4934 let risk_messages = risk_messages.get_messages();
4935
4936 let [TradingCommand::ModifyOrder(command)] = risk_messages.as_slice() else {
4937 panic!("expected one ModifyOrder command, was {risk_messages:?}");
4938 };
4939
4940 assert_eq!(command.client_order_id, order.client_order_id());
4941 assert_eq!(command.quantity, None);
4942 assert_eq!(command.price, None);
4943 assert_eq!(command.trigger_price, Some(Price::from("49500.0")));
4944 }
4945
4946 #[rstest]
4947 #[case::trailing_stop_market(OrderType::TrailingStopMarket)]
4948 #[case::trailing_stop_limit(OrderType::TrailingStopLimit)]
4949 fn test_modify_orders_changes_trailing_stop_trigger(#[case] order_type: OrderType) {
4950 let mut strategy = create_test_strategy();
4951 register_strategy(&mut strategy);
4952
4953 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4954 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4955 msgbus::register_trading_command_endpoint(
4956 MessagingSwitchboard::risk_engine_queue_execute(),
4957 risk_handler,
4958 );
4959
4960 let mut order = make_initialized_trailing_stop_order(order_type, "O-TRAILING-BATCH-MODIFY");
4961 let account_id = AccountId::from("ACC-001");
4962 order
4963 .apply(TestOrderEventStubs::submitted(&order, account_id))
4964 .unwrap();
4965 order
4966 .apply(TestOrderEventStubs::accepted(
4967 &order,
4968 account_id,
4969 VenueOrderId::from("V-TRAILING-BATCH-MODIFY"),
4970 ))
4971 .unwrap();
4972 add_order_to_cache(&strategy, &order);
4973
4974 strategy
4975 .modify_orders(
4976 vec![(
4977 order.client_order_id(),
4978 None,
4979 None,
4980 Some(Price::from("49500.0")),
4981 )],
4982 None,
4983 None,
4984 )
4985 .unwrap();
4986
4987 let risk_messages = risk_messages.get_messages();
4988
4989 let [TradingCommand::ModifyOrders(command)] = risk_messages.as_slice() else {
4990 panic!("expected one BatchModifyOrders command, was {risk_messages:?}");
4991 };
4992
4993 let [modify] = command.modifies.as_slice() else {
4994 panic!("expected one ModifyOrder, was {:?}", command.modifies);
4995 };
4996
4997 assert_eq!(modify.client_order_id, order.client_order_id());
4998 assert_eq!(
4999 modify.venue_order_id,
5000 Some(VenueOrderId::from("V-TRAILING-BATCH-MODIFY"))
5001 );
5002 assert_eq!(modify.quantity, None);
5003 assert_eq!(modify.price, None);
5004 assert_eq!(modify.trigger_price, Some(Price::from("49500.0")));
5005 }
5006
5007 #[rstest]
5008 #[case::market(OrderType::Market)]
5009 #[case::limit(OrderType::Limit)]
5010 #[case::market_to_limit(OrderType::MarketToLimit)]
5011 fn test_modify_order_rejects_trigger_for_order_without_trigger(#[case] order_type: OrderType) {
5012 let mut strategy = create_test_strategy();
5013 register_strategy(&mut strategy);
5014
5015 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
5016 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
5017 msgbus::register_trading_command_endpoint(
5018 MessagingSwitchboard::risk_engine_queue_execute(),
5019 risk_handler,
5020 );
5021
5022 let mut builder = OrderTestBuilder::new(order_type);
5023 builder
5024 .trader_id(TraderId::from("TRADER-001"))
5025 .strategy_id(StrategyId::from("TEST-001"))
5026 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
5027 .client_order_id(ClientOrderId::from("O-NO-TRIGGER-MODIFY"))
5028 .quantity(Quantity::from(100_000));
5029
5030 if order_type == OrderType::Limit {
5031 builder.price(Price::from("50000.0"));
5032 }
5033
5034 let order = builder.build();
5035 add_order_to_cache(&strategy, &order);
5036
5037 let result = strategy.modify_order(
5038 order.client_order_id(),
5039 None,
5040 None,
5041 Some(Price::from("49500.0")),
5042 None,
5043 None,
5044 );
5045
5046 assert_eq!(
5047 result.unwrap_err().to_string(),
5048 format!("{order_type} orders do not have a STOP trigger price")
5049 );
5050 assert!(risk_messages.get_messages().is_empty());
5051 }
5052
5053 #[rstest]
5054 fn test_modify_order_routes_active_local_algorithm_order_to_algorithm() {
5055 let mut strategy = create_test_strategy();
5056 register_strategy(&mut strategy);
5057
5058 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
5059 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
5060 msgbus::register_trading_command_endpoint(
5061 MessagingSwitchboard::risk_engine_queue_execute(),
5062 risk_handler,
5063 );
5064 let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
5065 get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
5066 msgbus::register_trading_command_endpoint(
5067 MessagingSwitchboard::exec_engine_queue_execute(),
5068 exec_handler,
5069 );
5070 let (algo_handler, algo_messages) =
5071 get_any_saving_handler::<TradingCommand>(Some(Ustr::from("TWAP.execute")));
5072 msgbus::register_any("TWAP.execute".into(), algo_handler);
5073
5074 let order = make_initialized_algorithm_order("O-20250208-ALGO-MODIFY-001");
5075 add_order_to_cache(&strategy, &order);
5076
5077 strategy
5078 .modify_order(
5079 order.client_order_id(),
5080 Some(Quantity::from(200_000)),
5081 None,
5082 None,
5083 None,
5084 None,
5085 )
5086 .unwrap();
5087
5088 let algo_messages = algo_messages.get_messages();
5089 assert_eq!(algo_messages.len(), 1);
5090 assert!(matches!(
5091 algo_messages.first(),
5092 Some(TradingCommand::ModifyOrder(command))
5093 if command.client_order_id == order.client_order_id()
5094 ));
5095 assert!(risk_messages.get_messages().is_empty());
5096 assert!(exec_messages.get_messages().is_empty());
5097 }
5098
5099 #[rstest]
5100 fn test_modify_order_routes_accepted_algorithm_order_to_risk() {
5101 let mut strategy = create_test_strategy();
5102 register_strategy(&mut strategy);
5103
5104 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
5105 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
5106 msgbus::register_trading_command_endpoint(
5107 MessagingSwitchboard::risk_engine_queue_execute(),
5108 risk_handler,
5109 );
5110 let (algo_handler, algo_messages) =
5111 get_any_saving_handler::<TradingCommand>(Some(Ustr::from("TWAP.execute")));
5112 msgbus::register_any("TWAP.execute".into(), algo_handler);
5113
5114 let mut order = make_initialized_algorithm_order("O-20250208-ALGO-MODIFY-002");
5115 let account_id = AccountId::from("ACC-001");
5116 order
5117 .apply(TestOrderEventStubs::submitted(&order, account_id))
5118 .unwrap();
5119 order
5120 .apply(TestOrderEventStubs::accepted(
5121 &order,
5122 account_id,
5123 VenueOrderId::from("O-20250208-ALGO-MODIFY-002"),
5124 ))
5125 .unwrap();
5126 add_order_to_cache(&strategy, &order);
5127
5128 strategy
5129 .modify_order(
5130 order.client_order_id(),
5131 Some(Quantity::from(200_000)),
5132 None,
5133 None,
5134 None,
5135 None,
5136 )
5137 .unwrap();
5138
5139 let risk_messages = risk_messages.get_messages();
5140 assert_eq!(risk_messages.len(), 1);
5141 assert!(matches!(
5142 risk_messages.first(),
5143 Some(TradingCommand::ModifyOrder(command))
5144 if command.client_order_id == order.client_order_id()
5145 ));
5146 assert!(algo_messages.get_messages().is_empty());
5147 assert_eq!(
5148 strategy
5149 .cache()
5150 .order(&order.client_order_id())
5151 .unwrap()
5152 .status(),
5153 OrderStatus::PendingUpdate
5154 );
5155 }
5156
5157 #[rstest]
5158 fn test_modify_order_routes_emulated_order_to_order_emulator() {
5159 let mut strategy = create_test_strategy();
5160 register_strategy(&mut strategy);
5161
5162 let (emulator_handler, emulator_messages): (
5163 _,
5164 TypedIntoMessageSavingHandler<TradingCommand>,
5165 ) = get_typed_into_message_saving_handler(Some(Ustr::from("OrderEmulator.execute")));
5166 msgbus::register_trading_command_endpoint(
5167 MessagingSwitchboard::order_emulator_execute(),
5168 emulator_handler,
5169 );
5170 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
5171 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
5172 msgbus::register_trading_command_endpoint(
5173 MessagingSwitchboard::risk_engine_queue_execute(),
5174 risk_handler,
5175 );
5176 let mut order = OrderTestBuilder::new(OrderType::StopMarket)
5177 .trader_id(TraderId::from("TRADER-001"))
5178 .strategy_id(StrategyId::from("TEST-001"))
5179 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
5180 .client_order_id(ClientOrderId::from("O-20250208-EMULATED-MODIFY-001"))
5181 .side(OrderSide::Buy)
5182 .trigger_price(Price::from("51000.0"))
5183 .quantity(Quantity::from(100_000))
5184 .emulation_trigger(TriggerType::BidAsk)
5185 .build();
5186 order
5187 .apply(OrderEventAny::Emulated(
5188 OrderEmulatedSpec::builder()
5189 .trader_id(order.trader_id())
5190 .strategy_id(order.strategy_id())
5191 .instrument_id(order.instrument_id())
5192 .client_order_id(order.client_order_id())
5193 .build(),
5194 ))
5195 .unwrap();
5196 add_order_to_cache(&strategy, &order);
5197
5198 strategy
5199 .modify_order(
5200 order.client_order_id(),
5201 None,
5202 None,
5203 Some(Price::from("52000.0")),
5204 None,
5205 None,
5206 )
5207 .unwrap();
5208
5209 let emulator_messages = emulator_messages.get_messages();
5210 assert_eq!(emulator_messages.len(), 1);
5211 assert!(matches!(
5212 emulator_messages.first(),
5213 Some(TradingCommand::ModifyOrder(command))
5214 if command.client_order_id == order.client_order_id()
5215 ));
5216 assert!(risk_messages.get_messages().is_empty());
5217 }
5218
5219 #[rstest]
5220 fn test_modify_order_routes_initialized_trigger_order_to_emulator_reentrantly() {
5221 let strategy_id = StrategyId::from("REENTRANT-001");
5222 let modified_quantity = Quantity::from(200_000);
5223 let mut strategy = InitializedModifyStrategy {
5224 core: StrategyCore::new(StrategyConfig {
5225 strategy_id: Some(strategy_id),
5226 ..Default::default()
5227 }),
5228 modified_quantity,
5229 };
5230 let trader_id = TraderId::from("TRADER-001");
5231 let clock = Rc::new(RefCell::new(VirtualClock::new()));
5232 let cache = Rc::new(RefCell::new(Cache::default()));
5233 let portfolio = Rc::new(RefCell::new(Portfolio::new(
5234 clock.clone(),
5235 cache.clone(),
5236 None,
5237 )));
5238 strategy
5239 .core
5240 .register(trader_id, clock, cache, portfolio)
5241 .unwrap();
5242 strategy.initialize().unwrap();
5243 strategy.start().unwrap();
5244
5245 let actor_id = strategy.actor_id().inner();
5246 register_component_actor(strategy);
5247 let order_handler = TypedHandler::from(move |event: &OrderEventAny| {
5248 let mut strategy =
5249 try_get_actor_unchecked::<InitializedModifyStrategy>(&actor_id).unwrap();
5250 strategy.handle_order_event(event.clone());
5251 });
5252 let topic = format!("events.order.{strategy_id}");
5253 msgbus::subscribe_order_events(topic.clone().into(), order_handler.clone(), None);
5254
5255 let (emulator_handler, emulator_messages): (
5256 _,
5257 TypedIntoMessageSavingHandler<TradingCommand>,
5258 ) = get_typed_into_message_saving_handler(Some(Ustr::from("OrderEmulator.execute")));
5259 msgbus::register_trading_command_endpoint(
5260 MessagingSwitchboard::order_emulator_execute(),
5261 emulator_handler,
5262 );
5263 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
5264 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
5265 msgbus::register_trading_command_endpoint(
5266 MessagingSwitchboard::risk_engine_queue_execute(),
5267 risk_handler,
5268 );
5269 let (algo_handler, algo_messages) =
5270 get_any_saving_handler::<TradingCommand>(Some(Ustr::from("TWAP.execute")));
5271 msgbus::register_any("TWAP.execute".into(), algo_handler);
5272
5273 let order = OrderTestBuilder::new(OrderType::StopMarket)
5274 .trader_id(trader_id)
5275 .strategy_id(strategy_id)
5276 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
5277 .client_order_id(ClientOrderId::from("O-REENTRANT-001"))
5278 .side(OrderSide::Buy)
5279 .trigger_price(Price::from("51000.0"))
5280 .quantity(Quantity::from(100_000))
5281 .emulation_trigger(TriggerType::BidAsk)
5282 .build();
5283 let client_order_id = order.client_order_id();
5284
5285 let mut strategy = try_get_actor_unchecked::<InitializedModifyStrategy>(&actor_id).unwrap();
5286 strategy.submit_order(order, None, None, None).unwrap();
5287 drop(strategy);
5288
5289 msgbus::unsubscribe_order_events(topic.into(), &order_handler);
5290 deregister_component(&strategy_id.inner());
5291 deregister_actor(&actor_id);
5292
5293 let emulator_messages = emulator_messages.get_messages();
5294 assert_eq!(emulator_messages.len(), 2);
5295 assert!(matches!(
5296 emulator_messages.first(),
5297 Some(TradingCommand::ModifyOrder(command))
5298 if command.client_order_id == client_order_id
5299 && command.quantity == Some(modified_quantity)
5300 ));
5301 assert!(
5302 risk_messages
5303 .get_messages()
5304 .iter()
5305 .all(|command| !matches!(command, TradingCommand::ModifyOrder(_)))
5306 );
5307 assert!(
5308 algo_messages
5309 .get_messages()
5310 .iter()
5311 .all(|command| !matches!(command, TradingCommand::ModifyOrder(_)))
5312 );
5313 assert!(matches!(
5314 emulator_messages.get(1),
5315 Some(TradingCommand::SubmitOrder(command))
5316 if command.client_order_id == client_order_id
5317 ));
5318 }
5319
5320 #[rstest]
5321 fn test_modify_order_prefers_emulator_over_algorithm_for_emulated_algorithm_order() {
5322 let mut strategy = create_test_strategy();
5323 register_strategy(&mut strategy);
5324
5325 let (emulator_handler, emulator_messages): (
5326 _,
5327 TypedIntoMessageSavingHandler<TradingCommand>,
5328 ) = get_typed_into_message_saving_handler(Some(Ustr::from("OrderEmulator.execute")));
5329 msgbus::register_trading_command_endpoint(
5330 MessagingSwitchboard::order_emulator_execute(),
5331 emulator_handler,
5332 );
5333 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
5334 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
5335 msgbus::register_trading_command_endpoint(
5336 MessagingSwitchboard::risk_engine_queue_execute(),
5337 risk_handler,
5338 );
5339 let (algo_handler, algo_messages) =
5340 get_any_saving_handler::<TradingCommand>(Some(Ustr::from("TWAP.execute")));
5341 msgbus::register_any("TWAP.execute".into(), algo_handler);
5342
5343 let mut order = OrderTestBuilder::new(OrderType::StopMarket)
5344 .trader_id(TraderId::from("TRADER-001"))
5345 .strategy_id(StrategyId::from("TEST-001"))
5346 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
5347 .client_order_id(ClientOrderId::from("O-20250208-EMULATED-ALGO-MODIFY-001"))
5348 .side(OrderSide::Buy)
5349 .trigger_price(Price::from("51000.0"))
5350 .quantity(Quantity::from(100_000))
5351 .emulation_trigger(TriggerType::BidAsk)
5352 .exec_algorithm_id(ExecAlgorithmId::from("TWAP"))
5353 .exec_spawn_id(ClientOrderId::from("O-20250208-EMULATED-ALGO-MODIFY-001"))
5354 .build();
5355 order
5356 .apply(OrderEventAny::Emulated(
5357 OrderEmulatedSpec::builder()
5358 .trader_id(order.trader_id())
5359 .strategy_id(order.strategy_id())
5360 .instrument_id(order.instrument_id())
5361 .client_order_id(order.client_order_id())
5362 .build(),
5363 ))
5364 .unwrap();
5365
5366 assert!(order.is_emulated());
5369 assert!(order.is_active_local());
5370 assert!(order.exec_algorithm_id().is_some());
5371
5372 add_order_to_cache(&strategy, &order);
5373
5374 strategy
5375 .modify_order(
5376 order.client_order_id(),
5377 None,
5378 None,
5379 Some(Price::from("52000.0")),
5380 None,
5381 None,
5382 )
5383 .unwrap();
5384
5385 let emulator_messages = emulator_messages.get_messages();
5386 assert_eq!(emulator_messages.len(), 1);
5387 assert!(matches!(
5388 emulator_messages.first(),
5389 Some(TradingCommand::ModifyOrder(command))
5390 if command.client_order_id == order.client_order_id()
5391 ));
5392 assert!(algo_messages.get_messages().is_empty());
5393 assert!(risk_messages.get_messages().is_empty());
5394 }
5395
5396 #[rstest]
5397 fn test_modify_order_marks_order_pending_update_locally_before_send() {
5398 let mut strategy = create_test_strategy();
5399 register_strategy(&mut strategy);
5400
5401 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
5402 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
5403 msgbus::register_trading_command_endpoint(
5404 MessagingSwitchboard::risk_engine_queue_execute(),
5405 risk_handler,
5406 );
5407
5408 let (event_handler, event_messages): (_, TypedMessageSavingHandler<OrderEventAny>) =
5409 get_typed_message_saving_handler(Some(Ustr::from("events.order.pending_update")));
5410 let order = make_accepted_limit_order("O-20250208-UPDATE-001");
5411 let topic = format!("events.order.{}", order.strategy_id());
5412 msgbus::subscribe_order_events(topic.clone().into(), event_handler.clone(), None);
5413 add_order_to_cache(&strategy, &order);
5414
5415 strategy
5416 .modify_order(
5417 order.client_order_id(),
5418 None,
5419 Some(Price::from("51000.0")),
5420 None,
5421 None,
5422 None,
5423 )
5424 .unwrap();
5425
5426 msgbus::unsubscribe_order_events(topic.into(), &event_handler);
5427
5428 let cache = strategy.cache();
5429 let cached_order = cache.order(&order.client_order_id()).unwrap();
5430 assert_eq!(cached_order.status(), OrderStatus::PendingUpdate);
5431
5432 let risk_messages = risk_messages.get_messages();
5433 assert_eq!(risk_messages.len(), 1);
5434 assert!(matches!(
5435 risk_messages.first(),
5436 Some(TradingCommand::ModifyOrder(_))
5437 ));
5438
5439 let event_messages = event_messages.get_messages();
5440 assert_eq!(event_messages.len(), 1);
5441 assert!(matches!(
5442 event_messages.first(),
5443 Some(OrderEventAny::PendingUpdate(_))
5444 ));
5445 }
5446
5447 #[rstest]
5448 fn test_late_modify_rejection_reaches_running_strategy_before_acceptance() {
5449 let mut strategy = ModifyRejectionStrategy {
5450 core: StrategyCore::new(StrategyConfig {
5451 strategy_id: Some(StrategyId::from("TEST-001")),
5452 ..Default::default()
5453 }),
5454 rejections: Vec::new(),
5455 };
5456
5457 let clock = Rc::new(RefCell::new(VirtualClock::new()));
5458 let cache = Rc::new(RefCell::new(Cache::default()));
5459
5460 let portfolio = Rc::new(RefCell::new(Portfolio::new(
5461 clock.clone(),
5462 cache.clone(),
5463 None,
5464 )));
5465 strategy
5466 .core
5467 .register(
5468 TraderId::from("TRADER-001"),
5469 clock.clone(),
5470 cache.clone(),
5471 portfolio,
5472 )
5473 .unwrap();
5474 strategy.initialize().unwrap();
5475 strategy.start().unwrap();
5476 let mut engine = ExecutionEngine::new(clock, cache.clone(), None);
5477 let order = OrderTestBuilder::new(OrderType::Limit)
5478 .trader_id(TraderId::from("TRADER-001"))
5479 .strategy_id(StrategyId::from("TEST-001"))
5480 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
5481 .client_order_id(ClientOrderId::from("O-LATE-MODIFY-001"))
5482 .side(OrderSide::Buy)
5483 .quantity(Quantity::from(10))
5484 .price(Price::from("50000.00"))
5485 .build();
5486 cache
5487 .borrow_mut()
5488 .add_order(order.clone(), None, None, false)
5489 .unwrap();
5490 let account_id = AccountId::from("ACC-001");
5491 engine.process(&TestOrderEventStubs::submitted(&order, account_id));
5492 assert_eq!(
5493 cache
5494 .borrow()
5495 .order(&order.client_order_id())
5496 .unwrap()
5497 .status(),
5498 OrderStatus::Submitted
5499 );
5500
5501 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
5502 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
5503 msgbus::register_trading_command_endpoint(
5504 MessagingSwitchboard::risk_engine_queue_execute(),
5505 risk_handler,
5506 );
5507
5508 for (quantity, price) in [(30, "52000.00"), (20, "51000.00")] {
5509 strategy
5510 .modify_order(
5511 order.client_order_id(),
5512 Some(Quantity::from(quantity)),
5513 Some(Price::from(price)),
5514 None,
5515 None,
5516 None,
5517 )
5518 .unwrap();
5519 }
5520
5521 let commands = risk_messages.get_messages();
5522 assert_eq!(commands.len(), 2);
5523
5524 let causes: Vec<_> = commands
5525 .iter()
5526 .map(|command| {
5527 let TradingCommand::ModifyOrder(command) = command else {
5528 panic!("expected modify command");
5529 };
5530
5531 command.command_id
5532 })
5533 .collect();
5534
5535 for (command, quantity, price) in [
5536 (&commands[0], 30, "52000.00"),
5537 (&commands[1], 20, "51000.00"),
5538 ] {
5539 let TradingCommand::ModifyOrder(command) = command else {
5540 panic!("Expected ModifyOrder command");
5541 };
5542
5543 assert_eq!(command.client_order_id, order.client_order_id());
5544 assert_eq!(command.venue_order_id, None);
5545 assert_eq!(command.quantity, Some(Quantity::from(quantity)));
5546 assert_eq!(command.price, Some(Price::from(price)));
5547 }
5548
5549 assert_eq!(
5550 cache
5551 .borrow()
5552 .order(&order.client_order_id())
5553 .unwrap()
5554 .status(),
5555 OrderStatus::PendingUpdate
5556 );
5557 let strategy = Rc::new(RefCell::new(strategy));
5558
5559 let handler = TypedHandler::from({
5560 let strategy = strategy.clone();
5561 move |event: &OrderEventAny| strategy.borrow_mut().handle_order_event(event.clone())
5562 });
5563
5564 let topic = msgbus::switchboard::get_event_order_topic(order.strategy_id());
5565 msgbus::subscribe_order_events(topic.into(), handler.clone(), None);
5566 let mut updated = OrderUpdatedSpec::builder()
5567 .trader_id(order.trader_id())
5568 .strategy_id(order.strategy_id())
5569 .instrument_id(order.instrument_id())
5570 .client_order_id(order.client_order_id())
5571 .venue_order_id(VenueOrderId::from("V-AMENDED-001"))
5572 .account_id(account_id)
5573 .quantity(Quantity::from(20))
5574 .price(Price::from("51000.00"))
5575 .build();
5576 updated.causation_id = Some(causes[1]);
5577 engine.process(&OrderEventAny::Updated(updated));
5578 assert_eq!(
5579 cache
5580 .borrow()
5581 .order(&order.client_order_id())
5582 .unwrap()
5583 .status(),
5584 OrderStatus::PendingUpdate
5585 );
5586 let mut rejection = OrderModifyRejectedSpec::builder()
5587 .trader_id(order.trader_id())
5588 .strategy_id(order.strategy_id())
5589 .instrument_id(order.instrument_id())
5590 .client_order_id(order.client_order_id())
5591 .account_id(account_id)
5592 .reason(Ustr::from("First amend rejected"))
5593 .event_id(UUID4::new())
5594 .ts_event(UnixNanos::from(123))
5595 .ts_init(UnixNanos::from(456))
5596 .build();
5597 rejection.causation_id = Some(causes[0]);
5598 engine.process(&OrderEventAny::ModifyRejected(rejection));
5599 msgbus::unsubscribe_order_events(topic.into(), &handler);
5600
5601 let strategy = strategy.borrow();
5602 assert_eq!(strategy.state(), ComponentState::Running);
5603 assert_eq!(strategy.rejections.len(), 1);
5604 let (received, cached) = &strategy.rejections[0];
5605 assert_eq!(*received, rejection);
5606 assert_eq!(cached.status(), OrderStatus::Submitted);
5607 assert_eq!(cached.previous_status(), Some(OrderStatus::Submitted));
5608 assert_eq!(cached.quantity(), Quantity::from(20));
5609 assert_eq!(cached.price(), Some(Price::from("51000.00")));
5610 assert_eq!(cached.filled_qty(), Quantity::from(0));
5611 assert_eq!(cached.leaves_qty(), Quantity::from(20));
5612 assert_eq!(
5613 cached.venue_order_id(),
5614 Some(VenueOrderId::from("V-AMENDED-001"))
5615 );
5616 assert_eq!(cached.events().len(), 6);
5617
5618 for (event, cause) in cached.events()[2..4].iter().zip(causes) {
5619 let OrderEventAny::PendingUpdate(event) = event else {
5620 panic!("expected command marker");
5621 };
5622
5623 assert_eq!(event.causation_id, Some(cause));
5624 }
5625
5626 assert_eq!(
5627 cached.last_event(),
5628 &OrderEventAny::ModifyRejected(rejection)
5629 );
5630 }
5631
5632 #[rstest]
5633 fn test_modify_orders_marks_orders_pending_update_locally_before_send() {
5634 let mut strategy = create_test_strategy();
5635 register_strategy(&mut strategy);
5636
5637 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
5638 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
5639 msgbus::register_trading_command_endpoint(
5640 MessagingSwitchboard::risk_engine_queue_execute(),
5641 risk_handler,
5642 );
5643
5644 let (event_handler, event_messages): (_, TypedMessageSavingHandler<OrderEventAny>) =
5645 get_typed_message_saving_handler(Some(Ustr::from("events.order.batch_pending_update")));
5646 let order1 = make_accepted_limit_order("O-20250208-BATCH-UPDATE-001");
5647 let order2 = make_accepted_limit_order("O-20250208-BATCH-UPDATE-002");
5648 let topic = format!("events.order.{}", order1.strategy_id());
5649 msgbus::subscribe_order_events(topic.clone().into(), event_handler.clone(), None);
5650 add_order_to_cache(&strategy, &order1);
5651 add_order_to_cache(&strategy, &order2);
5652
5653 strategy
5654 .modify_orders(
5655 vec![
5656 (
5657 order1.client_order_id(),
5658 None,
5659 Some(Price::from("51000.0")),
5660 None,
5661 ),
5662 (
5663 order2.client_order_id(),
5664 Some(Quantity::from("2.0")),
5665 None,
5666 None,
5667 ),
5668 ],
5669 None,
5670 None,
5671 )
5672 .unwrap();
5673
5674 msgbus::unsubscribe_order_events(topic.into(), &event_handler);
5675
5676 let cache = strategy.cache();
5677 let cached_order1 = cache.order(&order1.client_order_id()).unwrap();
5678 let cached_order2 = cache.order(&order2.client_order_id()).unwrap();
5679 assert_eq!(cached_order1.status(), OrderStatus::PendingUpdate);
5680 assert_eq!(cached_order2.status(), OrderStatus::PendingUpdate);
5681
5682 let risk_messages = risk_messages.get_messages();
5683 assert_eq!(risk_messages.len(), 1);
5684 let Some(TradingCommand::ModifyOrders(command)) = risk_messages.first() else {
5685 panic!("expected BatchModifyOrders command");
5686 };
5687 assert_eq!(command.modifies.len(), 2);
5688 assert_eq!(
5689 command
5690 .modifies
5691 .iter()
5692 .map(|modify| modify.client_order_id)
5693 .collect::<Vec<_>>(),
5694 vec![order1.client_order_id(), order2.client_order_id()]
5695 );
5696
5697 let event_messages = event_messages.get_messages();
5698 assert_eq!(event_messages.len(), 2);
5699 assert!(
5700 event_messages
5701 .iter()
5702 .all(|event| matches!(event, OrderEventAny::PendingUpdate(_)))
5703 );
5704 }
5705
5706 #[rstest]
5707 fn test_cancel_order_marks_order_pending_cancel_locally_before_send() {
5708 let mut strategy = create_test_strategy();
5709 register_strategy(&mut strategy);
5710
5711 let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
5712 get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
5713 msgbus::register_trading_command_endpoint(
5714 MessagingSwitchboard::exec_engine_queue_execute(),
5715 exec_handler,
5716 );
5717
5718 let (event_handler, event_messages): (_, TypedMessageSavingHandler<OrderEventAny>) =
5719 get_typed_message_saving_handler(Some(Ustr::from("events.order.pending_cancel")));
5720 let order = make_accepted_market_order("O-20250208-CANCEL-001");
5721 let topic = format!("events.order.{}", order.strategy_id());
5722 msgbus::subscribe_order_events(topic.clone().into(), event_handler.clone(), None);
5723 add_order_to_cache(&strategy, &order);
5724
5725 strategy
5726 .cancel_order(order.client_order_id(), None, None)
5727 .unwrap();
5728
5729 msgbus::unsubscribe_order_events(topic.into(), &event_handler);
5730
5731 let cache = strategy.cache();
5732 let cached_order = cache.order(&order.client_order_id()).unwrap();
5733 assert_eq!(cached_order.status(), OrderStatus::PendingCancel);
5734 let cache = strategy.core.cache_ref();
5735 assert!(cache.is_order_pending_cancel_local(&order.client_order_id()));
5736
5737 let exec_messages = exec_messages.get_messages();
5738 assert_eq!(exec_messages.len(), 1);
5739 assert!(matches!(
5740 exec_messages.first(),
5741 Some(TradingCommand::CancelOrder(_))
5742 ));
5743
5744 let event_messages = event_messages.get_messages();
5745 assert_eq!(event_messages.len(), 1);
5746 assert!(matches!(
5747 event_messages.first(),
5748 Some(OrderEventAny::PendingCancel(_))
5749 ));
5750 }
5751
5752 #[rstest]
5753 fn test_cancel_all_orders_strategy_only_sends_only_caller_strategy_cancels() {
5754 let mut strategy = create_test_strategy();
5755 register_strategy(&mut strategy);
5756
5757 let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
5758 get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
5759 msgbus::register_trading_command_endpoint(
5760 MessagingSwitchboard::exec_engine_queue_execute(),
5761 exec_handler,
5762 );
5763
5764 let order = make_accepted_market_order("O-20250208-CANCEL-ALL-001");
5765 let mut sibling_order = OrderTestBuilder::new(OrderType::Market)
5766 .trader_id(TraderId::from("TRADER-001"))
5767 .strategy_id(StrategyId::from("SIBLING-001"))
5768 .instrument_id(order.instrument_id())
5769 .client_order_id(ClientOrderId::from("O-20250208-CANCEL-ALL-002"))
5770 .side(OrderSide::Buy)
5771 .quantity(Quantity::from(100_000))
5772 .build();
5773 let account_id = AccountId::from("ACC-001");
5774 sibling_order
5775 .apply(TestOrderEventStubs::submitted(&sibling_order, account_id))
5776 .unwrap();
5777 sibling_order
5778 .apply(TestOrderEventStubs::accepted(
5779 &sibling_order,
5780 account_id,
5781 VenueOrderId::from("2"),
5782 ))
5783 .unwrap();
5784 add_order_to_cache(&strategy, &order);
5785 add_order_to_cache(&strategy, &sibling_order);
5786 strategy.core.cache_rc().borrow_mut().build_index();
5787
5788 strategy
5789 .cancel_all_orders(order.instrument_id(), None, None, true, None)
5790 .unwrap();
5791
5792 let messages = exec_messages.get_messages();
5793 let cache = strategy.cache();
5794 let cached_order = cache.order(&order.client_order_id()).unwrap();
5795 let cached_sibling = cache.order(&sibling_order.client_order_id()).unwrap();
5796 assert_eq!(messages.len(), 1);
5797 assert!(matches!(
5798 messages.first(),
5799 Some(TradingCommand::CancelOrder(command))
5800 if command.client_order_id == order.client_order_id()
5801 ));
5802 assert_eq!(cached_order.status(), OrderStatus::PendingCancel);
5803 assert_eq!(cached_sibling.status(), OrderStatus::Accepted);
5804 }
5805
5806 #[rstest]
5807 fn test_cancel_all_orders_strategy_only_deduplicates_emulated_inflight_order() {
5808 let mut strategy = create_test_strategy();
5809 register_strategy(&mut strategy);
5810
5811 let (emulator_handler, emulator_messages): (
5812 _,
5813 TypedIntoMessageSavingHandler<TradingCommand>,
5814 ) = get_typed_into_message_saving_handler(Some(Ustr::from("OrderEmulator.execute")));
5815 msgbus::register_trading_command_endpoint(
5816 MessagingSwitchboard::order_emulator_execute(),
5817 emulator_handler,
5818 );
5819 let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
5820 get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
5821 msgbus::register_trading_command_endpoint(
5822 MessagingSwitchboard::exec_engine_queue_execute(),
5823 exec_handler,
5824 );
5825
5826 let client_order_id = ClientOrderId::from("O-20250208-CANCEL-ALL-EMULATED-001");
5827 let order = OrderTestBuilder::new(OrderType::StopMarket)
5828 .trader_id(TraderId::from("TRADER-001"))
5829 .strategy_id(StrategyId::from("TEST-001"))
5830 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
5831 .client_order_id(client_order_id)
5832 .side(OrderSide::Buy)
5833 .trigger_price(Price::from("51000.0"))
5834 .quantity(Quantity::from(100_000))
5835 .emulation_trigger(TriggerType::BidAsk)
5836 .exec_algorithm_id(ExecAlgorithmId::from("ALGO-001"))
5837 .exec_spawn_id(client_order_id)
5838 .build();
5839 add_order_to_cache(&strategy, &order);
5840 strategy.core.cache_rc().borrow_mut().build_index();
5841
5842 strategy
5843 .cancel_all_orders(order.instrument_id(), None, None, true, None)
5844 .unwrap();
5845
5846 let emulator_messages = emulator_messages.get_messages();
5847 assert_eq!(emulator_messages.len(), 1);
5848 assert!(matches!(
5849 emulator_messages.first(),
5850 Some(TradingCommand::CancelOrder(command))
5851 if command.client_order_id == order.client_order_id()
5852 ));
5853 assert!(exec_messages.get_messages().is_empty());
5854 }
5855
5856 #[rstest]
5857 fn test_cancel_all_orders_without_strategy_only_sends_cancel_all_command() {
5858 let mut strategy = create_test_strategy();
5859 register_strategy(&mut strategy);
5860
5861 let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
5862 get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
5863 msgbus::register_trading_command_endpoint(
5864 MessagingSwitchboard::exec_engine_queue_execute(),
5865 exec_handler,
5866 );
5867
5868 let instrument_id = InstrumentId::from("BTCUSDT.BINANCE");
5869
5870 strategy
5871 .cancel_all_orders(instrument_id, None, None, false, None)
5872 .unwrap();
5873
5874 let messages = exec_messages.get_messages();
5875 assert_eq!(messages.len(), 1);
5876 let Some(TradingCommand::CancelAllOrders(command)) = messages.first() else {
5877 panic!("Expected a CancelAllOrders command, was {messages:?}");
5878 };
5879 assert_eq!(command.strategy_id, StrategyId::from("TEST-001"));
5880 assert_eq!(command.instrument_id, instrument_id);
5881 assert_eq!(command.client_id, None);
5882 assert_eq!(command.order_side, None);
5883 assert_eq!(command.correlation_id, Some(command.command_id));
5884 assert_eq!(command.causation_id, None);
5885 }
5886
5887 #[rstest]
5888 fn test_cancel_all_orders_without_strategy_only_delegates_local_routing_with_params() {
5889 let mut strategy = create_test_strategy();
5890 register_strategy(&mut strategy);
5891
5892 let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
5893 get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
5894 msgbus::register_trading_command_endpoint(
5895 MessagingSwitchboard::exec_engine_queue_execute(),
5896 exec_handler,
5897 );
5898 let (emulator_handler, emulator_messages): (
5899 _,
5900 TypedIntoMessageSavingHandler<TradingCommand>,
5901 ) = get_typed_into_message_saving_handler(Some(Ustr::from("OrderEmulator.execute")));
5902 msgbus::register_trading_command_endpoint(
5903 MessagingSwitchboard::order_emulator_execute(),
5904 emulator_handler,
5905 );
5906 let exec_algorithm_id = ExecAlgorithmId::from("ALGO-001");
5907 let algorithm_endpoint = format!("{exec_algorithm_id}.execute");
5908 let (algorithm_handler, algorithm_messages) =
5909 get_any_saving_handler::<TradingCommand>(Some(Ustr::from("ALGO-001.execute")));
5910 msgbus::register_any(algorithm_endpoint.into(), algorithm_handler);
5911
5912 let instrument_id = InstrumentId::from("BTCUSDT.BINANCE");
5913 let sibling_strategy_id = StrategyId::from("SIBLING-001");
5914 let selected_client = ClientId::from("CLIENT-001");
5915 let account_id = AccountId::from("ACC-001");
5916
5917 let mut open_order = OrderTestBuilder::new(OrderType::Limit)
5918 .strategy_id(sibling_strategy_id)
5919 .instrument_id(instrument_id)
5920 .client_order_id(ClientOrderId::from("O-BROAD-OPEN-001"))
5921 .side(OrderSide::Buy)
5922 .price(Price::from("49000.0"))
5923 .quantity(Quantity::from(100_000))
5924 .build();
5925 open_order
5926 .apply(TestOrderEventStubs::submitted(&open_order, account_id))
5927 .unwrap();
5928 open_order
5929 .apply(TestOrderEventStubs::accepted(
5930 &open_order,
5931 account_id,
5932 VenueOrderId::from("V-BROAD-OPEN-001"),
5933 ))
5934 .unwrap();
5935 let emulated_order = OrderTestBuilder::new(OrderType::StopMarket)
5936 .strategy_id(sibling_strategy_id)
5937 .instrument_id(instrument_id)
5938 .client_order_id(ClientOrderId::from("O-BROAD-EMULATED-001"))
5939 .side(OrderSide::Buy)
5940 .trigger_price(Price::from("51000.0"))
5941 .quantity(Quantity::from(100_000))
5942 .emulation_trigger(TriggerType::BidAsk)
5943 .build();
5944 let algorithm_order_id = ClientOrderId::from("O-BROAD-ALGO-001");
5945 let algorithm_order = OrderTestBuilder::new(OrderType::Market)
5946 .strategy_id(sibling_strategy_id)
5947 .instrument_id(instrument_id)
5948 .client_order_id(algorithm_order_id)
5949 .side(OrderSide::Buy)
5950 .quantity(Quantity::from(100_000))
5951 .exec_algorithm_id(exec_algorithm_id)
5952 .exec_spawn_id(algorithm_order_id)
5953 .build();
5954
5955 {
5956 let cache_rc = strategy.core.cache_rc();
5957 let mut cache = cache_rc.borrow_mut();
5958 for order in [&open_order, &emulated_order, &algorithm_order] {
5959 cache
5960 .add_order(order.clone(), None, Some(selected_client), true)
5961 .unwrap();
5962 }
5963 cache.build_index();
5964 }
5965
5966 let params = Params::new(
5967 [(
5968 "routing_hint".to_string(),
5969 Value::String("broad_cancel".to_string()),
5970 )]
5971 .into(),
5972 );
5973 strategy
5974 .cancel_all_orders(
5975 instrument_id,
5976 Some(OrderSide::Buy),
5977 Some(selected_client),
5978 false,
5979 Some(params.clone()),
5980 )
5981 .unwrap();
5982
5983 let exec_messages = exec_messages.get_messages();
5984 let Some(TradingCommand::CancelAllOrders(exec_command)) = exec_messages.first() else {
5985 panic!("Expected an execution CancelAllOrders command, was {exec_messages:?}");
5986 };
5987
5988 assert_eq!(exec_messages.len(), 1);
5989 assert!(emulator_messages.get_messages().is_empty());
5990 assert!(algorithm_messages.get_messages().is_empty());
5991 assert_eq!(exec_command.client_id, Some(selected_client));
5992 assert_eq!(exec_command.strategy_id, StrategyId::from("TEST-001"));
5993 assert_eq!(exec_command.instrument_id, instrument_id);
5994 assert_eq!(exec_command.order_side, Some(OrderSide::Buy));
5995 assert_eq!(exec_command.params.as_ref(), Some(¶ms));
5996 assert_eq!(exec_command.correlation_id, Some(exec_command.command_id));
5997 assert_eq!(exec_command.causation_id, None);
5998 }
5999
6000 #[rstest]
6001 fn test_cancel_all_orders_strategy_only_filters_by_client() {
6002 let mut strategy = create_test_strategy();
6003 register_strategy(&mut strategy);
6004
6005 let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6006 get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
6007 msgbus::register_trading_command_endpoint(
6008 MessagingSwitchboard::exec_engine_queue_execute(),
6009 exec_handler,
6010 );
6011
6012 let selected_client = ClientId::from("CLIENT-001");
6013 let other_client = ClientId::from("CLIENT-002");
6014 let selected_order = make_accepted_market_order("O-20250208-CANCEL-CLIENT-001");
6015 let other_order = make_accepted_market_order("O-20250208-CANCEL-CLIENT-002");
6016 let cache_rc = strategy.core.cache_rc();
6017 {
6018 let mut cache = cache_rc.borrow_mut();
6019 cache
6020 .add_order(selected_order.clone(), None, Some(selected_client), true)
6021 .unwrap();
6022 cache
6023 .add_order(other_order.clone(), None, Some(other_client), true)
6024 .unwrap();
6025 cache.build_index();
6026 }
6027
6028 strategy
6029 .cancel_all_orders(
6030 selected_order.instrument_id(),
6031 None,
6032 Some(selected_client),
6033 true,
6034 None,
6035 )
6036 .unwrap();
6037
6038 let messages = exec_messages.get_messages();
6039 let cache = strategy.cache();
6040 let cached_selected = cache.order(&selected_order.client_order_id()).unwrap();
6041 let cached_other = cache.order(&other_order.client_order_id()).unwrap();
6042 assert_eq!(messages.len(), 1);
6043 assert!(matches!(
6044 messages.first(),
6045 Some(TradingCommand::CancelOrder(command))
6046 if command.client_id == Some(selected_client)
6047 && command.client_order_id == selected_order.client_order_id()
6048 ));
6049 assert_eq!(cached_selected.status(), OrderStatus::PendingCancel);
6050 assert_eq!(cached_other.status(), OrderStatus::Accepted);
6051 }
6052
6053 #[rstest]
6054 fn test_cancel_all_orders_strategy_only_filters_by_side_and_preserves_params() {
6055 let mut strategy = create_test_strategy();
6056 register_strategy(&mut strategy);
6057
6058 let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6059 get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
6060 msgbus::register_trading_command_endpoint(
6061 MessagingSwitchboard::exec_engine_queue_execute(),
6062 exec_handler,
6063 );
6064
6065 let buy_order = make_accepted_market_order("O-20250208-CANCEL-SIDE-001");
6066 let mut sell_order = OrderTestBuilder::new(OrderType::Market)
6067 .trader_id(TraderId::from("TRADER-001"))
6068 .strategy_id(StrategyId::from("TEST-001"))
6069 .instrument_id(buy_order.instrument_id())
6070 .client_order_id(ClientOrderId::from("O-20250208-CANCEL-SIDE-002"))
6071 .side(OrderSide::Sell)
6072 .quantity(Quantity::from(100_000))
6073 .build();
6074 let account_id = AccountId::from("ACC-001");
6075 sell_order
6076 .apply(TestOrderEventStubs::submitted(&sell_order, account_id))
6077 .unwrap();
6078 sell_order
6079 .apply(TestOrderEventStubs::accepted(
6080 &sell_order,
6081 account_id,
6082 VenueOrderId::from("O-20250208-CANCEL-SIDE-002"),
6083 ))
6084 .unwrap();
6085 add_order_to_cache(&strategy, &buy_order);
6086 add_order_to_cache(&strategy, &sell_order);
6087 strategy.core.cache_rc().borrow_mut().build_index();
6088
6089 let params = Params::new(
6090 [(
6091 "routing_hint".to_string(),
6092 Value::String("strategy_only".to_string()),
6093 )]
6094 .into(),
6095 );
6096 strategy
6097 .cancel_all_orders(
6098 buy_order.instrument_id(),
6099 Some(OrderSide::Buy),
6100 None,
6101 true,
6102 Some(params.clone()),
6103 )
6104 .unwrap();
6105
6106 let messages = exec_messages.get_messages();
6107 let Some(TradingCommand::CancelOrder(command)) = messages.first() else {
6108 panic!("Expected a CancelOrder command, was {messages:?}");
6109 };
6110 let cache = strategy.cache();
6111 let cached_buy = cache.order(&buy_order.client_order_id()).unwrap();
6112 let cached_sell = cache.order(&sell_order.client_order_id()).unwrap();
6113 assert_eq!(messages.len(), 1);
6114 assert_eq!(command.trader_id, TraderId::from("TRADER-001"));
6115 assert_eq!(command.client_id, None);
6116 assert_eq!(command.strategy_id, StrategyId::from("TEST-001"));
6117 assert_eq!(command.instrument_id, buy_order.instrument_id());
6118 assert_eq!(command.client_order_id, buy_order.client_order_id());
6119 assert_eq!(command.venue_order_id, buy_order.venue_order_id());
6120 assert_eq!(command.params.as_ref(), Some(¶ms));
6121 assert_eq!(cached_buy.status(), OrderStatus::PendingCancel);
6122 assert_eq!(cached_sell.status(), OrderStatus::Accepted);
6123 }
6124
6125 #[rstest]
6126 fn test_cancel_all_orders_strategy_only_continues_after_error_and_returns_error() {
6127 let mut strategy = create_test_strategy();
6128 register_strategy(&mut strategy);
6129
6130 let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6131 get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
6132 msgbus::register_trading_command_endpoint(
6133 MessagingSwitchboard::exec_engine_queue_execute(),
6134 exec_handler,
6135 );
6136
6137 let mut failing_order = make_accepted_market_order("O-20250208-CANCEL-ERROR-001");
6138 let OrderAny::Market(order) = &mut failing_order else {
6139 panic!("Expected a MarketOrder");
6140 };
6141 order.account_id = None;
6142 let succeeding_order = make_accepted_market_order("O-20250208-CANCEL-ERROR-002");
6143 add_order_to_cache(&strategy, &failing_order);
6144 add_order_to_cache(&strategy, &succeeding_order);
6145 strategy.core.cache_rc().borrow_mut().build_index();
6146
6147 let error = strategy
6148 .cancel_all_orders(failing_order.instrument_id(), None, None, true, None)
6149 .unwrap_err()
6150 .to_string();
6151
6152 let messages = exec_messages.get_messages();
6153 let cache = strategy.cache();
6154 let cached_failing = cache.order(&failing_order.client_order_id()).unwrap();
6155 let cached_succeeding = cache.order(&succeeding_order.client_order_id()).unwrap();
6156 assert_eq!(
6157 error,
6158 "Cannot generate pending cancel event for O-20250208-CANCEL-ERROR-001: \
6159 account_id is not set"
6160 );
6161 assert_eq!(messages.len(), 1);
6162 assert!(matches!(
6163 messages.first(),
6164 Some(TradingCommand::CancelOrder(command))
6165 if command.client_order_id == succeeding_order.client_order_id()
6166 ));
6167 assert_eq!(cached_failing.status(), OrderStatus::Accepted);
6168 assert_eq!(cached_succeeding.status(), OrderStatus::PendingCancel);
6169 }
6170
6171 #[rstest]
6172 fn test_cancel_all_orders_strategy_only_cancels_order_submitted_in_same_handler() {
6173 let mut strategy = create_test_strategy();
6174 register_strategy(&mut strategy);
6175
6176 let (risk_handler, _risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6177 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
6178 msgbus::register_trading_command_endpoint(
6179 MessagingSwitchboard::risk_engine_queue_execute(),
6180 risk_handler,
6181 );
6182 let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6183 get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
6184 msgbus::register_trading_command_endpoint(
6185 MessagingSwitchboard::exec_engine_queue_execute(),
6186 exec_handler,
6187 );
6188
6189 let order = OrderTestBuilder::new(OrderType::Limit)
6190 .trader_id(TraderId::from("TRADER-001"))
6191 .strategy_id(StrategyId::from("TEST-001"))
6192 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
6193 .client_order_id(ClientOrderId::from("O-20250208-CANCEL-ALL-LOCAL-001"))
6194 .side(OrderSide::Buy)
6195 .price(Price::from("50000.0"))
6196 .quantity(Quantity::from(100_000))
6197 .build();
6198 let sibling_order = OrderTestBuilder::new(OrderType::Limit)
6199 .trader_id(TraderId::from("TRADER-001"))
6200 .strategy_id(StrategyId::from("SIBLING-001"))
6201 .instrument_id(order.instrument_id())
6202 .client_order_id(ClientOrderId::from("O-20250208-CANCEL-ALL-LOCAL-002"))
6203 .side(OrderSide::Buy)
6204 .price(Price::from("50000.0"))
6205 .quantity(Quantity::from(100_000))
6206 .build();
6207 add_order_to_cache(&strategy, &sibling_order);
6208
6209 strategy
6211 .submit_order(order.clone(), None, None, None)
6212 .unwrap();
6213 strategy
6214 .cancel_all_orders(order.instrument_id(), None, None, true, None)
6215 .unwrap();
6216
6217 let messages = exec_messages.get_messages();
6218 assert_eq!(messages.len(), 1);
6219 assert!(matches!(
6220 messages.first(),
6221 Some(TradingCommand::CancelOrder(command))
6222 if command.client_order_id == order.client_order_id()
6223 ));
6224 }
6225
6226 #[rstest]
6227 fn test_cancel_orders_marks_orders_pending_cancel_locally_before_send() {
6228 let mut strategy = create_test_strategy();
6229 register_strategy(&mut strategy);
6230
6231 let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6232 get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
6233 msgbus::register_trading_command_endpoint(
6234 MessagingSwitchboard::exec_engine_queue_execute(),
6235 exec_handler,
6236 );
6237
6238 let (event_handler, event_messages): (_, TypedMessageSavingHandler<OrderEventAny>) =
6239 get_typed_message_saving_handler(Some(Ustr::from("events.order.batch_pending_cancel")));
6240 let order1 = make_accepted_market_order("O-20250208-CANCEL-001");
6241 let order2 = make_accepted_market_order("O-20250208-CANCEL-002");
6242 let topic = format!("events.order.{}", order1.strategy_id());
6243 msgbus::subscribe_order_events(topic.clone().into(), event_handler.clone(), None);
6244 add_order_to_cache(&strategy, &order1);
6245 add_order_to_cache(&strategy, &order2);
6246
6247 strategy
6248 .cancel_orders(
6249 vec![order1.client_order_id(), order2.client_order_id()],
6250 None,
6251 None,
6252 )
6253 .unwrap();
6254
6255 msgbus::unsubscribe_order_events(topic.into(), &event_handler);
6256
6257 let cache = strategy.cache();
6258 let cached_order1 = cache.order(&order1.client_order_id()).unwrap();
6259 let cached_order2 = cache.order(&order2.client_order_id()).unwrap();
6260 assert_eq!(cached_order1.status(), OrderStatus::PendingCancel);
6261 assert_eq!(cached_order2.status(), OrderStatus::PendingCancel);
6262 let cache = strategy.core.cache_ref();
6263 assert!(cache.is_order_pending_cancel_local(&order1.client_order_id()));
6264 assert!(cache.is_order_pending_cancel_local(&order2.client_order_id()));
6265
6266 let exec_messages = exec_messages.get_messages();
6267 assert_eq!(exec_messages.len(), 1);
6268 let Some(TradingCommand::CancelOrders(command)) = exec_messages.first() else {
6269 panic!("expected BatchCancelOrders command");
6270 };
6271 assert_eq!(command.cancels.len(), 2);
6272
6273 let event_messages = event_messages.get_messages();
6274 assert_eq!(event_messages.len(), 2);
6275 assert!(
6276 event_messages
6277 .iter()
6278 .all(|event| matches!(event, OrderEventAny::PendingCancel(_)))
6279 );
6280 }
6281
6282 #[rstest]
6283 fn test_cancel_order_updates_own_book_status_before_send() {
6284 let mut strategy = create_test_strategy();
6285 register_strategy(&mut strategy);
6286
6287 let (exec_handler, _exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6288 get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
6289 msgbus::register_trading_command_endpoint(
6290 MessagingSwitchboard::exec_engine_queue_execute(),
6291 exec_handler,
6292 );
6293
6294 let order = make_accepted_limit_order("O-20250208-CANCEL-OWN-BOOK-001");
6295 add_order_to_cache_and_own_book(&strategy, &order);
6296
6297 strategy
6298 .cancel_order(order.client_order_id(), None, None)
6299 .unwrap();
6300
6301 let mut accepted = AHashSet::new();
6302 accepted.insert(OrderStatus::Accepted);
6303 let mut pending_cancel = AHashSet::new();
6304 pending_cancel.insert(OrderStatus::PendingCancel);
6305
6306 let cache = strategy.cache();
6307 let own_book = cache.own_order_book(&order.instrument_id()).unwrap();
6308 assert!(own_book.bids_as_map(Some(&accepted), None, None).is_empty());
6309 let pending_bids = own_book.bids_as_map(Some(&pending_cancel), None, None);
6310 assert_eq!(pending_bids.values().map(Vec::len).sum::<usize>(), 1);
6311 }
6312
6313 #[rstest]
6314 fn test_cancel_order_returns_error_when_not_in_cache() {
6315 let mut strategy = create_test_strategy();
6316 register_strategy(&mut strategy);
6317
6318 let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6319 get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
6320 msgbus::register_trading_command_endpoint(
6321 MessagingSwitchboard::exec_engine_queue_execute(),
6322 exec_handler,
6323 );
6324
6325 let missing_id = ClientOrderId::from("O-MISSING");
6326 let err = strategy
6327 .cancel_order(missing_id, None, None)
6328 .expect_err("expected cancel_order to fail when order is not in cache");
6329
6330 assert_eq!(
6331 err.to_string(),
6332 format!("Cannot cancel order: {ORDER_NOT_FOUND}: {missing_id}")
6333 );
6334 assert!(exec_messages.get_messages().is_empty());
6335 }
6336
6337 #[rstest]
6338 fn test_modify_order_returns_error_when_not_in_cache() {
6339 let mut strategy = create_test_strategy();
6340 register_strategy(&mut strategy);
6341
6342 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6343 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
6344 msgbus::register_trading_command_endpoint(
6345 MessagingSwitchboard::risk_engine_queue_execute(),
6346 risk_handler,
6347 );
6348
6349 let missing_id = ClientOrderId::from("O-MISSING");
6350 let err = strategy
6351 .modify_order(missing_id, Some(Quantity::from(1)), None, None, None, None)
6352 .expect_err("expected modify_order to fail when order is not in cache");
6353
6354 assert_eq!(
6355 err.to_string(),
6356 format!("Cannot modify order: {ORDER_NOT_FOUND}: {missing_id}")
6357 );
6358 assert!(risk_messages.get_messages().is_empty());
6359 }
6360
6361 #[rstest]
6362 fn test_modify_orders_returns_error_when_any_id_missing() {
6363 let mut strategy = create_test_strategy();
6364 register_strategy(&mut strategy);
6365
6366 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6367 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
6368 msgbus::register_trading_command_endpoint(
6369 MessagingSwitchboard::risk_engine_queue_execute(),
6370 risk_handler,
6371 );
6372
6373 let order = make_accepted_limit_order("O-PRESENT");
6374 add_order_to_cache(&strategy, &order);
6375
6376 let missing_id = ClientOrderId::from("O-MISSING");
6377 let err = strategy
6378 .modify_orders(
6379 vec![
6380 (
6381 order.client_order_id(),
6382 None,
6383 Some(Price::from("51000.0")),
6384 None,
6385 ),
6386 (missing_id, Some(Quantity::from("2.0")), None, None),
6387 ],
6388 None,
6389 None,
6390 )
6391 .expect_err("expected modify_orders to fail when any id is missing");
6392
6393 assert_eq!(
6394 err.to_string(),
6395 format!("Cannot modify order: {ORDER_NOT_FOUND}: {missing_id}")
6396 );
6397 assert!(risk_messages.get_messages().is_empty());
6398 }
6399
6400 #[rstest]
6401 fn test_cancel_orders_returns_error_when_any_id_missing() {
6402 let mut strategy = create_test_strategy();
6403 register_strategy(&mut strategy);
6404
6405 let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6406 get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
6407 msgbus::register_trading_command_endpoint(
6408 MessagingSwitchboard::exec_engine_queue_execute(),
6409 exec_handler,
6410 );
6411
6412 let order = make_accepted_limit_order("O-PRESENT");
6413 add_order_to_cache(&strategy, &order);
6414
6415 let missing_id = ClientOrderId::from("O-MISSING");
6416 let err = strategy
6417 .cancel_orders(vec![order.client_order_id(), missing_id], None, None)
6418 .expect_err("expected cancel_orders to fail when any id is missing");
6419
6420 assert_eq!(
6421 err.to_string(),
6422 format!("Cannot cancel order: {ORDER_NOT_FOUND}: {missing_id}")
6423 );
6424 assert!(exec_messages.get_messages().is_empty());
6425 }
6426
6427 #[rstest]
6430 fn test_has_gtd_expiry_timer_when_timer_not_set() {
6431 let mut strategy = create_test_strategy();
6432 let client_order_id = ClientOrderId::from("O-001");
6433
6434 assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
6435 }
6436
6437 #[rstest]
6438 fn test_has_gtd_expiry_timer_when_timer_set() {
6439 let mut strategy = create_test_strategy();
6440 let client_order_id = ClientOrderId::from("O-001");
6441
6442 strategy
6443 .core
6444 .gtd_timers
6445 .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
6446
6447 assert!(strategy.has_gtd_expiry_timer(&client_order_id));
6448 }
6449
6450 #[rstest]
6451 fn test_cancel_gtd_expiry_removes_timer() {
6452 let mut strategy = create_test_strategy();
6453 register_strategy(&mut strategy);
6454
6455 let client_order_id = ClientOrderId::from("O-001");
6456 strategy
6457 .core
6458 .gtd_timers
6459 .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
6460
6461 strategy.cancel_gtd_expiry(&client_order_id);
6462
6463 assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
6464 }
6465
6466 #[rstest]
6467 fn test_cancel_gtd_expiry_when_timer_not_set() {
6468 let mut strategy = create_test_strategy();
6469 register_strategy(&mut strategy);
6470
6471 let client_order_id = ClientOrderId::from("O-001");
6472
6473 strategy.cancel_gtd_expiry(&client_order_id);
6474
6475 assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
6476 }
6477
6478 #[rstest]
6479 #[case::matching_order(Ustr::from("GTD-EXPIRY:O-PRESENT"))]
6480 #[case::empty_order_id(Ustr::from("GTD-EXPIRY:"))]
6481 fn test_route_time_event_ignores_unregistered_gtd_timer(#[case] timer_name: Ustr) {
6482 let mut strategy = create_test_strategy();
6483 register_strategy(&mut strategy);
6484
6485 let order = make_accepted_limit_order("O-PRESENT");
6486 let client_order_id = order.client_order_id();
6487 add_order_to_cache(&strategy, &order);
6488 let event = TimeEvent::new(
6489 timer_name,
6490 UUID4::new(),
6491 UnixNanos::default(),
6492 UnixNanos::default(),
6493 );
6494
6495 route_time_event(&mut strategy, &event);
6496
6497 let cache = strategy.core.cache_ref();
6498 let cached_order = cache.order(&client_order_id).unwrap();
6499 assert_eq!(cached_order.status(), OrderStatus::Accepted);
6500 assert!(!strategy.core.gtd_timers.contains_key(&client_order_id));
6501 }
6502
6503 #[rstest]
6504 #[case::filled(make_filled)]
6505 #[case::canceled(make_canceled)]
6506 #[case::rejected(make_rejected)]
6507 #[case::expired(make_expired)]
6508 #[case::fill_voided(make_terminal_fill_voided)]
6509 fn test_handle_order_event_cancels_gtd_timer_for_terminal_event(
6510 #[case] make_event: fn(ClientOrderId) -> OrderEventAny,
6511 ) {
6512 let mut strategy = create_test_strategy();
6513 register_strategy(&mut strategy);
6514 start_strategy(&mut strategy);
6515
6516 let client_order_id = ClientOrderId::from("O-001");
6517 strategy
6518 .core
6519 .gtd_timers
6520 .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
6521
6522 strategy.handle_order_event(make_event(client_order_id));
6523
6524 assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
6525 }
6526
6527 #[rstest]
6528 #[case::partial_fill(make_filled)]
6529 #[case::non_reopened_fill_void(make_terminal_fill_voided)]
6530 fn test_handle_order_event_keeps_gtd_timer_when_cached_order_remains_open(
6531 #[case] make_event: fn(ClientOrderId) -> OrderEventAny,
6532 ) {
6533 let mut strategy = create_test_strategy();
6534 register_strategy(&mut strategy);
6535 start_strategy(&mut strategy);
6536
6537 let client_order_id = ClientOrderId::from("O-001");
6538 let order = make_accepted_limit_order(client_order_id.as_str());
6539 add_order_to_cache(&strategy, &order);
6540 strategy
6541 .core
6542 .gtd_timers
6543 .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
6544
6545 strategy.handle_order_event(make_event(client_order_id));
6546
6547 assert!(strategy.has_gtd_expiry_timer(&client_order_id));
6548 }
6549
6550 #[rstest]
6551 #[case::filled(make_filled)]
6552 #[case::canceled(make_canceled)]
6553 #[case::rejected(make_rejected)]
6554 #[case::expired(make_expired)]
6555 #[case::fill_voided(make_terminal_fill_voided)]
6556 fn test_handle_order_event_cancels_gtd_timer_when_stopped(
6557 #[case] make_event: fn(ClientOrderId) -> OrderEventAny,
6558 ) {
6559 let mut strategy = create_test_strategy();
6560 register_strategy(&mut strategy);
6561 start_strategy(&mut strategy);
6562
6563 let client_order_id = ClientOrderId::from("O-001");
6564 strategy
6565 .core
6566 .gtd_timers
6567 .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
6568
6569 stop_strategy(&mut strategy);
6570 assert_eq!(strategy.state(), ComponentState::Stopped);
6571
6572 strategy.handle_order_event(make_event(client_order_id));
6573
6574 assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
6575 }
6576
6577 #[rstest]
6578 fn test_handle_order_event_skips_gtd_cancel_for_non_terminal() {
6579 let mut strategy = create_test_strategy();
6580 register_strategy(&mut strategy);
6581 start_strategy(&mut strategy);
6582
6583 let client_order_id = ClientOrderId::from("O-001");
6584 strategy
6585 .core
6586 .gtd_timers
6587 .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
6588
6589 strategy.handle_order_event(make_accepted(client_order_id));
6590
6591 assert!(strategy.has_gtd_expiry_timer(&client_order_id));
6592 }
6593
6594 #[rstest]
6595 fn test_handle_reopened_fill_void_keeps_gtd_timer() {
6596 let mut strategy = create_test_strategy();
6597 register_strategy(&mut strategy);
6598 start_strategy(&mut strategy);
6599
6600 let client_order_id = ClientOrderId::from("O-001");
6601 strategy
6602 .core
6603 .gtd_timers
6604 .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
6605
6606 strategy.handle_order_event(make_fill_voided(client_order_id, true));
6607
6608 assert!(strategy.has_gtd_expiry_timer(&client_order_id));
6609 }
6610
6611 #[rstest]
6612 fn test_cancel_denied_after_expiry_does_not_resubmit_cancel() {
6613 let mut strategy = create_gtd_managed_strategy();
6614 let clock = register_gtd_strategy(&mut strategy);
6615 start_strategy(&mut strategy);
6616 clock.borrow_mut().set_time(UnixNanos::from(1_000_000_000));
6617 let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6618 get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
6619 msgbus::register_trading_command_endpoint(
6620 MessagingSwitchboard::exec_engine_queue_execute(),
6621 exec_handler,
6622 );
6623 let order = make_accepted_gtd_limit_order("O-001", UnixNanos::from(1));
6624 let client_order_id = order.client_order_id();
6625 add_order_to_cache(&strategy, &order);
6626 strategy.handle_order_event(make_cancel_denied(client_order_id));
6627 assert!(exec_messages.get_messages().is_empty());
6628 assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
6629 assert_eq!(
6630 strategy
6631 .core
6632 .cache_ref()
6633 .order(&client_order_id)
6634 .unwrap()
6635 .status(),
6636 OrderStatus::Accepted
6637 );
6638 }
6639
6640 #[rstest]
6641 #[case(false)]
6642 #[case(true)]
6643 fn test_handle_cancel_refusal_restores_future_gtd_timer(#[case] denied: bool) {
6644 let mut strategy = create_gtd_managed_strategy();
6645 let clock = register_gtd_strategy(&mut strategy);
6646 start_strategy(&mut strategy);
6647 let expire_time = UnixNanos::from(clock.borrow().timestamp_ns().as_u64() + 1_000_000_000);
6648
6649 let order = make_accepted_gtd_limit_order("O-001", expire_time);
6650 let client_order_id = order.client_order_id();
6651 add_order_to_cache(&strategy, &order);
6652
6653 strategy.handle_order_event(if denied {
6654 make_cancel_denied(client_order_id)
6655 } else {
6656 make_cancel_rejected(client_order_id)
6657 });
6658
6659 assert_eq!(
6660 strategy.core.gtd_timers.get(&client_order_id),
6661 Some(&Ustr::from("GTD-EXPIRY:O-001"))
6662 );
6663 }
6664
6665 #[rstest]
6666 fn test_cancel_rejected_restores_timer_after_cancel_order_removes_it() {
6667 let mut strategy = create_gtd_managed_strategy();
6668 let clock = register_gtd_strategy(&mut strategy);
6669 start_strategy(&mut strategy);
6670 let expire_time = UnixNanos::from(clock.borrow().timestamp_ns().as_u64() + 1_000_000_000);
6671 let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6672 get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
6673 msgbus::register_trading_command_endpoint(
6674 MessagingSwitchboard::exec_engine_queue_execute(),
6675 exec_handler,
6676 );
6677
6678 let order = make_accepted_gtd_limit_order("O-001", expire_time);
6679 let client_order_id = order.client_order_id();
6680 add_order_to_cache(&strategy, &order);
6681 strategy
6682 .core
6683 .gtd_timers
6684 .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
6685
6686 strategy.cancel_order(client_order_id, None, None).unwrap();
6687 assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
6688
6689 let event = make_cancel_rejected(client_order_id);
6690 strategy
6691 .core
6692 .cache_rc()
6693 .borrow_mut()
6694 .update_order(&event)
6695 .unwrap();
6696 assert_eq!(
6697 strategy
6698 .core
6699 .cache_ref()
6700 .order(&client_order_id)
6701 .unwrap()
6702 .status(),
6703 OrderStatus::Accepted
6704 );
6705
6706 strategy.handle_order_event(event);
6707
6708 assert_eq!(
6709 strategy.core.gtd_timers.get(&client_order_id),
6710 Some(&Ustr::from("GTD-EXPIRY:O-001"))
6711 );
6712 assert_eq!(exec_messages.get_messages().len(), 1);
6713 }
6714
6715 #[rstest]
6716 fn test_cancel_rejected_while_submitted_restores_timer_through_acceptance() {
6717 let mut strategy = create_gtd_managed_strategy();
6718 let clock = register_gtd_strategy(&mut strategy);
6719 start_strategy(&mut strategy);
6720 let expire_time = UnixNanos::from(clock.borrow().timestamp_ns().as_u64() + 1_000_000_000);
6721 let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6722 get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
6723 msgbus::register_trading_command_endpoint(
6724 MessagingSwitchboard::exec_engine_queue_execute(),
6725 exec_handler,
6726 );
6727
6728 let order = make_submitted_gtd_limit_order("O-001", expire_time);
6729 let client_order_id = order.client_order_id();
6730 add_order_to_cache(&strategy, &order);
6731 strategy.set_gtd_expiry(&order).unwrap();
6732
6733 strategy.cancel_order(client_order_id, None, None).unwrap();
6734 assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
6735
6736 let cancel_rejected = make_cancel_rejected(client_order_id);
6737 strategy
6738 .core
6739 .cache_rc()
6740 .borrow_mut()
6741 .update_order(&cancel_rejected)
6742 .unwrap();
6743 assert_eq!(
6744 strategy
6745 .core
6746 .cache_ref()
6747 .order(&client_order_id)
6748 .unwrap()
6749 .status(),
6750 OrderStatus::Submitted
6751 );
6752 strategy.handle_order_event(cancel_rejected);
6753 assert_eq!(
6754 strategy.core.gtd_timers.get(&client_order_id),
6755 Some(&Ustr::from("GTD-EXPIRY:O-001"))
6756 );
6757 assert_eq!(strategy.core.gtd_timers.len(), 1);
6758
6759 let accepted = make_accepted(client_order_id);
6760 strategy
6761 .core
6762 .cache_rc()
6763 .borrow_mut()
6764 .update_order(&accepted)
6765 .unwrap();
6766 strategy.handle_order_event(accepted);
6767
6768 assert_eq!(
6769 strategy.core.gtd_timers.get(&client_order_id),
6770 Some(&Ustr::from("GTD-EXPIRY:O-001"))
6771 );
6772 assert_eq!(strategy.core.gtd_timers.len(), 1);
6773 assert_eq!(exec_messages.get_messages().len(), 1);
6774 }
6775
6776 #[rstest]
6777 #[case::future_expiry(false)]
6778 #[case::elapsed_expiry(true)]
6779 fn test_cancel_rejected_after_acceptance_restores_gtd_expiry(#[case] expired: bool) {
6780 let mut strategy = create_gtd_managed_strategy();
6781 let clock = register_gtd_strategy(&mut strategy);
6782 start_strategy(&mut strategy);
6783 let expire_time = UnixNanos::from(1_000_000_000);
6784 let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6785 get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
6786 msgbus::register_trading_command_endpoint(
6787 MessagingSwitchboard::exec_engine_queue_execute(),
6788 exec_handler,
6789 );
6790 let order = make_submitted_gtd_limit_order("O-001", expire_time);
6791 let client_order_id = order.client_order_id();
6792 add_order_to_cache(&strategy, &order);
6793 strategy.set_gtd_expiry(&order).unwrap();
6794
6795 strategy.cancel_order(client_order_id, None, None).unwrap();
6796 let accepted = make_accepted(client_order_id);
6797 strategy
6798 .core
6799 .cache_rc()
6800 .borrow_mut()
6801 .update_order(&accepted)
6802 .unwrap();
6803 strategy.handle_order_event(accepted);
6804 assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
6805 assert_eq!(exec_messages.get_messages().len(), 1);
6806
6807 if expired {
6808 clock.borrow_mut().set_time(expire_time);
6809 }
6810 let cancel_rejected = make_cancel_rejected(client_order_id);
6811 strategy
6812 .core
6813 .cache_rc()
6814 .borrow_mut()
6815 .update_order(&cancel_rejected)
6816 .unwrap();
6817 strategy.handle_order_event(cancel_rejected);
6818
6819 if !expired {
6820 assert_eq!(
6821 strategy.core.gtd_timers.get(&client_order_id),
6822 Some(&Ustr::from("GTD-EXPIRY:O-001"))
6823 );
6824 assert_eq!(
6825 strategy
6826 .core
6827 .cache_ref()
6828 .order(&client_order_id)
6829 .unwrap()
6830 .status(),
6831 OrderStatus::Accepted
6832 );
6833 assert_eq!(exec_messages.get_messages().len(), 1);
6834 let events = clock.borrow_mut().advance_time(expire_time, true);
6835 assert_eq!(events.len(), 1);
6836 route_time_event(&mut strategy, &events[0]);
6837 }
6838
6839 assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
6840 assert_eq!(
6841 strategy
6842 .core
6843 .cache_ref()
6844 .order(&client_order_id)
6845 .unwrap()
6846 .status(),
6847 OrderStatus::PendingCancel
6848 );
6849 let commands = exec_messages.get_messages();
6850 assert_eq!(commands.len(), 2);
6851 for command in commands {
6852 assert!(matches!(
6853 command,
6854 TradingCommand::CancelOrder(command) if command.client_order_id == client_order_id
6855 ));
6856 }
6857 }
6858
6859 #[rstest]
6860 fn test_cancel_rejected_while_submitted_retries_already_expired_gtd_order() {
6861 let mut strategy = create_gtd_managed_strategy();
6862 let clock = register_gtd_strategy(&mut strategy);
6863 start_strategy(&mut strategy);
6864 clock.borrow_mut().set_time(UnixNanos::from(1_000_000_000));
6865 let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6866 get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
6867 msgbus::register_trading_command_endpoint(
6868 MessagingSwitchboard::exec_engine_queue_execute(),
6869 exec_handler,
6870 );
6871
6872 let order = make_submitted_gtd_limit_order("O-001", UnixNanos::from(1));
6873 let client_order_id = order.client_order_id();
6874 add_order_to_cache(&strategy, &order);
6875
6876 strategy.handle_order_event(make_cancel_rejected(client_order_id));
6877
6878 assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
6879 assert!(matches!(
6880 exec_messages.get_messages().as_slice(),
6881 [TradingCommand::CancelOrder(command)]
6882 if command.client_order_id == client_order_id
6883 ));
6884 assert_eq!(
6885 strategy
6886 .core
6887 .cache_ref()
6888 .order(&client_order_id)
6889 .unwrap()
6890 .status(),
6891 OrderStatus::PendingCancel
6892 );
6893 }
6894
6895 #[rstest]
6896 fn test_cancel_rejected_while_submitted_preserves_existing_gtd_timer() {
6897 let mut strategy = create_gtd_managed_strategy();
6898 let clock = register_gtd_strategy(&mut strategy);
6899 start_strategy(&mut strategy);
6900 let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6901 get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
6902 msgbus::register_trading_command_endpoint(
6903 MessagingSwitchboard::exec_engine_queue_execute(),
6904 exec_handler,
6905 );
6906
6907 let order =
6908 make_submitted_gtd_limit_order("O-001", UnixNanos::from(2_000_000_000_000_000_000));
6909 let client_order_id = order.client_order_id();
6910 add_order_to_cache(&strategy, &order);
6911 let timer_name = Ustr::from("GTD-EXPIRY:O-001");
6912 strategy.core.gtd_timers.insert(client_order_id, timer_name);
6913
6914 strategy.handle_order_event(make_cancel_rejected(client_order_id));
6915
6916 assert_eq!(
6917 strategy.core.gtd_timers.get(&client_order_id),
6918 Some(&timer_name)
6919 );
6920 assert_eq!(strategy.core.gtd_timers.len(), 1);
6921 assert!(!clock.borrow().timer_names().contains(&timer_name.as_str()));
6924 assert!(exec_messages.get_messages().is_empty());
6925 }
6926
6927 #[rstest]
6928 fn test_handle_cancel_rejected_retries_already_expired_gtd_order() {
6929 let mut strategy = create_gtd_managed_strategy();
6930 let clock = register_gtd_strategy(&mut strategy);
6931 start_strategy(&mut strategy);
6932 clock.borrow_mut().set_time(UnixNanos::from(1_000_000_000));
6933 let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6934 get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
6935 msgbus::register_trading_command_endpoint(
6936 MessagingSwitchboard::exec_engine_queue_execute(),
6937 exec_handler,
6938 );
6939
6940 let order = make_accepted_gtd_limit_order("O-001", UnixNanos::from(1));
6941 let client_order_id = order.client_order_id();
6942 add_order_to_cache(&strategy, &order);
6943
6944 strategy.handle_order_event(make_cancel_rejected(client_order_id));
6945
6946 assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
6947 assert!(matches!(
6948 exec_messages.get_messages().as_slice(),
6949 [TradingCommand::CancelOrder(command)]
6950 if command.client_order_id == client_order_id
6951 ));
6952 assert_eq!(
6953 strategy
6954 .core
6955 .cache_ref()
6956 .order(&client_order_id)
6957 .unwrap()
6958 .status(),
6959 OrderStatus::PendingCancel
6960 );
6961 }
6962
6963 #[rstest]
6964 fn test_handle_cancel_rejected_preserves_existing_gtd_timer() {
6965 let mut strategy = create_gtd_managed_strategy();
6966 let _clock = register_gtd_strategy(&mut strategy);
6967 start_strategy(&mut strategy);
6968 let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
6969 get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
6970 msgbus::register_trading_command_endpoint(
6971 MessagingSwitchboard::exec_engine_queue_execute(),
6972 exec_handler,
6973 );
6974
6975 let order =
6976 make_accepted_gtd_limit_order("O-001", UnixNanos::from(2_000_000_000_000_000_000));
6977 let client_order_id = order.client_order_id();
6978 add_order_to_cache(&strategy, &order);
6979 strategy
6980 .core
6981 .gtd_timers
6982 .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
6983
6984 strategy.handle_order_event(make_cancel_rejected(client_order_id));
6985
6986 assert_eq!(
6987 strategy.core.gtd_timers.get(&client_order_id),
6988 Some(&Ustr::from("GTD-EXPIRY:O-001"))
6989 );
6990 assert!(exec_messages.get_messages().is_empty());
6991 }
6992
6993 #[rstest]
6994 fn test_handle_cancel_rejected_preserves_existing_timer_for_expired_order() {
6995 let mut strategy = create_gtd_managed_strategy();
6996 let clock = register_gtd_strategy(&mut strategy);
6997 start_strategy(&mut strategy);
6998 clock.borrow_mut().set_time(UnixNanos::from(1_000_000_000));
6999 let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
7000 get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
7001 msgbus::register_trading_command_endpoint(
7002 MessagingSwitchboard::exec_engine_queue_execute(),
7003 exec_handler,
7004 );
7005
7006 let order = make_accepted_gtd_limit_order("O-001", UnixNanos::from(1));
7007 let client_order_id = order.client_order_id();
7008 add_order_to_cache(&strategy, &order);
7009 strategy
7010 .core
7011 .gtd_timers
7012 .insert(client_order_id, Ustr::from("GTD-EXPIRY:O-001"));
7013
7014 strategy.handle_order_event(make_cancel_rejected(client_order_id));
7015
7016 assert_eq!(
7017 strategy.core.gtd_timers.get(&client_order_id),
7018 Some(&Ustr::from("GTD-EXPIRY:O-001"))
7019 );
7020 assert!(exec_messages.get_messages().is_empty());
7021 }
7022
7023 #[rstest]
7024 fn test_restored_gtd_timer_event_cancels_order() {
7025 let mut strategy = create_gtd_managed_strategy();
7026 let clock = register_gtd_strategy(&mut strategy);
7027 start_strategy(&mut strategy);
7028 let expire_time = UnixNanos::from(clock.borrow().timestamp_ns().as_u64() + 1_000_000_000);
7029 let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
7030 get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
7031 msgbus::register_trading_command_endpoint(
7032 MessagingSwitchboard::exec_engine_queue_execute(),
7033 exec_handler,
7034 );
7035
7036 let order = make_accepted_gtd_limit_order("O-001", expire_time);
7037 let client_order_id = order.client_order_id();
7038 add_order_to_cache(&strategy, &order);
7039
7040 strategy.handle_order_event(make_cancel_rejected(client_order_id));
7041 assert!(strategy.has_gtd_expiry_timer(&client_order_id));
7042
7043 let events = clock.borrow_mut().advance_time(expire_time, true);
7044 for event in &events {
7045 route_time_event(&mut strategy, event);
7046 }
7047
7048 assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
7049 assert!(matches!(
7050 exec_messages.get_messages().as_slice(),
7051 [TradingCommand::CancelOrder(command)]
7052 if command.client_order_id == client_order_id
7053 ));
7054 }
7055
7056 enum IneligibleRestoreCase {
7057 MissingOrder,
7058 ClosedOrder,
7059 NonGtdOrder,
7060 ManagementDisabled,
7061 Stopped,
7062 Emulated,
7063 }
7064
7065 fn assert_cancel_rejected_does_not_restore(case: &IneligibleRestoreCase) {
7066 let mut strategy = match case {
7067 IneligibleRestoreCase::ManagementDisabled => create_test_strategy(),
7068 _ => create_gtd_managed_strategy(),
7069 };
7070 let _clock = register_gtd_strategy(&mut strategy);
7071 start_strategy(&mut strategy);
7072 let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
7073 get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
7074 msgbus::register_trading_command_endpoint(
7075 MessagingSwitchboard::exec_engine_queue_execute(),
7076 exec_handler,
7077 );
7078
7079 let client_order_id = ClientOrderId::from("O-001");
7080 let order = match case {
7081 IneligibleRestoreCase::MissingOrder => None,
7082 IneligibleRestoreCase::NonGtdOrder => {
7083 Some(make_accepted_limit_order(client_order_id.as_str()))
7084 }
7085 IneligibleRestoreCase::ClosedOrder => {
7086 let mut order = make_accepted_gtd_limit_order(
7087 client_order_id.as_str(),
7088 UnixNanos::from(2_000_000_000_000_000_000),
7089 );
7090 order.apply(make_canceled(client_order_id)).unwrap();
7091 Some(order)
7092 }
7093 IneligibleRestoreCase::Emulated => {
7094 let mut order = make_submitted_gtd_limit_order(
7095 client_order_id.as_str(),
7096 UnixNanos::from(2_000_000_000_000_000_000),
7097 );
7098 order.set_emulation_trigger(Some(TriggerType::BidAsk));
7099 Some(order)
7100 }
7101 IneligibleRestoreCase::ManagementDisabled | IneligibleRestoreCase::Stopped => {
7102 Some(make_accepted_gtd_limit_order(
7103 client_order_id.as_str(),
7104 UnixNanos::from(2_000_000_000_000_000_000),
7105 ))
7106 }
7107 };
7108
7109 if let Some(order) = order {
7110 add_order_to_cache(&strategy, &order);
7111 }
7112
7113 if matches!(case, IneligibleRestoreCase::Stopped) {
7114 stop_strategy(&mut strategy);
7115 }
7116
7117 strategy.handle_order_event(make_cancel_rejected(client_order_id));
7118
7119 assert!(!strategy.has_gtd_expiry_timer(&client_order_id));
7120 assert!(exec_messages.get_messages().is_empty());
7121 }
7122
7123 #[rstest]
7124 fn test_handle_cancel_rejected_does_not_restore_missing_order() {
7125 assert_cancel_rejected_does_not_restore(&IneligibleRestoreCase::MissingOrder);
7126 }
7127
7128 #[rstest]
7129 fn test_handle_cancel_rejected_does_not_restore_closed_order() {
7130 assert_cancel_rejected_does_not_restore(&IneligibleRestoreCase::ClosedOrder);
7131 }
7132
7133 #[rstest]
7134 fn test_handle_cancel_rejected_does_not_restore_non_gtd_order() {
7135 assert_cancel_rejected_does_not_restore(&IneligibleRestoreCase::NonGtdOrder);
7136 }
7137
7138 #[rstest]
7139 fn test_handle_cancel_rejected_does_not_restore_when_disabled() {
7140 assert_cancel_rejected_does_not_restore(&IneligibleRestoreCase::ManagementDisabled);
7141 }
7142
7143 #[rstest]
7144 fn test_handle_cancel_rejected_does_not_restore_when_stopped() {
7145 assert_cancel_rejected_does_not_restore(&IneligibleRestoreCase::Stopped);
7146 }
7147
7148 #[rstest]
7149 fn test_handle_cancel_rejected_does_not_restore_emulated_order() {
7150 assert_cancel_rejected_does_not_restore(&IneligibleRestoreCase::Emulated);
7151 }
7152
7153 #[rstest]
7154 fn test_handle_order_event_skips_dispatch_when_stopped() {
7155 let mut strategy = create_test_strategy();
7156 register_strategy(&mut strategy);
7157 start_strategy(&mut strategy);
7158 stop_strategy(&mut strategy);
7159 assert_eq!(strategy.state(), ComponentState::Stopped);
7160
7161 strategy.handle_order_event(make_rejected(ClientOrderId::from("O-001")));
7162
7163 assert!(!strategy.on_order_event_called);
7164 assert!(!strategy.on_order_rejected_called);
7165 }
7166
7167 #[rstest]
7168 #[case::enabled(true)]
7169 #[case::disabled(false)]
7170 fn test_component_start_reactivates_cached_gtd_orders(#[case] enabled: bool) {
7171 let mut strategy = create_gtd_managed_strategy();
7172 strategy.core.config.manage_gtd_expiry = enabled;
7173 let clock = register_gtd_strategy(&mut strategy);
7174 clock.borrow_mut().set_time(UnixNanos::from(2_000_000_000));
7175 let future = make_accepted_gtd_limit_order("O-FUTURE", UnixNanos::from(3_000_000_000));
7176 let expired = make_accepted_gtd_limit_order("O-EXPIRED", UnixNanos::from(1_000_000_000));
7177 add_order_to_cache(&strategy, &future);
7178 add_order_to_cache(&strategy, &expired);
7179 strategy.core.cache_rc().borrow_mut().build_index();
7180 let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
7181 get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
7182 msgbus::register_trading_command_endpoint(
7183 MessagingSwitchboard::exec_engine_queue_execute(),
7184 exec_handler,
7185 );
7186
7187 Component::start(&mut strategy).unwrap();
7188
7189 assert_eq!(strategy.state(), ComponentState::Running);
7190 assert_eq!(strategy.start_calls, 1);
7191 assert_eq!(
7192 strategy.has_gtd_expiry_timer(&future.client_order_id()),
7193 enabled
7194 );
7195 assert!(!strategy.has_gtd_expiry_timer(&expired.client_order_id()));
7196 assert_eq!(
7197 clock.borrow().timer_names(),
7198 if enabled {
7199 vec!["GTD-EXPIRY:O-FUTURE"]
7200 } else {
7201 vec![]
7202 }
7203 );
7204 let commands = exec_messages.get_messages();
7205
7206 if enabled {
7207 assert!(
7208 matches!(commands.as_slice(), [TradingCommand::CancelOrder(command)]
7209 if command.client_order_id == expired.client_order_id())
7210 );
7211 assert_eq!(
7212 strategy
7213 .core
7214 .cache_ref()
7215 .order(&expired.client_order_id())
7216 .unwrap()
7217 .status(),
7218 OrderStatus::PendingCancel
7219 );
7220 } else {
7221 assert!(commands.is_empty());
7222 assert_eq!(
7223 strategy
7224 .core
7225 .cache_ref()
7226 .order(&expired.client_order_id())
7227 .unwrap()
7228 .status(),
7229 OrderStatus::Accepted
7230 );
7231 }
7232 }
7233
7234 #[rstest]
7235 fn test_on_start_calls_reactivate_gtd_timers_when_enabled() {
7236 let config = StrategyConfig {
7237 strategy_id: Some(StrategyId::from("TEST-001")),
7238 order_id_tag: Some("001".to_string()),
7239 manage_gtd_expiry: true,
7240 ..Default::default()
7241 };
7242 let mut strategy = TestStrategy::new(config);
7243 register_strategy(&mut strategy);
7244
7245 let result = Strategy::on_start(&mut strategy);
7246 assert!(result.is_ok());
7247 }
7248
7249 #[rstest]
7250 fn test_on_start_does_not_panic_when_gtd_disabled() {
7251 let config = StrategyConfig {
7252 strategy_id: Some(StrategyId::from("TEST-001")),
7253 order_id_tag: Some("001".to_string()),
7254 manage_gtd_expiry: false,
7255 ..Default::default()
7256 };
7257 let mut strategy = TestStrategy::new(config);
7258 register_strategy(&mut strategy);
7259
7260 let result = Strategy::on_start(&mut strategy);
7261 assert!(result.is_ok());
7262 }
7263
7264 #[rstest]
7265 fn test_on_start_errors_when_strategy_id_is_not_set() {
7266 let mut strategy = TestStrategy::new(StrategyConfig::default());
7267
7268 let err = Strategy::on_start(&mut strategy).unwrap_err().to_string();
7269
7270 assert_eq!(err, "Strategy not registered: strategy_id is not set");
7271 }
7272
7273 #[rstest]
7276 fn test_query_account_when_registered() {
7277 let mut strategy = create_test_strategy();
7278 register_strategy(&mut strategy);
7279
7280 let account_id = AccountId::from("ACC-001");
7281
7282 let result = strategy.query_account(account_id, None, None);
7283
7284 assert!(result.is_ok());
7285 }
7286
7287 #[rstest]
7288 fn test_query_account_with_client_id() {
7289 let mut strategy = create_test_strategy();
7290 register_strategy(&mut strategy);
7291
7292 let account_id = AccountId::from("ACC-001");
7293 let client_id = ClientId::from("BINANCE");
7294
7295 let result = strategy.query_account(account_id, Some(client_id), None);
7296
7297 assert!(result.is_ok());
7298 }
7299
7300 #[rstest]
7301 fn test_query_order_when_registered() {
7302 let mut strategy = create_test_strategy();
7303 register_strategy(&mut strategy);
7304
7305 let order = OrderAny::Market(MarketOrder::test_default());
7306
7307 let result = strategy.query_order(&order, None, None);
7308
7309 assert!(result.is_ok());
7310 }
7311
7312 #[rstest]
7313 fn test_query_order_with_client_id() {
7314 let mut strategy = create_test_strategy();
7315 register_strategy(&mut strategy);
7316
7317 let order = OrderAny::Market(MarketOrder::test_default());
7318 let client_id = ClientId::from("BINANCE");
7319
7320 let result = strategy.query_order(&order, Some(client_id), None);
7321
7322 assert!(result.is_ok());
7323 }
7324
7325 #[rstest]
7326 fn test_is_exiting_returns_false_by_default() {
7327 let strategy = create_test_strategy();
7328 assert!(!strategy.is_exiting());
7329 }
7330
7331 #[rstest]
7332 fn test_is_exiting_returns_true_when_set_manually() {
7333 let mut strategy = create_test_strategy();
7334 register_strategy(&mut strategy);
7335
7336 strategy.core.is_exiting = true;
7338
7339 assert!(strategy.is_exiting());
7340 }
7341
7342 #[rstest]
7343 fn test_market_exit_sets_is_exiting_flag() {
7344 let mut strategy = create_test_strategy();
7346 register_strategy(&mut strategy);
7347
7348 assert!(!strategy.core.is_exiting);
7349
7350 strategy.core.is_exiting = true;
7352 strategy.core.market_exit_attempts = 0;
7353
7354 assert!(strategy.core.is_exiting);
7355 assert_eq!(strategy.core.market_exit_attempts, 0);
7356 }
7357
7358 #[rstest]
7359 fn test_market_exit_uses_config_time_in_force_and_reduce_only() {
7360 let config = StrategyConfig {
7361 strategy_id: Some(StrategyId::from("TEST-001")),
7362 order_id_tag: Some("001".to_string()),
7363 market_exit_time_in_force: TimeInForce::Ioc,
7364 market_exit_reduce_only: false,
7365 ..Default::default()
7366 };
7367 let strategy = TestStrategy::new(config);
7368
7369 assert_eq!(
7370 strategy.core.config.market_exit_time_in_force,
7371 TimeInForce::Ioc
7372 );
7373 assert!(!strategy.core.config.market_exit_reduce_only);
7374 }
7375
7376 #[rstest]
7377 fn test_market_exit_resets_attempt_counter() {
7378 let mut strategy = create_test_strategy();
7379 register_strategy(&mut strategy);
7380
7381 strategy.core.market_exit_attempts = 50;
7383
7384 strategy.core.reset_market_exit_state();
7386
7387 assert_eq!(strategy.core.market_exit_attempts, 0);
7388 }
7389
7390 #[rstest]
7391 fn test_market_exit_second_call_returns_early_when_exiting() {
7392 let mut strategy = create_test_strategy();
7393 register_strategy(&mut strategy);
7394
7395 strategy.core.is_exiting = true;
7397
7398 let result = strategy.market_exit();
7400 assert!(result.is_ok());
7401 assert!(strategy.core.is_exiting);
7402 }
7403
7404 #[rstest]
7405 fn test_finalize_market_exit_resets_state() {
7406 let mut strategy = create_test_strategy();
7407 register_strategy(&mut strategy);
7408
7409 strategy.core.is_exiting = true;
7411 strategy.core.pending_stop = true;
7412 strategy.core.market_exit_attempts = 50;
7413
7414 strategy.finalize_market_exit();
7415
7416 assert!(!strategy.core.is_exiting);
7417 assert!(!strategy.core.pending_stop);
7418 assert_eq!(strategy.core.market_exit_attempts, 0);
7419 }
7420
7421 #[rstest]
7422 fn test_market_exit_config_defaults() {
7423 let config = StrategyConfig::default();
7424
7425 assert!(!config.manage_stop);
7426 assert_eq!(config.market_exit_interval_ms, 100);
7427 assert_eq!(config.market_exit_max_attempts, 100);
7428 }
7429
7430 #[rstest]
7431 fn test_market_exit_with_custom_config() {
7432 let config = StrategyConfig {
7433 strategy_id: Some(StrategyId::from("TEST-001")),
7434 manage_stop: true,
7435 market_exit_interval_ms: 50,
7436 market_exit_max_attempts: 200,
7437 ..Default::default()
7438 };
7439 let strategy = TestStrategy::new(config);
7440
7441 assert!(strategy.core.config.manage_stop);
7442 assert_eq!(strategy.core.config.market_exit_interval_ms, 50);
7443 assert_eq!(strategy.core.config.market_exit_max_attempts, 200);
7444 }
7445
7446 #[derive(Debug)]
7447 struct MarketExitHookTrackingStrategy {
7448 core: StrategyCore,
7449 on_market_exit_called: bool,
7450 post_market_exit_called: bool,
7451 }
7452
7453 impl MarketExitHookTrackingStrategy {
7454 fn new(config: StrategyConfig) -> Self {
7455 Self {
7456 core: StrategyCore::new(config),
7457 on_market_exit_called: false,
7458 post_market_exit_called: false,
7459 }
7460 }
7461 }
7462
7463 impl DataActor for MarketExitHookTrackingStrategy {}
7464
7465 nautilus_strategy!(MarketExitHookTrackingStrategy, {
7466 fn on_market_exit(&mut self) {
7467 self.on_market_exit_called = true;
7468 }
7469
7470 fn post_market_exit(&mut self) {
7471 self.post_market_exit_called = true;
7472 }
7473 });
7474
7475 #[rstest]
7476 fn test_market_exit_calls_on_market_exit_hook() {
7477 let config = StrategyConfig {
7478 strategy_id: Some(StrategyId::from("TEST-001")),
7479 order_id_tag: Some("001".to_string()),
7480 ..Default::default()
7481 };
7482 let mut strategy = MarketExitHookTrackingStrategy::new(config);
7483
7484 let trader_id = TraderId::from("TRADER-001");
7485 let clock = Rc::new(RefCell::new(VirtualClock::new()));
7486 let cache = Rc::new(RefCell::new(Cache::default()));
7487 let portfolio = Rc::new(RefCell::new(Portfolio::new(
7488 clock.clone(),
7489 cache.clone(),
7490 None,
7491 )));
7492 strategy
7493 .core
7494 .register(trader_id, clock, cache, portfolio)
7495 .unwrap();
7496 strategy.initialize().unwrap();
7497 strategy.start().unwrap();
7498
7499 let _ = strategy.market_exit();
7500
7501 assert!(strategy.on_market_exit_called);
7502 }
7503
7504 #[rstest]
7505 fn test_finalize_market_exit_calls_post_market_exit_hook() {
7506 let config = StrategyConfig {
7507 strategy_id: Some(StrategyId::from("TEST-001")),
7508 order_id_tag: Some("001".to_string()),
7509 ..Default::default()
7510 };
7511 let mut strategy = MarketExitHookTrackingStrategy::new(config);
7512
7513 let trader_id = TraderId::from("TRADER-001");
7514 let clock = Rc::new(RefCell::new(VirtualClock::new()));
7515 let cache = Rc::new(RefCell::new(Cache::default()));
7516 let portfolio = Rc::new(RefCell::new(Portfolio::new(
7517 clock.clone(),
7518 cache.clone(),
7519 None,
7520 )));
7521 strategy
7522 .core
7523 .register(trader_id, clock, cache, portfolio)
7524 .unwrap();
7525
7526 strategy.core.is_exiting = true;
7527 strategy.finalize_market_exit();
7528
7529 assert!(strategy.post_market_exit_called);
7530 }
7531
7532 #[derive(Debug)]
7533 struct FailingPostExitStrategy {
7534 core: StrategyCore,
7535 }
7536
7537 impl FailingPostExitStrategy {
7538 fn new(config: StrategyConfig) -> Self {
7539 Self {
7540 core: StrategyCore::new(config),
7541 }
7542 }
7543 }
7544
7545 impl DataActor for FailingPostExitStrategy {}
7546
7547 nautilus_strategy!(FailingPostExitStrategy, {
7548 fn post_market_exit(&mut self) {
7549 panic!("Simulated error in post_market_exit");
7550 }
7551 });
7552
7553 #[rstest]
7554 fn test_finalize_market_exit_handles_hook_panic() {
7555 let config = StrategyConfig {
7556 strategy_id: Some(StrategyId::from("TEST-001")),
7557 order_id_tag: Some("001".to_string()),
7558 ..Default::default()
7559 };
7560 let mut strategy = FailingPostExitStrategy::new(config);
7561
7562 let trader_id = TraderId::from("TRADER-001");
7563 let clock = Rc::new(RefCell::new(VirtualClock::new()));
7564 let cache = Rc::new(RefCell::new(Cache::default()));
7565 let portfolio = Rc::new(RefCell::new(Portfolio::new(
7566 clock.clone(),
7567 cache.clone(),
7568 None,
7569 )));
7570 strategy
7571 .core
7572 .register(trader_id, clock, cache, portfolio)
7573 .unwrap();
7574
7575 strategy.core.is_exiting = true;
7576 strategy.core.pending_stop = true;
7577
7578 strategy.finalize_market_exit();
7580
7581 assert!(!strategy.core.is_exiting);
7583 assert!(!strategy.core.pending_stop);
7584 }
7585
7586 #[rstest]
7587 fn test_check_market_exit_increments_attempts_before_finalizing() {
7588 let mut strategy = create_test_strategy();
7589 register_strategy(&mut strategy);
7590
7591 strategy.core.is_exiting = true;
7592 assert_eq!(strategy.core.market_exit_attempts, 0);
7593
7594 let event = TimeEvent::new(
7595 Ustr::from("MARKET_EXIT_CHECK:TEST-001"),
7596 UUID4::new(),
7597 UnixNanos::default(),
7598 UnixNanos::default(),
7599 );
7600 strategy.check_market_exit(event);
7601
7602 assert!(!strategy.core.is_exiting);
7606 assert_eq!(strategy.core.market_exit_attempts, 0);
7607 }
7608
7609 #[rstest]
7610 fn test_route_time_event_is_idempotent_when_callback_forwards() {
7611 let mut strategy = TestStrategy::new(StrategyConfig {
7612 strategy_id: Some(StrategyId::from("TEST-001")),
7613 ..Default::default()
7614 });
7615 register_strategy(&mut strategy);
7616
7617 let order = make_accepted_limit_order("O-PRESENT");
7618 add_order_to_cache(&strategy, &order);
7619 strategy.core.cache_rc().borrow_mut().build_index();
7620 strategy.core.is_exiting = true;
7621 let event = TimeEvent::new(
7622 strategy.core.market_exit_timer_name,
7623 UUID4::new(),
7624 UnixNanos::default(),
7625 UnixNanos::default(),
7626 );
7627
7628 route_time_event(&mut strategy, &event);
7629 Strategy::on_time_event(&mut strategy, &event).unwrap();
7630
7631 assert!(strategy.core.is_exiting);
7632 assert_eq!(strategy.core.market_exit_attempts, 1);
7633 assert_eq!(
7634 strategy.core.managed_time_event_last_id,
7635 Some(event.event_id)
7636 );
7637 }
7638
7639 #[rstest]
7640 fn test_route_time_event_deduplicates_overridden_managed_handlers() {
7641 let mut strategy = TimerOverrideStrategy {
7642 core: StrategyCore::new(StrategyConfig {
7643 strategy_id: Some(StrategyId::from("TEST-001")),
7644 ..Default::default()
7645 }),
7646 gtd_expiries: 0,
7647 market_exit_checks: 0,
7648 };
7649 let market_event = TimeEvent::new(
7650 strategy.core.market_exit_timer_name,
7651 UUID4::new(),
7652 UnixNanos::default(),
7653 UnixNanos::default(),
7654 );
7655
7656 route_time_event(&mut strategy, &market_event);
7657 DataActor::on_time_event(&mut strategy, &market_event).unwrap();
7658
7659 let client_order_id = ClientOrderId::from("O-001");
7660 let gtd_timer_name = Ustr::from("GTD-EXPIRY:O-001");
7661 strategy
7662 .core
7663 .gtd_timers
7664 .insert(client_order_id, gtd_timer_name);
7665 let gtd_event = TimeEvent::new(
7666 gtd_timer_name,
7667 UUID4::new(),
7668 UnixNanos::default(),
7669 UnixNanos::default(),
7670 );
7671
7672 route_time_event(&mut strategy, >d_event);
7673 DataActor::on_time_event(&mut strategy, >d_event).unwrap();
7674
7675 assert_eq!(strategy.market_exit_checks, 1);
7676 assert_eq!(strategy.gtd_expiries, 1);
7677 assert!(strategy.core.gtd_timers.contains_key(&client_order_id));
7678 assert_eq!(
7679 strategy.core.managed_time_event_last_id,
7680 Some(gtd_event.event_id)
7681 );
7682 }
7683
7684 #[rstest]
7685 fn test_route_time_event_ignores_unowned_market_exit_timer() {
7686 let mut strategy = TestStrategy::new(StrategyConfig {
7687 strategy_id: Some(StrategyId::from("TEST-001")),
7688 ..Default::default()
7689 });
7690 register_strategy(&mut strategy);
7691
7692 let order = make_accepted_limit_order("O-PRESENT");
7693 add_order_to_cache(&strategy, &order);
7694 strategy.core.cache_rc().borrow_mut().build_index();
7695 strategy.core.is_exiting = true;
7696 let event = TimeEvent::new(
7697 Ustr::from("MARKET_EXIT_CHECK:OTHER-001"),
7698 UUID4::new(),
7699 UnixNanos::default(),
7700 UnixNanos::default(),
7701 );
7702
7703 route_time_event(&mut strategy, &event);
7704
7705 assert!(strategy.core.is_exiting);
7706 assert_eq!(strategy.core.market_exit_attempts, 0);
7707 assert_eq!(strategy.core.managed_time_event_last_id, None);
7708 }
7709
7710 #[rstest]
7711 #[case(OrderStatus::Accepted)]
7712 #[case(OrderStatus::Submitted)]
7713 fn test_check_market_exit_waits_for_open_or_inflight_order(#[case] status: OrderStatus) {
7714 let mut strategy = create_test_strategy();
7715 register_strategy(&mut strategy);
7716 start_strategy(&mut strategy);
7717 let mut order = OrderTestBuilder::new(OrderType::Limit)
7718 .trader_id(TraderId::from("TRADER-001"))
7719 .strategy_id(StrategyId::from("TEST-001"))
7720 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
7721 .client_order_id(ClientOrderId::from("O-EXIT-WORKING"))
7722 .side(OrderSide::Buy)
7723 .quantity(Quantity::from(3))
7724 .price(Price::from("50002.00"))
7725 .build();
7726 let account_id = AccountId::from("ACC-001");
7727 order
7728 .apply(TestOrderEventStubs::submitted(&order, account_id))
7729 .unwrap();
7730
7731 if status == OrderStatus::Accepted {
7732 order
7733 .apply(TestOrderEventStubs::accepted(
7734 &order,
7735 account_id,
7736 VenueOrderId::from("V-EXIT-WORKING"),
7737 ))
7738 .unwrap();
7739 }
7740
7741 add_order_to_cache(&strategy, &order);
7742 strategy.core.cache_rc().borrow_mut().build_index();
7743 strategy.core.is_exiting = true;
7744
7745 let event = TimeEvent::new(
7746 strategy.core.market_exit_timer_name,
7747 UUID4::new(),
7748 0.into(),
7749 0.into(),
7750 );
7751
7752 strategy.check_market_exit(event);
7753
7754 assert!(strategy.is_exiting());
7755 assert_eq!(strategy.core.market_exit_attempts, 1);
7756 assert_eq!(strategy.state(), ComponentState::Running);
7757 assert_eq!(
7758 strategy
7759 .cache()
7760 .order(&order.client_order_id())
7761 .unwrap()
7762 .status(),
7763 status
7764 );
7765
7766 strategy
7767 .core
7768 .cache_rc()
7769 .borrow_mut()
7770 .update_order(&make_canceled(order.client_order_id()))
7771 .unwrap();
7772
7773 let event = TimeEvent::new(
7774 strategy.core.market_exit_timer_name,
7775 UUID4::new(),
7776 0.into(),
7777 0.into(),
7778 );
7779 strategy.check_market_exit(event);
7780
7781 assert!(!strategy.is_exiting());
7782 assert_eq!(strategy.core.market_exit_attempts, 0);
7783 }
7784
7785 #[rstest]
7786 #[case(false)]
7787 #[case(true)]
7788 fn test_check_market_exit_finalizes_when_max_attempts_reached(#[case] pending_stop: bool) {
7789 let config = StrategyConfig {
7790 strategy_id: Some(StrategyId::from("TEST-001")),
7791 order_id_tag: Some("001".to_string()),
7792 market_exit_max_attempts: 3,
7793 ..Default::default()
7794 };
7795 let mut strategy = TestStrategy::new(config);
7796 register_strategy(&mut strategy);
7797 start_strategy(&mut strategy);
7798
7799 let order = OrderTestBuilder::new(OrderType::Market)
7800 .trader_id(TraderId::from("TRADER-001"))
7801 .strategy_id(StrategyId::from("TEST-001"))
7802 .client_order_id(ClientOrderId::from("O-EXIT-MAX-LOCAL"))
7803 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
7804 .side(OrderSide::Buy)
7805 .quantity(Quantity::from(3))
7806 .build();
7807 add_order_to_cache(&strategy, &order);
7808
7809 strategy.core.is_exiting = true;
7810 strategy.core.pending_stop = pending_stop;
7811 strategy.core.market_exit_attempts = 2; let event = TimeEvent::new(
7814 Ustr::from("MARKET_EXIT_CHECK:TEST-001"),
7815 UUID4::new(),
7816 UnixNanos::default(),
7817 UnixNanos::default(),
7818 );
7819 strategy.check_market_exit(event);
7820
7821 assert!(!strategy.core.is_exiting);
7823 assert!(!strategy.core.pending_stop);
7824 assert_eq!(strategy.core.market_exit_attempts, 0);
7825 assert_eq!(
7826 strategy.state(),
7827 if pending_stop {
7828 ComponentState::Stopped
7829 } else {
7830 ComponentState::Running
7831 }
7832 );
7833 assert_eq!(
7834 strategy
7835 .cache()
7836 .order(&order.client_order_id())
7837 .unwrap()
7838 .status(),
7839 OrderStatus::Initialized
7840 );
7841 }
7842
7843 #[rstest]
7844 #[case(OrderStatus::Initialized, false)]
7845 #[case(OrderStatus::Emulated, false)]
7846 #[case(OrderStatus::Released, false)]
7847 #[case(OrderStatus::Initialized, true)]
7848 #[case(OrderStatus::Emulated, true)]
7849 #[case(OrderStatus::Released, true)]
7850 fn test_market_exit_cancels_active_local_orders_before_finalizing(
7851 #[case] status: OrderStatus,
7852 #[case] manage_stop: bool,
7853 ) {
7854 let mut strategy = TestStrategy::new(StrategyConfig {
7855 strategy_id: Some(StrategyId::from("TEST-001")),
7856 order_id_tag: Some("001".to_string()),
7857 manage_stop,
7858 ..Default::default()
7859 });
7860
7861 let clock = register_strategy_with_clock(&mut strategy);
7862 clock
7863 .borrow_mut()
7864 .register_default_handler(TimeEventCallback::from(|_event: TimeEvent| {}));
7865 start_strategy(&mut strategy);
7866 let (emulator_handler, emulator_messages): (
7867 _,
7868 TypedIntoMessageSavingHandler<TradingCommand>,
7869 ) = get_typed_into_message_saving_handler(Some(Ustr::from("OrderEmulator.execute")));
7870 msgbus::register_trading_command_endpoint(
7871 MessagingSwitchboard::order_emulator_execute(),
7872 emulator_handler,
7873 );
7874 let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
7875 get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
7876 msgbus::register_trading_command_endpoint(
7877 MessagingSwitchboard::exec_engine_queue_execute(),
7878 exec_handler,
7879 );
7880 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
7881 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
7882 msgbus::register_trading_command_endpoint(
7883 MessagingSwitchboard::risk_engine_queue_execute(),
7884 risk_handler,
7885 );
7886 let mut order = OrderTestBuilder::new(OrderType::StopMarket)
7887 .trader_id(TraderId::from("TRADER-001"))
7888 .strategy_id(StrategyId::from("TEST-001"))
7889 .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
7890 .client_order_id(ClientOrderId::from("O-EXIT-LOCAL"))
7891 .side(OrderSide::Buy)
7892 .trigger_price(Price::from("51000.00"))
7893 .quantity(Quantity::from(2))
7894 .emulation_trigger(TriggerType::BidAsk)
7895 .build();
7896
7897 match status {
7898 OrderStatus::Emulated => {
7899 let event = OrderEmulatedSpec::builder()
7900 .trader_id(order.trader_id())
7901 .strategy_id(order.strategy_id())
7902 .instrument_id(order.instrument_id())
7903 .client_order_id(order.client_order_id())
7904 .build();
7905 order.apply(OrderEventAny::Emulated(event)).unwrap();
7906 add_order_to_cache(&strategy, &order);
7907 }
7908 OrderStatus::Released => {
7909 let event = OrderReleasedSpec::builder()
7910 .trader_id(order.trader_id())
7911 .strategy_id(order.strategy_id())
7912 .instrument_id(order.instrument_id())
7913 .client_order_id(order.client_order_id())
7914 .released_price(Price::from("51001.00"))
7915 .build();
7916 order.apply(OrderEventAny::Released(event)).unwrap();
7917 order.set_emulation_trigger(None);
7918 add_order_to_cache(&strategy, &order);
7919 }
7920 OrderStatus::Initialized => {
7921 order.set_emulation_trigger(None);
7922 strategy
7923 .submit_order(order.clone(), None, None, None)
7924 .unwrap();
7925 }
7926 _ => unreachable!(),
7927 }
7928
7929 assert_eq!(
7930 strategy
7931 .cache()
7932 .order(&order.client_order_id())
7933 .unwrap()
7934 .status(),
7935 status
7936 );
7937
7938 if manage_stop {
7939 assert!(!Strategy::stop(&mut strategy));
7940 } else {
7941 strategy.market_exit().unwrap();
7942 }
7943
7944 let event = TimeEvent::new(
7945 strategy.core.market_exit_timer_name,
7946 UUID4::new(),
7947 0.into(),
7948 0.into(),
7949 );
7950 strategy.check_market_exit(event);
7951
7952 let emulator_messages = emulator_messages.get_messages();
7953 let exec_messages = exec_messages.get_messages();
7954 assert_eq!(emulator_messages.len() + exec_messages.len(), 1);
7955 let command = emulator_messages
7956 .iter()
7957 .chain(&exec_messages)
7958 .next()
7959 .unwrap();
7960 assert!(matches!(command, TradingCommand::CancelOrder(cancel)
7961 if cancel.client_order_id == order.client_order_id()
7962 && cancel.instrument_id == order.instrument_id()
7963 && cancel.strategy_id == order.strategy_id()));
7964 assert_eq!(
7965 emulator_messages.len(),
7966 usize::from(status == OrderStatus::Emulated)
7967 );
7968 assert_eq!(
7969 risk_messages.get_messages().len(),
7970 usize::from(status == OrderStatus::Initialized)
7971 );
7972 assert!(strategy.is_exiting());
7973 assert_eq!(strategy.core.pending_stop, manage_stop);
7974 assert_eq!(strategy.state(), ComponentState::Running);
7975 assert_eq!(strategy.core.market_exit_attempts, 1);
7976
7977 strategy
7978 .core
7979 .cache_rc()
7980 .borrow_mut()
7981 .update_order(&make_canceled(order.client_order_id()))
7982 .unwrap();
7983
7984 let event = TimeEvent::new(
7985 strategy.core.market_exit_timer_name,
7986 UUID4::new(),
7987 0.into(),
7988 0.into(),
7989 );
7990 strategy.check_market_exit(event);
7991
7992 assert!(!strategy.is_exiting());
7993 assert!(!strategy.core.pending_stop);
7994 assert_eq!(strategy.core.market_exit_attempts, 0);
7995 assert_eq!(
7996 strategy.state(),
7997 if manage_stop {
7998 ComponentState::Stopped
7999 } else {
8000 ComponentState::Running
8001 }
8002 );
8003 }
8004
8005 #[rstest]
8006 #[case(false)]
8007 #[case(true)]
8008 fn test_market_exit_canceled_emulated_order_cannot_release_to_risk(#[case] manage_stop: bool) {
8009 let mut strategy = TestStrategy::new(StrategyConfig {
8010 strategy_id: Some(StrategyId::from("TEST-001")),
8011 order_id_tag: Some("001".to_string()),
8012 manage_stop,
8013 ..Default::default()
8014 });
8015
8016 let clock = register_strategy_with_clock(&mut strategy);
8017 clock
8018 .borrow_mut()
8019 .register_default_handler(TimeEventCallback::from(|_event: TimeEvent| {}));
8020 start_strategy(&mut strategy);
8021 let cache = strategy.core.cache_rc();
8022 let instrument = currency_pair_btcusdt();
8023 cache
8024 .borrow_mut()
8025 .add_instrument(InstrumentAny::CurrencyPair(instrument.clone()))
8026 .unwrap();
8027 let emulator = Rc::new(RefCell::new(OrderEmulator::new(clock, cache.clone())));
8028 OrderEmulator::register_msgbus_handlers(&emulator);
8029 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
8030 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
8031 msgbus::register_trading_command_endpoint(
8032 MessagingSwitchboard::risk_engine_queue_execute(),
8033 risk_handler,
8034 );
8035 let order_ids = [
8036 ClientOrderId::from("O-EXIT-EMULATED"),
8037 ClientOrderId::from("O-SIBLING-EMULATED"),
8038 ];
8039
8040 for (client_order_id, strategy_id) in order_ids.into_iter().zip([
8041 StrategyId::from("TEST-001"),
8042 StrategyId::from("SIBLING-001"),
8043 ]) {
8044 let order = OrderTestBuilder::new(OrderType::StopMarket)
8045 .trader_id(TraderId::from("TRADER-001"))
8046 .strategy_id(strategy_id)
8047 .instrument_id(instrument.id)
8048 .client_order_id(client_order_id)
8049 .side(OrderSide::Buy)
8050 .trigger_price(Price::from("51000.00"))
8051 .quantity(Quantity::from(2))
8052 .emulation_trigger(TriggerType::BidAsk)
8053 .build();
8054
8055 if strategy_id == StrategyId::from("TEST-001") {
8056 strategy.submit_order(order, None, None, None).unwrap();
8057 } else {
8058 add_order_to_cache(&strategy, &order);
8059 let command = make_submit_command(&order);
8060 emulator
8061 .borrow_mut()
8062 .execute(TradingCommand::SubmitOrder(command));
8063 }
8064 }
8065
8066 assert_eq!(
8067 cache.borrow().order(&order_ids[0]).unwrap().status(),
8068 OrderStatus::Emulated
8069 );
8070 assert_eq!(
8071 cache.borrow().order(&order_ids[1]).unwrap().status(),
8072 OrderStatus::Emulated
8073 );
8074 assert!(risk_messages.get_messages().is_empty());
8075
8076 if manage_stop {
8077 assert!(!Strategy::stop(&mut strategy));
8078 } else {
8079 strategy.market_exit().unwrap();
8080 }
8081
8082 assert!(strategy.is_exiting());
8083 assert_eq!(
8084 cache.borrow().order(&order_ids[0]).unwrap().status(),
8085 OrderStatus::Canceled
8086 );
8087 assert_eq!(
8088 cache.borrow().order(&order_ids[1]).unwrap().status(),
8089 OrderStatus::Emulated
8090 );
8091 assert!(
8092 !emulator
8093 .borrow()
8094 .get_submit_order_commands()
8095 .contains_key(&order_ids[0])
8096 );
8097
8098 let event = TimeEvent::new(
8099 strategy.core.market_exit_timer_name,
8100 UUID4::new(),
8101 0.into(),
8102 0.into(),
8103 );
8104 strategy.check_market_exit(event);
8105
8106 assert!(!strategy.is_exiting());
8107 assert_eq!(
8108 strategy.state(),
8109 if manage_stop {
8110 ComponentState::Stopped
8111 } else {
8112 ComponentState::Running
8113 }
8114 );
8115
8116 let quote = QuoteTick::new(
8117 instrument.id,
8118 Price::from("51001.00"),
8119 Price::from("51002.00"),
8120 Quantity::from(3),
8121 Quantity::from(4),
8122 1.into(),
8123 1.into(),
8124 );
8125 emulator.borrow_mut().on_quote_tick(quote);
8126
8127 assert_eq!(
8128 cache.borrow().order(&order_ids[0]).unwrap().status(),
8129 OrderStatus::Canceled
8130 );
8131 assert_eq!(
8132 cache.borrow().order(&order_ids[1]).unwrap().status(),
8133 OrderStatus::Released
8134 );
8135 let commands = risk_messages.get_messages();
8136 assert_eq!(commands.len(), 1);
8137 assert!(
8138 matches!(&commands[0], TradingCommand::SubmitOrder(command) if command.client_order_id == order_ids[1])
8139 );
8140 }
8141
8142 #[rstest]
8143 fn test_check_market_exit_finalizes_when_no_orders_or_positions() {
8144 let mut strategy = create_test_strategy();
8145 register_strategy(&mut strategy);
8146
8147 strategy.core.is_exiting = true;
8148
8149 let event = TimeEvent::new(
8150 Ustr::from("MARKET_EXIT_CHECK:TEST-001"),
8151 UUID4::new(),
8152 UnixNanos::default(),
8153 UnixNanos::default(),
8154 );
8155 strategy.check_market_exit(event);
8156
8157 assert!(!strategy.core.is_exiting);
8159 }
8160
8161 #[rstest]
8162 fn test_market_exit_timer_name_format() {
8163 let config = StrategyConfig {
8164 strategy_id: Some(StrategyId::from("MY-STRATEGY-001")),
8165 ..Default::default()
8166 };
8167 let strategy = TestStrategy::new(config);
8168
8169 assert_eq!(
8170 strategy.core.market_exit_timer_name,
8171 "MARKET_EXIT_CHECK:MY-STRATEGY-001"
8172 );
8173 }
8174
8175 #[rstest]
8176 fn test_reset_market_exit_state() {
8177 let mut strategy = create_test_strategy();
8178
8179 strategy.core.is_exiting = true;
8180 strategy.core.pending_stop = true;
8181 strategy.core.market_exit_attempts = 50;
8182
8183 strategy.core.reset_market_exit_state();
8184
8185 assert!(!strategy.core.is_exiting);
8186 assert!(!strategy.core.pending_stop);
8187 assert_eq!(strategy.core.market_exit_attempts, 0);
8188 }
8189
8190 #[rstest]
8191 fn test_cancel_market_exit_resets_state_without_hooks() {
8192 let config = StrategyConfig {
8193 strategy_id: Some(StrategyId::from("TEST-001")),
8194 order_id_tag: Some("001".to_string()),
8195 ..Default::default()
8196 };
8197 let mut strategy = MarketExitHookTrackingStrategy::new(config);
8198
8199 let trader_id = TraderId::from("TRADER-001");
8200 let clock = Rc::new(RefCell::new(VirtualClock::new()));
8201 let cache = Rc::new(RefCell::new(Cache::default()));
8202 let portfolio = Rc::new(RefCell::new(Portfolio::new(
8203 clock.clone(),
8204 cache.clone(),
8205 None,
8206 )));
8207 strategy
8208 .core
8209 .register(trader_id, clock, cache, portfolio)
8210 .unwrap();
8211
8212 strategy.core.is_exiting = true;
8214 strategy.core.pending_stop = true;
8215 strategy.core.market_exit_attempts = 50;
8216
8217 strategy.cancel_market_exit();
8219
8220 assert!(!strategy.core.is_exiting);
8222 assert!(!strategy.core.pending_stop);
8223 assert_eq!(strategy.core.market_exit_attempts, 0);
8224
8225 assert!(!strategy.on_market_exit_called);
8227 assert!(!strategy.post_market_exit_called);
8228 }
8229
8230 #[rstest]
8231 fn test_market_exit_returns_early_when_not_running() {
8232 let mut strategy = create_test_strategy();
8233 register_strategy(&mut strategy);
8234
8235 assert!(!strategy.is_running());
8237
8238 let result = strategy.market_exit();
8239
8240 assert!(result.is_ok());
8242 assert!(!strategy.core.is_exiting);
8243 }
8244
8245 #[rstest]
8246 fn test_stop_with_manage_stop_false_cleans_up_active_exit() {
8247 let config = StrategyConfig {
8248 strategy_id: Some(StrategyId::from("TEST-001")),
8249 order_id_tag: Some("001".to_string()),
8250 manage_stop: false,
8251 ..Default::default()
8252 };
8253 let mut strategy = TestStrategy::new(config);
8254 register_strategy(&mut strategy);
8255
8256 strategy.core.is_exiting = true;
8258 strategy.core.market_exit_attempts = 5;
8259
8260 let should_proceed = Strategy::stop(&mut strategy);
8262
8263 assert!(should_proceed);
8265 assert!(!strategy.core.is_exiting);
8266 assert_eq!(strategy.core.market_exit_attempts, 0);
8267 }
8268
8269 #[rstest]
8270 fn test_stop_with_manage_stop_true_defers_when_running() {
8271 let config = StrategyConfig {
8272 strategy_id: Some(StrategyId::from("TEST-001")),
8273 order_id_tag: Some("001".to_string()),
8274 manage_stop: true,
8275 ..Default::default()
8276 };
8277 let mut strategy = TestStrategy::new(config);
8278
8279 let trader_id = TraderId::from("TRADER-001");
8281 let clock = Rc::new(RefCell::new(VirtualClock::new()));
8282 clock
8283 .borrow_mut()
8284 .register_default_handler(TimeEventCallback::from(|_event: TimeEvent| {}));
8285 let cache = Rc::new(RefCell::new(Cache::default()));
8286 let portfolio = Rc::new(RefCell::new(Portfolio::new(
8287 clock.clone(),
8288 cache.clone(),
8289 None,
8290 )));
8291 strategy
8292 .core
8293 .register(trader_id, clock, cache, portfolio)
8294 .unwrap();
8295 strategy.initialize().unwrap();
8296 strategy.start().unwrap();
8297
8298 let should_proceed = Strategy::stop(&mut strategy);
8299
8300 assert!(!should_proceed);
8302 assert!(strategy.core.pending_stop);
8303 }
8304
8305 #[rstest]
8306 fn test_stop_with_manage_stop_true_returns_early_if_pending() {
8307 let config = StrategyConfig {
8308 strategy_id: Some(StrategyId::from("TEST-001")),
8309 order_id_tag: Some("001".to_string()),
8310 manage_stop: true,
8311 ..Default::default()
8312 };
8313 let mut strategy = TestStrategy::new(config);
8314 register_strategy(&mut strategy);
8315 start_strategy(&mut strategy);
8316 strategy.core.pending_stop = true;
8317
8318 let should_proceed = Strategy::stop(&mut strategy);
8320
8321 assert!(!should_proceed);
8323 assert!(strategy.core.pending_stop);
8324 }
8325
8326 #[rstest]
8327 fn test_stop_with_manage_stop_true_proceeds_when_not_running() {
8328 let config = StrategyConfig {
8329 strategy_id: Some(StrategyId::from("TEST-001")),
8330 order_id_tag: Some("001".to_string()),
8331 manage_stop: true,
8332 ..Default::default()
8333 };
8334 let mut strategy = TestStrategy::new(config);
8335 register_strategy(&mut strategy);
8336
8337 assert!(!strategy.is_running());
8339
8340 let should_proceed = Strategy::stop(&mut strategy);
8341
8342 assert!(should_proceed);
8344 }
8345
8346 #[rstest]
8347 fn test_finalize_market_exit_stops_strategy_when_pending() {
8348 let config = StrategyConfig {
8349 strategy_id: Some(StrategyId::from("TEST-001")),
8350 order_id_tag: Some("001".to_string()),
8351 ..Default::default()
8352 };
8353 let mut strategy = TestStrategy::new(config);
8354 register_strategy(&mut strategy);
8355 start_strategy(&mut strategy);
8356
8357 strategy.core.is_exiting = true;
8359 strategy.core.pending_stop = true;
8360
8361 strategy.finalize_market_exit();
8362
8363 assert_eq!(strategy.state(), ComponentState::Stopped);
8365 assert!(!strategy.core.is_exiting);
8366 assert!(!strategy.core.pending_stop);
8367 }
8368
8369 #[rstest]
8370 fn test_finalize_market_exit_stays_running_when_not_pending() {
8371 let config = StrategyConfig {
8372 strategy_id: Some(StrategyId::from("TEST-001")),
8373 order_id_tag: Some("001".to_string()),
8374 ..Default::default()
8375 };
8376 let mut strategy = TestStrategy::new(config);
8377 register_strategy(&mut strategy);
8378 start_strategy(&mut strategy);
8379
8380 strategy.core.is_exiting = true;
8382 strategy.core.pending_stop = false;
8383
8384 strategy.finalize_market_exit();
8385
8386 assert_eq!(strategy.state(), ComponentState::Running);
8388 assert!(!strategy.core.is_exiting);
8389 }
8390
8391 #[rstest]
8392 fn test_submit_order_denied_during_market_exit_when_not_reduce_only() {
8393 let mut strategy = create_test_strategy();
8394 register_strategy(&mut strategy);
8395 start_strategy(&mut strategy);
8396 strategy.core.is_exiting = true;
8397
8398 let (event_handler, event_messages): (_, TypedMessageSavingHandler<OrderEventAny>) =
8399 get_typed_message_saving_handler(Some(Ustr::from("events.order.denied")));
8400 let order = OrderAny::Market(MarketOrder::new(
8401 TraderId::from("TRADER-001"),
8402 StrategyId::from("TEST-001"),
8403 InstrumentId::from("BTCUSDT.BINANCE"),
8404 ClientOrderId::from("O-20250208-0001"),
8405 OrderSide::Buy,
8406 Quantity::from(100_000),
8407 TimeInForce::Gtc,
8408 UUID4::new(),
8409 UnixNanos::default(),
8410 false, false,
8412 None,
8413 None,
8414 None,
8415 None,
8416 None,
8417 None,
8418 None,
8419 None,
8420 ));
8421 let topic = format!("events.order.{}", order.strategy_id());
8422 msgbus::subscribe_order_events(topic.clone().into(), event_handler.clone(), None);
8423 let client_order_id = order.client_order_id();
8424 let result = strategy.submit_order(order.clone(), None, None, None);
8425
8426 msgbus::unsubscribe_order_events(topic.into(), &event_handler);
8427
8428 assert!(result.is_ok());
8429 let cache = strategy.cache();
8430 let cached_order = cache.order(&client_order_id).unwrap();
8431 assert_eq!(cached_order.status(), OrderStatus::Denied);
8432
8433 let event_messages = event_messages.get_messages();
8434 assert_eq!(event_messages.len(), 2);
8435 assert_eq!(
8436 event_messages[0],
8437 OrderEventAny::Initialized(order.init_event().clone())
8438 );
8439 let OrderEventAny::Denied(denied) = &event_messages[1] else {
8440 panic!("expected OrderDenied event");
8441 };
8442 assert_eq!(denied.reason, Ustr::from("MARKET_EXIT_IN_PROGRESS"));
8443 }
8444
8445 #[rstest]
8446 fn test_submit_order_list_denied_during_market_exit_publishes_init_then_denied_events() {
8447 let mut strategy = create_test_strategy();
8448 register_strategy(&mut strategy);
8449 start_strategy(&mut strategy);
8450 strategy.core.is_exiting = true;
8451
8452 let orders = vec![
8453 make_initialized_market_order("O-20250208-LIST-DENY-001"),
8454 make_initialized_market_order("O-20250208-LIST-DENY-002"),
8455 ];
8456 let client_order_id1 = orders[0].client_order_id();
8457 let client_order_id2 = orders[1].client_order_id();
8458 let cache_rc = strategy.core.cache_rc();
8459 let timeline = Rc::new(RefCell::new(Vec::new()));
8460 let event_messages = Rc::new(RefCell::new(Vec::new()));
8461
8462 let event_handler = {
8463 let event_messages = event_messages.clone();
8464 let timeline = timeline.clone();
8465 TypedHandler::from_with_id("events.order.list_denied", move |event: &OrderEventAny| {
8466 match event {
8467 OrderEventAny::Initialized(e) if e.client_order_id == client_order_id1 => {
8468 assert!(cache_rc.borrow().order_exists(&client_order_id1));
8469 timeline.borrow_mut().push("init1");
8470 }
8471 OrderEventAny::Initialized(e) if e.client_order_id == client_order_id2 => {
8472 assert!(cache_rc.borrow().order_exists(&client_order_id2));
8473 timeline.borrow_mut().push("init2");
8474 }
8475 OrderEventAny::Denied(e) if e.client_order_id == client_order_id1 => {
8476 assert_eq!(e.reason, Ustr::from("MARKET_EXIT_IN_PROGRESS"));
8477 let cache = cache_rc.borrow();
8478 let cached_order = cache.order(&client_order_id1).unwrap();
8479 assert_eq!(cached_order.status(), OrderStatus::Denied);
8480 timeline.borrow_mut().push("denied1");
8481 }
8482 OrderEventAny::Denied(e) if e.client_order_id == client_order_id2 => {
8483 assert_eq!(e.reason, Ustr::from("MARKET_EXIT_IN_PROGRESS"));
8484 let cache = cache_rc.borrow();
8485 let cached_order = cache.order(&client_order_id2).unwrap();
8486 assert_eq!(cached_order.status(), OrderStatus::Denied);
8487 timeline.borrow_mut().push("denied2");
8488 }
8489 _ => panic!("unexpected order event {event:?}"),
8490 }
8491 event_messages.borrow_mut().push(event.clone());
8492 })
8493 };
8494 let risk_handler = {
8495 let timeline = timeline.clone();
8496 TypedIntoHandler::from_with_id(
8497 "RiskEngine.queue_execute",
8498 move |_command: TradingCommand| {
8499 timeline.borrow_mut().push("command");
8500 },
8501 )
8502 };
8503 msgbus::register_trading_command_endpoint(
8504 MessagingSwitchboard::risk_engine_queue_execute(),
8505 risk_handler,
8506 );
8507
8508 let topic = format!("events.order.{}", orders[0].strategy_id());
8509 msgbus::subscribe_order_events(topic.clone().into(), event_handler.clone(), None);
8510 let result = strategy.submit_order_list(orders.clone(), None, None, None);
8511
8512 msgbus::unsubscribe_order_events(topic.into(), &event_handler);
8513
8514 assert!(result.is_ok());
8515
8516 let cache = strategy.cache();
8517 let cached_order1 = cache.order(&client_order_id1).unwrap();
8518 let cached_order2 = cache.order(&client_order_id2).unwrap();
8519 assert_eq!(cached_order1.status(), OrderStatus::Denied);
8520 assert_eq!(cached_order2.status(), OrderStatus::Denied);
8521
8522 let event_messages = event_messages.borrow();
8523 assert_eq!(event_messages.len(), 4);
8524 assert_eq!(
8525 event_messages[0],
8526 OrderEventAny::Initialized(orders[0].init_event().clone())
8527 );
8528 assert!(matches!(
8529 &event_messages[1],
8530 OrderEventAny::Denied(e)
8531 if e.client_order_id == client_order_id1
8532 && e.reason == Ustr::from("MARKET_EXIT_IN_PROGRESS")
8533 ));
8534 assert_eq!(
8535 event_messages[2],
8536 OrderEventAny::Initialized(orders[1].init_event().clone())
8537 );
8538 assert!(matches!(
8539 &event_messages[3],
8540 OrderEventAny::Denied(e)
8541 if e.client_order_id == client_order_id2
8542 && e.reason == Ustr::from("MARKET_EXIT_IN_PROGRESS")
8543 ));
8544 assert_eq!(
8545 timeline.borrow().as_slice(),
8546 &["init1", "denied1", "init2", "denied2"]
8547 );
8548 }
8549
8550 #[rstest]
8551 fn test_submit_order_list_market_exit_rejects_non_initialized_without_events() {
8552 let mut strategy = create_test_strategy();
8553 register_strategy(&mut strategy);
8554 start_strategy(&mut strategy);
8555 strategy.core.is_exiting = true;
8556
8557 let order = make_accepted_market_order("O-20250208-LIST-DENY-ACCEPTED");
8558 let topic = format!("events.order.{}", order.strategy_id());
8559 let (event_handler, event_messages): (_, TypedMessageSavingHandler<OrderEventAny>) =
8560 get_typed_message_saving_handler(Some(Ustr::from("events.order.list_invalid")));
8561
8562 msgbus::subscribe_order_events(topic.clone().into(), event_handler.clone(), None);
8563 let result = strategy.submit_order_list(vec![order], None, None, None);
8564
8565 msgbus::unsubscribe_order_events(topic.into(), &event_handler);
8566
8567 assert!(result.is_err());
8568 assert!(
8569 result
8570 .unwrap_err()
8571 .to_string()
8572 .contains("expected INITIALIZED")
8573 );
8574 assert!(event_messages.get_messages().is_empty());
8575 }
8576
8577 #[rstest]
8578 fn test_submit_order_list_rejects_mixed_venues_with_friendly_error() {
8579 let mut strategy = create_test_strategy();
8580 register_strategy(&mut strategy);
8581 start_strategy(&mut strategy);
8582
8583 let binance_order = make_initialized_market_order("O-MIXED-VENUE-001");
8584 let bybit_order = OrderAny::Market(MarketOrder::new(
8585 TraderId::from("TRADER-001"),
8586 StrategyId::from("TEST-001"),
8587 InstrumentId::from("BTCUSDT.BYBIT"),
8588 ClientOrderId::from("O-MIXED-VENUE-002"),
8589 OrderSide::Buy,
8590 Quantity::from(100_000),
8591 TimeInForce::Gtc,
8592 UUID4::new(),
8593 UnixNanos::default(),
8594 false,
8595 false,
8596 None,
8597 None,
8598 None,
8599 None,
8600 None,
8601 None,
8602 None,
8603 None,
8604 ));
8605
8606 let result = strategy.submit_order_list(vec![binance_order, bybit_order], None, None, None);
8607
8608 let err = result.unwrap_err();
8609 let msg = err.to_string();
8610 assert!(
8611 msg.contains("OrderList denied: orders must share the same venue"),
8612 "unexpected error: {msg}",
8613 );
8614 assert!(msg.contains("BINANCE"), "expected BINANCE in error: {msg}");
8615 assert!(msg.contains("BYBIT"), "expected BYBIT in error: {msg}");
8616 }
8617
8618 #[rstest]
8619 fn test_submit_order_allowed_during_market_exit_when_reduce_only() {
8620 let mut strategy = create_test_strategy();
8621 register_strategy(&mut strategy);
8622 start_strategy(&mut strategy);
8623 strategy.core.is_exiting = true;
8624
8625 let order = OrderAny::Market(MarketOrder::new(
8626 TraderId::from("TRADER-001"),
8627 StrategyId::from("TEST-001"),
8628 InstrumentId::from("BTCUSDT.BINANCE"),
8629 ClientOrderId::from("O-20250208-0001"),
8630 OrderSide::Buy,
8631 Quantity::from(100_000),
8632 TimeInForce::Gtc,
8633 UUID4::new(),
8634 UnixNanos::default(),
8635 true, false,
8637 None,
8638 None,
8639 None,
8640 None,
8641 None,
8642 None,
8643 None,
8644 None,
8645 ));
8646 let client_order_id = order.client_order_id();
8647 let result = strategy.submit_order(order, None, None, None);
8648
8649 assert!(result.is_ok());
8650 let cache = strategy.cache();
8651 let cached_order = cache.order(&client_order_id).unwrap();
8652 assert_ne!(cached_order.status(), OrderStatus::Denied);
8653 }
8654
8655 #[rstest]
8656 fn test_submit_order_allowed_during_market_exit_when_tagged() {
8657 let mut strategy = create_test_strategy();
8658 register_strategy(&mut strategy);
8659 start_strategy(&mut strategy);
8660 strategy.core.is_exiting = true;
8661
8662 let order = OrderAny::Market(MarketOrder::new(
8663 TraderId::from("TRADER-001"),
8664 StrategyId::from("TEST-001"),
8665 InstrumentId::from("BTCUSDT.BINANCE"),
8666 ClientOrderId::from("O-20250208-0002"),
8667 OrderSide::Buy,
8668 Quantity::from(100_000),
8669 TimeInForce::Gtc,
8670 UUID4::new(),
8671 UnixNanos::default(),
8672 false, false,
8674 None,
8675 None,
8676 None,
8677 None,
8678 None,
8679 None,
8680 None,
8681 Some(vec![Ustr::from("MARKET_EXIT")]),
8682 ));
8683 let client_order_id = order.client_order_id();
8684 let result = strategy.submit_order(order, None, None, None);
8685
8686 assert!(result.is_ok());
8687 let cache = strategy.cache();
8688 let cached_order = cache.order(&client_order_id).unwrap();
8689 assert_ne!(cached_order.status(), OrderStatus::Denied);
8690 }
8691
8692 #[derive(Debug)]
8693 struct MacroTestSimple {
8694 core: StrategyCore,
8695 }
8696
8697 nautilus_strategy!(MacroTestSimple);
8698
8699 impl DataActor for MacroTestSimple {}
8700
8701 #[derive(Debug)]
8702 struct MacroTestWithHooks {
8703 core: StrategyCore,
8704 start_trace: Vec<&'static str>,
8705 start_fails: bool,
8706 }
8707
8708 nautilus_strategy!(MacroTestWithHooks, {
8709 fn on_start(&mut self) -> anyhow::Result<()> {
8710 self.start_trace.push("strategy");
8711 anyhow::ensure!(!self.start_fails, "Strategy start failed");
8712 Ok(())
8713 }
8714
8715 fn on_order_rejected(&mut self, _event: OrderRejected) {}
8716 });
8717
8718 impl DataActor for MacroTestWithHooks {
8719 fn on_start(&mut self) -> anyhow::Result<()> {
8720 self.start_trace.push("actor");
8721 Ok(())
8722 }
8723 }
8724
8725 #[rstest]
8726 #[case::success(false)]
8727 #[case::failure(true)]
8728 fn test_component_start_dispatches_strategy_before_actor(#[case] start_fails: bool) {
8729 let mut strategy = MacroTestWithHooks {
8730 core: create_test_strategy().core,
8731 start_trace: Vec::new(),
8732 start_fails,
8733 };
8734
8735 strategy.initialize().unwrap();
8736
8737 let result = Component::start(&mut strategy);
8738
8739 if start_fails {
8740 assert_eq!(result.unwrap_err().to_string(), "Strategy start failed");
8741 assert_eq!(strategy.state(), ComponentState::Starting);
8742 assert_eq!(strategy.start_trace, vec!["strategy"]);
8743 } else {
8744 result.unwrap();
8745 assert_eq!(strategy.state(), ComponentState::Running);
8746 assert_eq!(strategy.start_trace, vec!["strategy", "actor"]);
8747 }
8748 }
8749
8750 #[derive(Debug)]
8751 struct MacroTestCustomField {
8752 inner: StrategyCore,
8753 }
8754
8755 nautilus_strategy!(MacroTestCustomField, inner, {
8756 fn external_order_instrument_ids(&self) -> Option<Vec<InstrumentId>> {
8757 None
8758 }
8759 });
8760
8761 impl DataActor for MacroTestCustomField {}
8762
8763 #[rstest]
8764 fn test_strategy_behavior_does_not_require_native_core_access() {
8765 fn assert_strategy<T: Strategy + DataActor>() {}
8766
8767 assert_strategy::<CoreFreeStrategy>();
8768
8769 let mut strategy = CoreFreeStrategy { started: false };
8770 DataActor::on_start(&mut strategy).unwrap();
8771
8772 assert!(strategy.started);
8773 }
8774
8775 #[rstest]
8776 fn test_nautilus_strategy_macro_forms() {
8777 let config = StrategyConfig {
8778 strategy_id: Some(StrategyId::from("MACRO-001")),
8779 order_id_tag: Some("001".to_string()),
8780 ..Default::default()
8781 };
8782
8783 let simple = MacroTestSimple {
8784 core: StrategyCore::new(config.clone()),
8785 };
8786 assert_eq!(simple.strategy_id(), config.strategy_id);
8787 assert_eq!(simple.config().order_id_tag, config.order_id_tag);
8788 assert_eq!(simple.actor_id(), ActorId::from("MACRO-001"));
8789
8790 let hooks = MacroTestWithHooks {
8791 core: StrategyCore::new(config.clone()),
8792 start_trace: Vec::new(),
8793 start_fails: false,
8794 };
8795 assert_eq!(hooks.strategy_id(), config.strategy_id);
8796 assert_eq!(hooks.config().order_id_tag, config.order_id_tag);
8797 assert_eq!(hooks.actor_id(), ActorId::from("MACRO-001"));
8798
8799 let custom = MacroTestCustomField {
8800 inner: StrategyCore::new(config.clone()),
8801 };
8802 assert_eq!(custom.strategy_id(), config.strategy_id);
8803 assert_eq!(custom.config().order_id_tag, config.order_id_tag);
8804 assert_eq!(custom.actor_id(), ActorId::from("MACRO-001"));
8805 assert!(custom.external_order_instrument_ids().is_none());
8806 }
8807
8808 #[rstest]
8809 fn test_cancel_all_orders_returns_all_errors_without_logging() {
8810 let _guard = init_logging(
8811 TraderId::from("TRADER-001"),
8812 UUID4::new(),
8813 Default::default(),
8814 Default::default(),
8815 )
8816 .unwrap();
8817 let mut strategy = create_test_strategy();
8818 register_strategy(&mut strategy);
8819 let (handler, messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
8820 get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
8821 msgbus::register_trading_command_endpoint(
8822 MessagingSwitchboard::exec_engine_queue_execute(),
8823 handler,
8824 );
8825 let mut first = make_accepted_market_order("O-CANCEL-001");
8826 let mut second = make_accepted_market_order("O-CANCEL-002");
8827 for order in [&mut first, &mut second] {
8828 let OrderAny::Market(order) = order else {
8829 unreachable!()
8830 };
8831
8832 order.account_id = None;
8833 }
8834
8835 let third = make_accepted_market_order("O-CANCEL-003");
8836 for order in [&first, &second, &third] {
8837 add_order_to_cache(&strategy, order);
8838 }
8839
8840 strategy.core.cache_rc().borrow_mut().build_index();
8841 arm_shutdown_on_error(true);
8842 let result = strategy.cancel_all_orders(first.instrument_id(), None, None, true, None);
8843 let trigger = take_shutdown_on_error_trigger();
8844 disarm_shutdown_on_error();
8845
8846 assert_eq!(
8847 result.unwrap_err().to_string(),
8848 "Cannot generate pending cancel event for O-CANCEL-001: account_id is not set; Cannot generate pending cancel event for O-CANCEL-002: account_id is not set"
8849 );
8850 assert_eq!(trigger, None);
8851 let messages = messages.get_messages();
8852 assert_eq!(messages.len(), 1);
8853 assert!(
8854 matches!(&messages[0], TradingCommand::CancelOrder(command) if command.client_order_id == third.client_order_id())
8855 );
8856 let cache = strategy.cache();
8857 assert_eq!(
8858 cache.order(&first.client_order_id()).unwrap().status(),
8859 OrderStatus::Accepted
8860 );
8861 assert_eq!(
8862 cache.order(&second.client_order_id()).unwrap().status(),
8863 OrderStatus::Accepted
8864 );
8865 assert_eq!(
8866 cache.order(&third.client_order_id()).unwrap().status(),
8867 OrderStatus::PendingCancel
8868 );
8869 }
8870}