1pub mod config;
39pub mod core;
40pub mod twap;
41
42pub use core::ExecutionAlgorithmCore;
43
44pub use config::ExecutionAlgorithmConfig;
45use nautilus_common::{
46 actor::DataActor,
47 enums::ComponentState,
48 logging::{CMD, EVT, RECV, SEND},
49 messages::execution::{CancelOrder, ModifyOrder, SubmitOrder, TradingCommand},
50 msgbus,
51 timer::TimeEvent,
52};
53use nautilus_core::{UUID4, UnixNanos};
54use nautilus_model::{
55 enums::{OrderStatus, TimeInForce, TriggerType},
56 events::{
57 OrderAccepted, OrderCancelRejected, OrderCanceled, OrderDenied, OrderEmulated,
58 OrderEventAny, OrderExpired, OrderFilled, OrderInitialized, OrderModifyRejected,
59 OrderPendingCancel, OrderPendingUpdate, OrderRejected, OrderReleased, OrderSubmitted,
60 OrderTriggered, OrderUpdated, PositionChanged, PositionClosed, PositionEvent,
61 PositionOpened,
62 },
63 identifiers::{ClientId, ExecAlgorithmId, PositionId, StrategyId},
64 orders::{LimitOrder, MarketOrder, MarketToLimitOrder, Order, OrderAny, OrderList},
65 types::{Price, Quantity},
66};
67pub use twap::{TwapAlgorithm, TwapAlgorithmConfig};
68use ustr::Ustr;
69
70pub trait ExecutionAlgorithm: DataActor {
89 fn core_mut(&mut self) -> &mut ExecutionAlgorithmCore;
91
92 fn id(&mut self) -> ExecAlgorithmId {
94 self.core_mut().exec_algorithm_id
95 }
96
97 fn execute(&mut self, command: TradingCommand) -> anyhow::Result<()> {
108 let core = self.core_mut();
109 if core.config.log_commands {
110 let id = &core.actor.actor_id;
111 log::info!("{id} {RECV}{CMD} {command:?}");
112 }
113
114 if core.state() != ComponentState::Running {
115 return Ok(());
116 }
117
118 match command {
119 TradingCommand::SubmitOrder(cmd) => {
120 self.subscribe_to_strategy_events(cmd.strategy_id);
121 self.on_order(cmd.order)
122 }
123 TradingCommand::SubmitOrderList(cmd) => {
124 self.subscribe_to_strategy_events(cmd.strategy_id);
125 self.on_order_list(cmd.order_list)
126 }
127 TradingCommand::CancelOrder(cmd) => self.handle_cancel_order(cmd),
128 _ => {
129 log::warn!("Unhandled command type: {command:?}");
130 Ok(())
131 }
132 }
133 }
134
135 fn on_order(&mut self, order: OrderAny) -> anyhow::Result<()>;
143
144 fn on_order_list(&mut self, order_list: OrderList) -> anyhow::Result<()> {
153 for order in order_list.orders {
154 self.on_order(order)?;
155 }
156 Ok(())
157 }
158
159 fn handle_cancel_order(&mut self, command: CancelOrder) -> anyhow::Result<()> {
168 let (mut order, is_pending_cancel) = {
169 let cache = self.core_mut().cache();
170
171 let Some(order) = cache.order(&command.client_order_id) else {
172 log::warn!(
173 "Cannot cancel order: {} not found in cache",
174 command.client_order_id
175 );
176 return Ok(());
177 };
178
179 let is_pending = cache.is_order_pending_cancel_local(&command.client_order_id);
180 (order.clone(), is_pending)
181 };
182
183 if is_pending_cancel {
184 return Ok(());
185 }
186
187 if order.is_closed() {
188 log::warn!("Order already closed for {command:?}");
189 return Ok(());
190 }
191
192 let event = self.generate_order_canceled(&order);
193
194 if let Err(e) = order.apply(OrderEventAny::Canceled(event)) {
195 log::warn!("InvalidStateTrigger: {e}, did not apply cancel event");
196 return Ok(());
197 }
198
199 {
200 let cache_rc = self.core_mut().cache_rc();
201 let mut cache = cache_rc.borrow_mut();
202 cache.update_order(&order)?;
203 }
204
205 let topic = format!("events.order.{}", order.strategy_id());
206 msgbus::publish(topic.into(), &event);
207
208 Ok(())
209 }
210
211 fn generate_order_canceled(&mut self, order: &OrderAny) -> OrderCanceled {
213 let ts_now = self.core_mut().clock().timestamp_ns();
214
215 OrderCanceled::new(
216 order.trader_id(),
217 order.strategy_id(),
218 order.instrument_id(),
219 order.client_order_id(),
220 UUID4::new(),
221 ts_now,
222 ts_now,
223 false, order.venue_order_id(),
225 order.account_id(),
226 )
227 }
228
229 fn spawn_market(
240 &mut self,
241 primary: &mut OrderAny,
242 quantity: Quantity,
243 time_in_force: TimeInForce,
244 reduce_only: bool,
245 tags: Option<Vec<Ustr>>,
246 reduce_primary: bool,
247 ) -> MarketOrder {
248 if reduce_primary {
249 self.reduce_primary_order(primary, quantity);
250 }
251
252 let core = self.core_mut();
253 let client_order_id = core.spawn_client_order_id(&primary.client_order_id());
254 let ts_init = core.clock().timestamp_ns();
255 let exec_algorithm_id = core.exec_algorithm_id;
256
257 MarketOrder::new(
258 primary.trader_id(),
259 primary.strategy_id(),
260 primary.instrument_id(),
261 client_order_id,
262 primary.order_side(),
263 quantity,
264 time_in_force,
265 UUID4::new(),
266 ts_init,
267 reduce_only,
268 false, primary.contingency_type(),
270 primary.order_list_id(),
271 primary.linked_order_ids().map(|ids| ids.to_vec()),
272 primary.parent_order_id(),
273 Some(exec_algorithm_id),
274 primary.exec_algorithm_params().cloned(),
275 Some(primary.client_order_id()),
276 tags.or_else(|| primary.tags().map(|t| t.to_vec())),
277 )
278 }
279
280 #[allow(clippy::too_many_arguments)]
291 fn spawn_limit(
292 &mut self,
293 primary: &mut OrderAny,
294 quantity: Quantity,
295 price: Price,
296 time_in_force: TimeInForce,
297 expire_time: Option<UnixNanos>,
298 post_only: bool,
299 reduce_only: bool,
300 display_qty: Option<Quantity>,
301 emulation_trigger: Option<TriggerType>,
302 tags: Option<Vec<Ustr>>,
303 reduce_primary: bool,
304 ) -> LimitOrder {
305 if reduce_primary {
306 self.reduce_primary_order(primary, quantity);
307 }
308
309 let core = self.core_mut();
310 let client_order_id = core.spawn_client_order_id(&primary.client_order_id());
311 let ts_init = core.clock().timestamp_ns();
312 let exec_algorithm_id = core.exec_algorithm_id;
313
314 LimitOrder::new(
315 primary.trader_id(),
316 primary.strategy_id(),
317 primary.instrument_id(),
318 client_order_id,
319 primary.order_side(),
320 quantity,
321 price,
322 time_in_force,
323 expire_time,
324 post_only,
325 reduce_only,
326 false, display_qty,
328 emulation_trigger,
329 None, primary.contingency_type(),
331 primary.order_list_id(),
332 primary.linked_order_ids().map(|ids| ids.to_vec()),
333 primary.parent_order_id(),
334 Some(exec_algorithm_id),
335 primary.exec_algorithm_params().cloned(),
336 Some(primary.client_order_id()),
337 tags.or_else(|| primary.tags().map(|t| t.to_vec())),
338 UUID4::new(),
339 ts_init,
340 )
341 }
342
343 #[allow(clippy::too_many_arguments)]
354 fn spawn_market_to_limit(
355 &mut self,
356 primary: &mut OrderAny,
357 quantity: Quantity,
358 time_in_force: TimeInForce,
359 expire_time: Option<UnixNanos>,
360 post_only: bool,
361 reduce_only: bool,
362 display_qty: Option<Quantity>,
363 tags: Option<Vec<Ustr>>,
364 reduce_primary: bool,
365 ) -> MarketToLimitOrder {
366 if reduce_primary {
367 self.reduce_primary_order(primary, quantity);
368 }
369
370 let core = self.core_mut();
371 let client_order_id = core.spawn_client_order_id(&primary.client_order_id());
372 let ts_init = core.clock().timestamp_ns();
373 let exec_algorithm_id = core.exec_algorithm_id;
374
375 MarketToLimitOrder::new(
376 primary.trader_id(),
377 primary.strategy_id(),
378 primary.instrument_id(),
379 client_order_id,
380 primary.order_side(),
381 quantity,
382 time_in_force,
383 expire_time,
384 post_only,
385 reduce_only,
386 false, display_qty,
388 primary.contingency_type(),
389 primary.order_list_id(),
390 primary.linked_order_ids().map(|ids| ids.to_vec()),
391 primary.parent_order_id(),
392 Some(exec_algorithm_id),
393 primary.exec_algorithm_params().cloned(),
394 Some(primary.client_order_id()),
395 tags.or_else(|| primary.tags().map(|t| t.to_vec())),
396 UUID4::new(),
397 ts_init,
398 )
399 }
400
401 fn reduce_primary_order(&mut self, primary: &mut OrderAny, spawn_qty: Quantity) {
406 let primary_qty = primary.quantity();
407 assert!(
408 primary_qty >= spawn_qty,
409 "Spawn order quantity must be less than or equal to primary order quantity"
410 );
411
412 let new_qty = Quantity::from_raw(primary_qty.raw - spawn_qty.raw, primary_qty.precision);
413
414 let core = self.core_mut();
415 let ts_now = core.clock().timestamp_ns();
416
417 let updated = OrderUpdated::new(
418 primary.trader_id(),
419 primary.strategy_id(),
420 primary.instrument_id(),
421 primary.client_order_id(),
422 new_qty,
423 UUID4::new(),
424 ts_now,
425 ts_now,
426 false, primary.venue_order_id(),
428 primary.account_id(),
429 None, None, None, );
433
434 primary
435 .apply(OrderEventAny::Updated(updated))
436 .expect("Failed to apply OrderUpdated");
437
438 let cache_rc = core.cache_rc();
439 let mut cache = cache_rc.borrow_mut();
440 cache
441 .update_order(primary)
442 .expect("Failed to update order in cache");
443 }
444
445 fn submit_order(
451 &mut self,
452 order: OrderAny,
453 position_id: Option<PositionId>,
454 client_id: Option<ClientId>,
455 ) -> anyhow::Result<()> {
456 let core = self.core_mut();
457
458 let trader_id = core.trader_id().expect("Trader ID not set");
459 let ts_init = core.clock().timestamp_ns();
460
461 let strategy_id = order.strategy_id();
463
464 {
465 let cache_rc = core.cache_rc();
466 let mut cache = cache_rc.borrow_mut();
467 cache.add_order(order.clone(), position_id, client_id, true)?;
468 }
469
470 let command = SubmitOrder::new(
471 trader_id,
472 client_id,
473 strategy_id,
474 order.instrument_id(),
475 order.clone(),
476 order.exec_algorithm_id(),
477 position_id,
478 None, UUID4::new(),
480 ts_init,
481 );
482
483 if core.config.log_commands {
484 let id = &core.actor.actor_id;
485 log::info!("{id} {SEND}{CMD} {command:?}");
486 }
487
488 msgbus::send_any(
489 "RiskEngine.execute".into(),
490 &TradingCommand::SubmitOrder(command),
491 );
492
493 Ok(())
494 }
495
496 fn modify_order(
502 &mut self,
503 order: &OrderAny,
504 quantity: Option<Quantity>,
505 price: Option<Price>,
506 trigger_price: Option<Price>,
507 client_id: Option<ClientId>,
508 ) -> anyhow::Result<()> {
509 let core = self.core_mut();
510
511 let trader_id = core.trader_id().expect("Trader ID not set");
512 let strategy_id = order.strategy_id();
513 let ts_init = core.clock().timestamp_ns();
514
515 let command = ModifyOrder::new(
516 trader_id,
517 client_id,
518 strategy_id,
519 order.instrument_id(),
520 order.client_order_id(),
521 order.venue_order_id(),
522 quantity,
523 price,
524 trigger_price,
525 UUID4::new(),
526 ts_init,
527 None, );
529
530 if core.config.log_commands {
531 let id = &core.actor.actor_id;
532 log::info!("{id} {SEND}{CMD} {command:?}");
533 }
534
535 if order.is_emulated() {
537 msgbus::send_any(
538 "OrderEmulator.execute".into(),
539 &TradingCommand::ModifyOrder(command),
540 );
541 } else {
542 msgbus::send_any(
543 "ExecEngine.execute".into(),
544 &TradingCommand::ModifyOrder(command),
545 );
546 }
547
548 Ok(())
549 }
550
551 fn modify_order_in_place(
563 &mut self,
564 order: &mut OrderAny,
565 quantity: Option<Quantity>,
566 price: Option<Price>,
567 trigger_price: Option<Price>,
568 ) -> anyhow::Result<()> {
569 let status = order.status();
571 if status != OrderStatus::Initialized && status != OrderStatus::Released {
572 anyhow::bail!(
573 "Cannot modify order in place: status is {status:?}, expected INITIALIZED or RELEASED"
574 );
575 }
576
577 let qty_changing = quantity.is_some_and(|q| q != order.quantity());
579 let price_changing = price.is_some() && price != order.price();
580 let trigger_changing = trigger_price.is_some() && trigger_price != order.trigger_price();
581
582 if !qty_changing && !price_changing && !trigger_changing {
583 anyhow::bail!("Cannot modify order in place: no parameters differ from current values");
584 }
585
586 let core = self.core_mut();
587 let ts_now = core.clock().timestamp_ns();
588
589 let updated = OrderUpdated::new(
590 order.trader_id(),
591 order.strategy_id(),
592 order.instrument_id(),
593 order.client_order_id(),
594 quantity.unwrap_or_else(|| order.quantity()),
595 UUID4::new(),
596 ts_now,
597 ts_now,
598 false, order.venue_order_id(),
600 order.account_id(),
601 price,
602 trigger_price,
603 None, );
605
606 order
607 .apply(OrderEventAny::Updated(updated))
608 .map_err(|e| anyhow::anyhow!("Failed to apply OrderUpdated: {e}"))?;
609
610 let cache_rc = core.cache_rc();
611 let mut cache = cache_rc.borrow_mut();
612 cache.update_order(order)?;
613
614 Ok(())
615 }
616
617 fn cancel_order(
623 &mut self,
624 order: &OrderAny,
625 client_id: Option<ClientId>,
626 ) -> anyhow::Result<()> {
627 let core = self.core_mut();
628
629 let trader_id = core.trader_id().expect("Trader ID not set");
630 let strategy_id = order.strategy_id();
631 let ts_init = core.clock().timestamp_ns();
632
633 let command = CancelOrder::new(
634 trader_id,
635 client_id,
636 strategy_id,
637 order.instrument_id(),
638 order.client_order_id(),
639 order.venue_order_id(),
640 UUID4::new(),
641 ts_init,
642 None, );
644
645 if core.config.log_commands {
646 let id = &core.actor.actor_id;
647 log::info!("{id} {SEND}{CMD} {command:?}");
648 }
649
650 if order.is_emulated()
652 || matches!(order.emulation_trigger(), Some(t) if t != TriggerType::NoTrigger)
653 {
654 msgbus::send_any(
655 "OrderEmulator.execute".into(),
656 &TradingCommand::CancelOrder(command),
657 );
658 } else {
659 msgbus::send_any(
660 "ExecEngine.execute".into(),
661 &TradingCommand::CancelOrder(command),
662 );
663 }
664
665 Ok(())
666 }
667
668 fn subscribe_to_strategy_events(&mut self, strategy_id: StrategyId) {
672 let core = self.core_mut();
673 if core.is_strategy_subscribed(&strategy_id) {
674 return;
675 }
676
677 core.add_subscribed_strategy(strategy_id);
685 log::debug!("Subscribed to events for strategy {strategy_id}");
686 }
687
688 fn handle_order_event(&mut self, event: OrderEventAny) {
690 let order = {
692 let cache = self.core_mut().cache();
693 cache.order(&event.client_order_id()).cloned()
694 };
695
696 let Some(order) = order else {
697 return;
698 };
699
700 let Some(order_algo_id) = order.exec_algorithm_id() else {
702 return;
703 };
704
705 if order_algo_id != self.id() {
706 return;
707 }
708
709 {
710 let core = self.core_mut();
711 if core.config.log_events {
712 let id = &core.actor.actor_id;
713 log::info!("{id} {RECV}{EVT} {event}");
714 }
715 }
716
717 match &event {
718 OrderEventAny::Initialized(e) => self.on_order_initialized(e.clone()),
719 OrderEventAny::Denied(e) => self.on_order_denied(*e),
720 OrderEventAny::Emulated(e) => self.on_order_emulated(*e),
721 OrderEventAny::Released(e) => self.on_order_released(*e),
722 OrderEventAny::Submitted(e) => self.on_order_submitted(*e),
723 OrderEventAny::Rejected(e) => self.on_order_rejected(*e),
724 OrderEventAny::Accepted(e) => self.on_order_accepted(*e),
725 OrderEventAny::Canceled(e) => self.on_algo_order_canceled(*e),
726 OrderEventAny::Expired(e) => self.on_order_expired(*e),
727 OrderEventAny::Triggered(e) => self.on_order_triggered(*e),
728 OrderEventAny::PendingUpdate(e) => self.on_order_pending_update(*e),
729 OrderEventAny::PendingCancel(e) => self.on_order_pending_cancel(*e),
730 OrderEventAny::ModifyRejected(e) => self.on_order_modify_rejected(*e),
731 OrderEventAny::CancelRejected(e) => self.on_order_cancel_rejected(*e),
732 OrderEventAny::Updated(e) => self.on_order_updated(*e),
733 OrderEventAny::Filled(e) => self.on_algo_order_filled(*e),
734 }
735
736 self.on_order_event(event);
737 }
738
739 fn handle_position_event(&mut self, event: PositionEvent) {
741 {
742 let core = self.core_mut();
743 if core.config.log_events {
744 let id = &core.actor.actor_id;
745 log::info!("{id} {RECV}{EVT} {event:?}");
746 }
747 }
748
749 match &event {
750 PositionEvent::PositionOpened(e) => self.on_position_opened(e.clone()),
751 PositionEvent::PositionChanged(e) => self.on_position_changed(e.clone()),
752 PositionEvent::PositionClosed(e) => self.on_position_closed(e.clone()),
753 PositionEvent::PositionAdjusted(_) => {}
754 }
755
756 self.on_position_event(event);
757 }
758
759 fn on_start(&mut self) -> anyhow::Result<()> {
767 let id = self.id();
768 log::info!("Starting {id}");
769 Ok(())
770 }
771
772 fn on_stop(&mut self) -> anyhow::Result<()> {
778 Ok(())
779 }
780
781 fn on_reset(&mut self) -> anyhow::Result<()> {
787 self.core_mut().reset();
788 Ok(())
789 }
790
791 fn on_time_event(&mut self, _event: &TimeEvent) -> anyhow::Result<()> {
799 Ok(())
800 }
801
802 #[allow(unused_variables)]
804 fn on_order_initialized(&mut self, event: OrderInitialized) {}
805
806 #[allow(unused_variables)]
808 fn on_order_denied(&mut self, event: OrderDenied) {}
809
810 #[allow(unused_variables)]
812 fn on_order_emulated(&mut self, event: OrderEmulated) {}
813
814 #[allow(unused_variables)]
816 fn on_order_released(&mut self, event: OrderReleased) {}
817
818 #[allow(unused_variables)]
820 fn on_order_submitted(&mut self, event: OrderSubmitted) {}
821
822 #[allow(unused_variables)]
824 fn on_order_rejected(&mut self, event: OrderRejected) {}
825
826 #[allow(unused_variables)]
828 fn on_order_accepted(&mut self, event: OrderAccepted) {}
829
830 #[allow(unused_variables)]
832 fn on_algo_order_canceled(&mut self, event: OrderCanceled) {}
833
834 #[allow(unused_variables)]
836 fn on_order_expired(&mut self, event: OrderExpired) {}
837
838 #[allow(unused_variables)]
840 fn on_order_triggered(&mut self, event: OrderTriggered) {}
841
842 #[allow(unused_variables)]
844 fn on_order_pending_update(&mut self, event: OrderPendingUpdate) {}
845
846 #[allow(unused_variables)]
848 fn on_order_pending_cancel(&mut self, event: OrderPendingCancel) {}
849
850 #[allow(unused_variables)]
852 fn on_order_modify_rejected(&mut self, event: OrderModifyRejected) {}
853
854 #[allow(unused_variables)]
856 fn on_order_cancel_rejected(&mut self, event: OrderCancelRejected) {}
857
858 #[allow(unused_variables)]
860 fn on_order_updated(&mut self, event: OrderUpdated) {}
861
862 #[allow(unused_variables)]
864 fn on_algo_order_filled(&mut self, event: OrderFilled) {}
865
866 #[allow(unused_variables)]
868 fn on_order_event(&mut self, event: OrderEventAny) {}
869
870 #[allow(unused_variables)]
872 fn on_position_opened(&mut self, event: PositionOpened) {}
873
874 #[allow(unused_variables)]
876 fn on_position_changed(&mut self, event: PositionChanged) {}
877
878 #[allow(unused_variables)]
880 fn on_position_closed(&mut self, event: PositionClosed) {}
881
882 #[allow(unused_variables)]
884 fn on_position_event(&mut self, event: PositionEvent) {}
885}
886
887#[cfg(test)]
888mod tests {
889 use std::{
890 cell::RefCell,
891 ops::{Deref, DerefMut},
892 rc::Rc,
893 };
894
895 use nautilus_common::{
896 actor::{DataActor, DataActorCore},
897 cache::Cache,
898 clock::TestClock,
899 };
900 use nautilus_model::{
901 enums::OrderSide,
902 identifiers::{ClientOrderId, ExecAlgorithmId, InstrumentId, StrategyId, TraderId},
903 orders::{MarketOrder, OrderAny, stubs::TestOrderStubs},
904 types::{Price, Quantity},
905 };
906 use rstest::rstest;
907
908 use super::*;
909
910 #[derive(Debug)]
911 struct TestAlgorithm {
912 core: ExecutionAlgorithmCore,
913 on_order_called: bool,
914 last_order_client_id: Option<ClientOrderId>,
915 }
916
917 impl TestAlgorithm {
918 fn new(config: ExecutionAlgorithmConfig) -> Self {
919 Self {
920 core: ExecutionAlgorithmCore::new(config),
921 on_order_called: false,
922 last_order_client_id: None,
923 }
924 }
925 }
926
927 impl Deref for TestAlgorithm {
928 type Target = DataActorCore;
929 fn deref(&self) -> &Self::Target {
930 &self.core.actor
931 }
932 }
933
934 impl DerefMut for TestAlgorithm {
935 fn deref_mut(&mut self) -> &mut Self::Target {
936 &mut self.core.actor
937 }
938 }
939
940 impl DataActor for TestAlgorithm {}
941
942 impl ExecutionAlgorithm for TestAlgorithm {
943 fn core_mut(&mut self) -> &mut ExecutionAlgorithmCore {
944 &mut self.core
945 }
946
947 fn on_order(&mut self, order: OrderAny) -> anyhow::Result<()> {
948 self.on_order_called = true;
949 self.last_order_client_id = Some(order.client_order_id());
950 Ok(())
951 }
952 }
953
954 fn create_test_algorithm() -> TestAlgorithm {
955 let unique_id = format!("TEST-{}", UUID4::new());
957 let config = ExecutionAlgorithmConfig {
958 exec_algorithm_id: Some(ExecAlgorithmId::new(&unique_id)),
959 ..Default::default()
960 };
961 TestAlgorithm::new(config)
962 }
963
964 fn register_algorithm(algo: &mut TestAlgorithm) {
965 let trader_id = TraderId::from("TRADER-001");
966 let clock = Rc::new(RefCell::new(TestClock::new()));
967 let cache = Rc::new(RefCell::new(Cache::default()));
968
969 algo.core.register(trader_id, clock, cache).unwrap();
970 }
971
972 #[rstest]
973 fn test_algorithm_creation() {
974 let algo = create_test_algorithm();
975 assert!(algo.core.exec_algorithm_id.inner().starts_with("TEST-"));
976 assert!(!algo.on_order_called);
977 assert!(algo.last_order_client_id.is_none());
978 }
979
980 #[rstest]
981 fn test_algorithm_registration() {
982 let mut algo = create_test_algorithm();
983 register_algorithm(&mut algo);
984
985 assert!(algo.core.trader_id().is_some());
986 assert_eq!(algo.core.trader_id(), Some(TraderId::from("TRADER-001")));
987 }
988
989 #[rstest]
990 fn test_algorithm_id() {
991 let mut algo = create_test_algorithm();
992 assert!(algo.id().inner().starts_with("TEST-"));
993 }
994
995 #[rstest]
996 fn test_algorithm_spawn_market_creates_valid_order() {
997 let mut algo = create_test_algorithm();
998 register_algorithm(&mut algo);
999
1000 let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
1001 let mut primary = OrderAny::Market(MarketOrder::new(
1002 TraderId::from("TRADER-001"),
1003 StrategyId::from("STRAT-001"),
1004 instrument_id,
1005 ClientOrderId::from("O-001"),
1006 OrderSide::Buy,
1007 Quantity::from("1.0"),
1008 TimeInForce::Gtc,
1009 UUID4::new(),
1010 0.into(),
1011 false, false, None, None, None, None, None, None, None, None, ));
1022
1023 let spawned = algo.spawn_market(
1024 &mut primary,
1025 Quantity::from("0.5"),
1026 TimeInForce::Ioc,
1027 false,
1028 None, false, );
1031
1032 assert_eq!(spawned.client_order_id.as_str(), "O-001-E1");
1033 assert_eq!(spawned.instrument_id, instrument_id);
1034 assert_eq!(spawned.order_side(), OrderSide::Buy);
1035 assert_eq!(spawned.quantity, Quantity::from("0.5"));
1036 assert_eq!(spawned.time_in_force, TimeInForce::Ioc);
1037 assert_eq!(spawned.exec_algorithm_id, Some(algo.id()));
1038 assert_eq!(spawned.exec_spawn_id, Some(ClientOrderId::from("O-001")));
1039 }
1040
1041 #[rstest]
1042 fn test_algorithm_spawn_increments_sequence() {
1043 let mut algo = create_test_algorithm();
1044 register_algorithm(&mut algo);
1045
1046 let mut primary = OrderAny::Market(MarketOrder::new(
1047 TraderId::from("TRADER-001"),
1048 StrategyId::from("STRAT-001"),
1049 InstrumentId::from("BTC/USDT.BINANCE"),
1050 ClientOrderId::from("O-001"),
1051 OrderSide::Buy,
1052 Quantity::from("1.0"),
1053 TimeInForce::Gtc,
1054 UUID4::new(),
1055 0.into(),
1056 false,
1057 false,
1058 None,
1059 None,
1060 None,
1061 None,
1062 None,
1063 None,
1064 None,
1065 None,
1066 ));
1067
1068 let spawned1 = algo.spawn_market(
1069 &mut primary,
1070 Quantity::from("0.25"),
1071 TimeInForce::Ioc,
1072 false,
1073 None,
1074 false,
1075 );
1076 let spawned2 = algo.spawn_market(
1077 &mut primary,
1078 Quantity::from("0.25"),
1079 TimeInForce::Ioc,
1080 false,
1081 None,
1082 false,
1083 );
1084 let spawned3 = algo.spawn_market(
1085 &mut primary,
1086 Quantity::from("0.25"),
1087 TimeInForce::Ioc,
1088 false,
1089 None,
1090 false,
1091 );
1092
1093 assert_eq!(spawned1.client_order_id.as_str(), "O-001-E1");
1094 assert_eq!(spawned2.client_order_id.as_str(), "O-001-E2");
1095 assert_eq!(spawned3.client_order_id.as_str(), "O-001-E3");
1096 }
1097
1098 #[rstest]
1099 fn test_algorithm_default_handlers_do_not_panic() {
1100 let mut algo = create_test_algorithm();
1101
1102 algo.on_order_initialized(Default::default());
1103 algo.on_order_denied(Default::default());
1104 algo.on_order_emulated(Default::default());
1105 algo.on_order_released(Default::default());
1106 algo.on_order_submitted(Default::default());
1107 algo.on_order_rejected(Default::default());
1108 algo.on_order_accepted(Default::default());
1109 algo.on_algo_order_canceled(Default::default());
1110 algo.on_order_expired(Default::default());
1111 algo.on_order_triggered(Default::default());
1112 algo.on_order_pending_update(Default::default());
1113 algo.on_order_pending_cancel(Default::default());
1114 algo.on_order_modify_rejected(Default::default());
1115 algo.on_order_cancel_rejected(Default::default());
1116 algo.on_order_updated(Default::default());
1117 algo.on_algo_order_filled(Default::default());
1118 }
1119
1120 #[rstest]
1121 fn test_strategy_subscription_tracking() {
1122 let mut algo = create_test_algorithm();
1123 let strategy_id = StrategyId::from("STRAT-001");
1124
1125 assert!(!algo.core.is_strategy_subscribed(&strategy_id));
1126
1127 algo.subscribe_to_strategy_events(strategy_id);
1128 assert!(algo.core.is_strategy_subscribed(&strategy_id));
1129
1130 algo.subscribe_to_strategy_events(strategy_id);
1132 assert!(algo.core.is_strategy_subscribed(&strategy_id));
1133 }
1134
1135 #[rstest]
1136 fn test_algorithm_reset() {
1137 let mut algo = create_test_algorithm();
1138 let strategy_id = StrategyId::from("STRAT-001");
1139 let primary_id = ClientOrderId::new("O-001");
1140
1141 let _ = algo.core.spawn_client_order_id(&primary_id);
1142 algo.core.add_subscribed_strategy(strategy_id);
1143
1144 assert!(algo.core.spawn_sequence(&primary_id).is_some());
1145 assert!(algo.core.is_strategy_subscribed(&strategy_id));
1146
1147 ExecutionAlgorithm::on_reset(&mut algo).unwrap();
1148
1149 assert!(algo.core.spawn_sequence(&primary_id).is_none());
1150 assert!(!algo.core.is_strategy_subscribed(&strategy_id));
1151 }
1152
1153 #[rstest]
1154 fn test_algorithm_spawn_limit_creates_valid_order() {
1155 let mut algo = create_test_algorithm();
1156 register_algorithm(&mut algo);
1157
1158 let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
1159 let mut primary = OrderAny::Market(MarketOrder::new(
1160 TraderId::from("TRADER-001"),
1161 StrategyId::from("STRAT-001"),
1162 instrument_id,
1163 ClientOrderId::from("O-001"),
1164 OrderSide::Buy,
1165 Quantity::from("1.0"),
1166 TimeInForce::Gtc,
1167 UUID4::new(),
1168 0.into(),
1169 false,
1170 false,
1171 None,
1172 None,
1173 None,
1174 None,
1175 None,
1176 None,
1177 None,
1178 None,
1179 ));
1180
1181 let price = Price::from("50000.0");
1182 let spawned = algo.spawn_limit(
1183 &mut primary,
1184 Quantity::from("0.5"),
1185 price,
1186 TimeInForce::Gtc,
1187 None, false, false, None, None, None, false, );
1195
1196 assert_eq!(spawned.client_order_id.as_str(), "O-001-E1");
1197 assert_eq!(spawned.instrument_id, instrument_id);
1198 assert_eq!(spawned.order_side(), OrderSide::Buy);
1199 assert_eq!(spawned.quantity, Quantity::from("0.5"));
1200 assert_eq!(spawned.price, price);
1201 assert_eq!(spawned.time_in_force, TimeInForce::Gtc);
1202 assert_eq!(spawned.exec_algorithm_id, Some(algo.id()));
1203 assert_eq!(spawned.exec_spawn_id, Some(ClientOrderId::from("O-001")));
1204 }
1205
1206 #[rstest]
1207 fn test_algorithm_spawn_market_to_limit_creates_valid_order() {
1208 let mut algo = create_test_algorithm();
1209 register_algorithm(&mut algo);
1210
1211 let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
1212 let mut primary = OrderAny::Market(MarketOrder::new(
1213 TraderId::from("TRADER-001"),
1214 StrategyId::from("STRAT-001"),
1215 instrument_id,
1216 ClientOrderId::from("O-001"),
1217 OrderSide::Buy,
1218 Quantity::from("1.0"),
1219 TimeInForce::Gtc,
1220 UUID4::new(),
1221 0.into(),
1222 false,
1223 false,
1224 None,
1225 None,
1226 None,
1227 None,
1228 None,
1229 None,
1230 None,
1231 None,
1232 ));
1233
1234 let spawned = algo.spawn_market_to_limit(
1235 &mut primary,
1236 Quantity::from("0.5"),
1237 TimeInForce::Gtc,
1238 None, false, false, None, None, false, );
1245
1246 assert_eq!(spawned.client_order_id.as_str(), "O-001-E1");
1247 assert_eq!(spawned.instrument_id, instrument_id);
1248 assert_eq!(spawned.order_side(), OrderSide::Buy);
1249 assert_eq!(spawned.quantity, Quantity::from("0.5"));
1250 assert_eq!(spawned.time_in_force, TimeInForce::Gtc);
1251 assert_eq!(spawned.exec_algorithm_id, Some(algo.id()));
1252 assert_eq!(spawned.exec_spawn_id, Some(ClientOrderId::from("O-001")));
1253 }
1254
1255 #[rstest]
1256 fn test_algorithm_spawn_market_with_tags() {
1257 let mut algo = create_test_algorithm();
1258 register_algorithm(&mut algo);
1259
1260 let mut primary = OrderAny::Market(MarketOrder::new(
1261 TraderId::from("TRADER-001"),
1262 StrategyId::from("STRAT-001"),
1263 InstrumentId::from("BTC/USDT.BINANCE"),
1264 ClientOrderId::from("O-001"),
1265 OrderSide::Buy,
1266 Quantity::from("1.0"),
1267 TimeInForce::Gtc,
1268 UUID4::new(),
1269 0.into(),
1270 false,
1271 false,
1272 None,
1273 None,
1274 None,
1275 None,
1276 None,
1277 None,
1278 None,
1279 None,
1280 ));
1281
1282 let tags = vec![ustr::Ustr::from("TAG1"), ustr::Ustr::from("TAG2")];
1283 let spawned = algo.spawn_market(
1284 &mut primary,
1285 Quantity::from("0.5"),
1286 TimeInForce::Ioc,
1287 false,
1288 Some(tags.clone()),
1289 false,
1290 );
1291
1292 assert_eq!(spawned.tags, Some(tags));
1293 }
1294
1295 #[rstest]
1296 fn test_algorithm_reduce_primary_order() {
1297 let mut algo = create_test_algorithm();
1298 register_algorithm(&mut algo);
1299
1300 let order = OrderAny::Market(MarketOrder::new(
1301 TraderId::from("TRADER-001"),
1302 StrategyId::from("STRAT-001"),
1303 InstrumentId::from("BTC/USDT.BINANCE"),
1304 ClientOrderId::from("O-001"),
1305 OrderSide::Buy,
1306 Quantity::from("1.0"),
1307 TimeInForce::Gtc,
1308 UUID4::new(),
1309 0.into(),
1310 false,
1311 false,
1312 None,
1313 None,
1314 None,
1315 None,
1316 None,
1317 None,
1318 None,
1319 None,
1320 ));
1321
1322 let mut primary = TestOrderStubs::make_accepted_order(&order);
1324
1325 {
1327 let cache_rc = algo.core.cache_rc();
1328 let mut cache = cache_rc.borrow_mut();
1329 cache.add_order(primary.clone(), None, None, false).unwrap();
1330 }
1331
1332 let spawn_qty = Quantity::from("0.3");
1333 algo.reduce_primary_order(&mut primary, spawn_qty);
1334
1335 assert_eq!(primary.quantity(), Quantity::from("0.7"));
1336 }
1337
1338 #[rstest]
1339 fn test_algorithm_spawn_market_with_reduce_primary() {
1340 let mut algo = create_test_algorithm();
1341 register_algorithm(&mut algo);
1342
1343 let order = OrderAny::Market(MarketOrder::new(
1344 TraderId::from("TRADER-001"),
1345 StrategyId::from("STRAT-001"),
1346 InstrumentId::from("BTC/USDT.BINANCE"),
1347 ClientOrderId::from("O-001"),
1348 OrderSide::Buy,
1349 Quantity::from("1.0"),
1350 TimeInForce::Gtc,
1351 UUID4::new(),
1352 0.into(),
1353 false,
1354 false,
1355 None,
1356 None,
1357 None,
1358 None,
1359 None,
1360 None,
1361 None,
1362 None,
1363 ));
1364
1365 let mut primary = TestOrderStubs::make_accepted_order(&order);
1367
1368 {
1370 let cache_rc = algo.core.cache_rc();
1371 let mut cache = cache_rc.borrow_mut();
1372 cache.add_order(primary.clone(), None, None, false).unwrap();
1373 }
1374
1375 let spawned = algo.spawn_market(
1376 &mut primary,
1377 Quantity::from("0.4"),
1378 TimeInForce::Ioc,
1379 false,
1380 None,
1381 true, );
1383
1384 assert_eq!(spawned.quantity, Quantity::from("0.4"));
1385 assert_eq!(primary.quantity(), Quantity::from("0.6"));
1386 }
1387
1388 #[rstest]
1389 fn test_algorithm_generate_order_canceled() {
1390 let mut algo = create_test_algorithm();
1391 register_algorithm(&mut algo);
1392
1393 let order = OrderAny::Market(MarketOrder::new(
1394 TraderId::from("TRADER-001"),
1395 StrategyId::from("STRAT-001"),
1396 InstrumentId::from("BTC/USDT.BINANCE"),
1397 ClientOrderId::from("O-001"),
1398 OrderSide::Buy,
1399 Quantity::from("1.0"),
1400 TimeInForce::Gtc,
1401 UUID4::new(),
1402 0.into(),
1403 false,
1404 false,
1405 None,
1406 None,
1407 None,
1408 None,
1409 None,
1410 None,
1411 None,
1412 None,
1413 ));
1414
1415 let event = algo.generate_order_canceled(&order);
1416
1417 assert_eq!(event.trader_id, TraderId::from("TRADER-001"));
1418 assert_eq!(event.strategy_id, StrategyId::from("STRAT-001"));
1419 assert_eq!(event.instrument_id, InstrumentId::from("BTC/USDT.BINANCE"));
1420 assert_eq!(event.client_order_id, ClientOrderId::from("O-001"));
1421 }
1422
1423 #[rstest]
1424 fn test_algorithm_modify_order_in_place_updates_quantity() {
1425 use nautilus_model::orders::LimitOrder;
1426
1427 let mut algo = create_test_algorithm();
1428 register_algorithm(&mut algo);
1429
1430 let mut order = OrderAny::Limit(LimitOrder::new(
1431 TraderId::from("TRADER-001"),
1432 StrategyId::from("STRAT-001"),
1433 InstrumentId::from("BTC/USDT.BINANCE"),
1434 ClientOrderId::from("O-001"),
1435 OrderSide::Buy,
1436 Quantity::from("1.0"),
1437 Price::from("50000.0"),
1438 TimeInForce::Gtc,
1439 None, false, false, false, None, None, None, None, None, None, None, None, None, None, None, UUID4::new(),
1455 0.into(),
1456 ));
1457
1458 {
1460 let cache_rc = algo.core.cache_rc();
1461 let mut cache = cache_rc.borrow_mut();
1462 cache.add_order(order.clone(), None, None, false).unwrap();
1463 }
1464
1465 let new_qty = Quantity::from("0.5");
1466 algo.modify_order_in_place(&mut order, Some(new_qty), None, None)
1467 .unwrap();
1468
1469 assert_eq!(order.quantity(), new_qty);
1470 }
1471
1472 #[rstest]
1473 fn test_algorithm_modify_order_in_place_rejects_no_changes() {
1474 use nautilus_model::orders::LimitOrder;
1475
1476 let mut algo = create_test_algorithm();
1477 register_algorithm(&mut algo);
1478
1479 let mut order = OrderAny::Limit(LimitOrder::new(
1480 TraderId::from("TRADER-001"),
1481 StrategyId::from("STRAT-001"),
1482 InstrumentId::from("BTC/USDT.BINANCE"),
1483 ClientOrderId::from("O-001"),
1484 OrderSide::Buy,
1485 Quantity::from("1.0"),
1486 Price::from("50000.0"),
1487 TimeInForce::Gtc,
1488 None,
1489 false,
1490 false,
1491 false,
1492 None,
1493 None,
1494 None,
1495 None,
1496 None,
1497 None,
1498 None,
1499 None,
1500 None,
1501 None,
1502 None,
1503 UUID4::new(),
1504 0.into(),
1505 ));
1506
1507 let result =
1509 algo.modify_order_in_place(&mut order, Some(Quantity::from("1.0")), None, None);
1510
1511 assert!(result.is_err());
1512 assert!(
1513 result
1514 .unwrap_err()
1515 .to_string()
1516 .contains("no parameters differ")
1517 );
1518 }
1519}