1use std::fmt::Display;
39
40pub mod config;
41pub mod core;
42pub mod twap;
43
44pub use core::{ExecutionAlgorithmCore, ExecutionAlgorithmNative, StrategyEventHandlers};
45
46pub use config::{ExecutionAlgorithmConfig, ImportableExecutionAlgorithmConfig};
47use nautilus_common::{
48 actor::{DataActor, DataActorNative, registry::try_get_actor_unchecked},
49 enums::ComponentState,
50 logging::{CMD, EVT, RECV, SEND},
51 messages::execution::{CancelOrder, ModifyOrder, SubmitOrder, TradingCommand},
52 msgbus::{self, MessagingSwitchboard, TypedHandler},
53 timer::TimeEvent,
54};
55use nautilus_core::{UUID4, UnixNanos};
56use nautilus_model::{
57 enums::{OrderStatus, TimeInForce, TriggerType},
58 events::{
59 OrderAccepted, OrderCancelRejected, OrderCanceled, OrderDenied, OrderEmulated,
60 OrderEventAny, OrderExpired, OrderFillVoided, OrderFilled, OrderInitialized,
61 OrderModifyRejected, OrderPendingCancel, OrderPendingUpdate, OrderRejected, OrderReleased,
62 OrderSubmitted, OrderTriggered, OrderUpdated, PositionChanged, PositionClosed,
63 PositionEvent, PositionOpened,
64 },
65 identifiers::{
66 AccountId, ClientId, ClientOrderId, ExecAlgorithmId, PositionId, StrategyId, TraderId,
67 },
68 orders::{LimitOrder, MarketOrder, MarketToLimitOrder, Order, OrderAny, OrderError, OrderList},
69 types::{Price, Quantity, quantity::QuantityRaw},
70};
71pub use twap::{TwapAlgorithm, TwapAlgorithmConfig};
72use ustr::Ustr;
73
74use crate::algorithm::core::SpawnReduction;
75
76pub trait ExecutionAlgorithm: DataActor {
104 fn id(&self) -> ExecAlgorithmId
106 where
107 Self: ExecutionAlgorithmNative,
108 {
109 ExecutionAlgorithmNative::exec_algorithm_core(self).exec_algorithm_id
110 }
111
112 fn execute(&mut self, command: TradingCommand) -> anyhow::Result<()>
123 where
124 Self: ExecutionAlgorithmNative,
125 Self: 'static + std::fmt::Debug + Sized,
126 {
127 let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
128 if core.config.log_commands {
129 let id = &core.actor.actor_id;
130 log::info!("{id} {RECV}{CMD} {command}");
131 }
132
133 if DataActorNative::core(core).state() != ComponentState::Running {
134 return Ok(());
135 }
136
137 match command {
138 TradingCommand::SubmitOrder(cmd) => {
139 self.subscribe_to_strategy_events(cmd.strategy_id);
140 let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
141 core.remember_submit_params(cmd.client_order_id, cmd.params.clone());
142 let order = core.get_order(&cmd.client_order_id)?;
143 self.on_order(order)
144 }
145 TradingCommand::SubmitOrderList(cmd) => {
146 self.subscribe_to_strategy_events(cmd.strategy_id);
147 let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
148 for client_order_id in &cmd.order_list.client_order_ids {
149 core.remember_submit_params(*client_order_id, cmd.params.clone());
150 }
151 let orders = core.get_orders_for_list(&cmd.order_list)?;
152 self.on_order_list(cmd.order_list, orders)
153 }
154 TradingCommand::ModifyOrder(cmd) => self.handle_modify_order(cmd),
155 TradingCommand::CancelOrder(cmd) => self.handle_cancel_order(cmd),
156 _ => {
157 log::warn!("Unhandled command type: {command}");
158 Ok(())
159 }
160 }
161 }
162
163 fn on_order(&mut self, order: OrderAny) -> anyhow::Result<()>;
171
172 fn on_order_list(
181 &mut self,
182 _order_list: OrderList,
183 orders: Vec<OrderAny>,
184 ) -> anyhow::Result<()> {
185 for order in orders {
186 self.on_order(order)?;
187 }
188 Ok(())
189 }
190
191 fn deny_order(&mut self, order: &OrderAny, reason: Ustr) -> anyhow::Result<()>
206 where
207 Self: ExecutionAlgorithmNative,
208 {
209 let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
210 registered_trader_id(core)?;
211 let ts_now = core.clock_mut().timestamp_ns();
212 let event = OrderEventAny::Denied(OrderDenied::new(
213 order.trader_id(),
214 order.strategy_id(),
215 order.instrument_id(),
216 order.client_order_id(),
217 reason,
218 UUID4::new(),
219 ts_now,
220 ts_now,
221 ));
222
223 let publish_initialized = {
224 let cache_rc = core.cache_rc();
225 let mut cache = cache_rc.borrow_mut();
226
227 if cache
228 .order(&order.client_order_id())
229 .is_some_and(|cached_order| cached_order.is_closed())
230 {
231 return Ok(());
232 }
233
234 let publish_initialized = if cache.order_exists(&order.client_order_id()) {
235 false
236 } else {
237 cache.add_order(order.clone(), None, None, false)?;
238 true
239 };
240
241 cache.update_order(&event)?;
242 publish_initialized
243 };
244
245 if publish_initialized {
246 publish_order_initialized(order);
247 }
248 publish_order_event(&event);
249
250 ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
253 .remove_submit_params(&order.client_order_id());
254
255 Ok(())
256 }
257
258 fn handle_cancel_order(&mut self, command: CancelOrder) -> anyhow::Result<()>
267 where
268 Self: ExecutionAlgorithmNative,
269 {
270 let (order, is_pending_cancel) = {
271 let cache = ExecutionAlgorithmNative::exec_algorithm_core_mut(self).cache_ref();
272
273 let Some(order) = cache.order(&command.client_order_id) else {
274 log::warn!(
275 "Cannot cancel order: {} not found in cache",
276 command.client_order_id
277 );
278 return Ok(());
279 };
280
281 let is_pending = cache.is_order_pending_cancel_local(&command.client_order_id);
282 (order.clone(), is_pending)
283 };
284
285 if is_pending_cancel {
286 return Ok(());
287 }
288
289 if order.is_closed() {
290 log::warn!("Order already closed for {command}");
291 return Ok(());
292 }
293
294 let event = OrderEventAny::Canceled(self.generate_order_canceled(&order));
295
296 let order = {
297 let cache_rc = ExecutionAlgorithmNative::exec_algorithm_core_mut(self).cache_rc();
298 let mut cache = cache_rc.borrow_mut();
299 match cache.update_order(&event) {
300 Ok(order) => order,
301 Err(e)
302 if matches!(
303 e.downcast_ref::<OrderError>(),
304 Some(OrderError::InvalidStateTransition)
305 ) =>
306 {
307 log::warn!("InvalidStateTrigger: {e}, did not apply cancel event");
308 return Ok(());
309 }
310 Err(e) => return Err(e),
311 }
312 };
313
314 let topic = format!("events.order.{}", order.strategy_id());
315 msgbus::publish_order_event(topic.into(), &event);
316 msgbus::publish_order_event(
317 msgbus::switchboard::get_order_canceled_topic(order.instrument_id()),
318 &event,
319 );
320
321 Ok(())
322 }
323
324 fn handle_modify_order(&mut self, command: ModifyOrder) -> anyhow::Result<()>
332 where
333 Self: ExecutionAlgorithmNative,
334 {
335 let (is_closed, is_active_local) = {
336 let cache = ExecutionAlgorithmNative::exec_algorithm_core_mut(self).cache_ref();
337
338 let Some(order) = cache.order(&command.client_order_id) else {
339 log::warn!(
340 "Cannot modify order: {} not found in cache",
341 command.client_order_id
342 );
343 return Ok(());
344 };
345
346 (order.is_closed(), order.is_active_local())
347 };
348
349 if is_closed {
350 log::warn!("Order already closed for {command}");
351 return Ok(());
352 }
353
354 if is_active_local {
355 log::warn!(
356 "Cannot modify {}: order is being executed by this algorithm",
357 command.client_order_id
358 );
359 return Ok(());
360 }
361
362 log::warn!(
364 "Cannot modify {}: order is not active-local",
365 command.client_order_id
366 );
367 Ok(())
368 }
369
370 fn generate_order_canceled(&mut self, order: &OrderAny) -> OrderCanceled
372 where
373 Self: ExecutionAlgorithmNative,
374 {
375 let ts_now = ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
376 .clock_mut()
377 .timestamp_ns();
378
379 OrderCanceled::new(
380 order.trader_id(),
381 order.strategy_id(),
382 order.instrument_id(),
383 order.client_order_id(),
384 UUID4::new(),
385 ts_now,
386 ts_now,
387 false, order.venue_order_id(),
389 order.account_id(),
390 None,
391 )
392 }
393
394 fn generate_order_pending_update(&mut self, order: &OrderAny) -> OrderPendingUpdate
396 where
397 Self: ExecutionAlgorithmNative,
398 {
399 let ts_now = ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
400 .clock_mut()
401 .timestamp_ns();
402
403 OrderPendingUpdate::new(
404 order.trader_id(),
405 order.strategy_id(),
406 order.instrument_id(),
407 order.client_order_id(),
408 order.account_id(),
409 UUID4::new(),
410 ts_now,
411 ts_now,
412 false, order.venue_order_id(),
414 )
415 }
416
417 fn generate_order_pending_cancel(&mut self, order: &OrderAny) -> OrderPendingCancel
419 where
420 Self: ExecutionAlgorithmNative,
421 {
422 let ts_now = ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
423 .clock_mut()
424 .timestamp_ns();
425
426 OrderPendingCancel::new(
427 order.trader_id(),
428 order.strategy_id(),
429 order.instrument_id(),
430 order.client_order_id(),
431 order.account_id(),
432 UUID4::new(),
433 ts_now,
434 ts_now,
435 false, order.venue_order_id(),
437 )
438 }
439
440 fn spawn_market(
455 &mut self,
456 primary: &mut OrderAny,
457 quantity: Quantity,
458 time_in_force: TimeInForce,
459 reduce_only: bool,
460 tags: Option<Vec<Ustr>>,
461 reduce_primary: bool,
462 ) -> MarketOrder
463 where
464 Self: ExecutionAlgorithmNative,
465 {
466 let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
468 let client_order_id = core.spawn_client_order_id(&primary.client_order_id());
469 let ts_init = core.clock_mut().timestamp_ns();
470 let exec_algorithm_id = core.exec_algorithm_id;
471
472 if reduce_primary {
473 self.reduce_primary_order(primary, quantity);
474 ExecutionAlgorithmNative::exec_algorithm_core_mut(self).track_pending_spawn_reduction(
475 client_order_id,
476 primary.client_order_id(),
477 quantity,
478 primary.is_quote_quantity(),
479 );
480 }
481
482 MarketOrder::new(
483 primary.trader_id(),
484 primary.strategy_id(),
485 primary.instrument_id(),
486 client_order_id,
487 primary.order_side(),
488 quantity,
489 time_in_force,
490 UUID4::new(),
491 ts_init,
492 reduce_only,
493 primary.is_quote_quantity(),
494 primary.contingency_type(),
495 primary.order_list_id(),
496 primary.linked_order_ids().map(<[ClientOrderId]>::to_vec),
497 primary.parent_order_id(),
498 Some(exec_algorithm_id),
499 primary.exec_algorithm_params().cloned(),
500 Some(primary.client_order_id()),
501 tags.or_else(|| primary.tags().map(<[Ustr]>::to_vec)),
502 )
503 }
504
505 #[expect(clippy::too_many_arguments)]
523 fn spawn_limit(
524 &mut self,
525 primary: &mut OrderAny,
526 quantity: Quantity,
527 price: Price,
528 time_in_force: TimeInForce,
529 expire_time: Option<UnixNanos>,
530 post_only: bool,
531 reduce_only: bool,
532 display_qty: Option<Quantity>,
533 emulation_trigger: Option<TriggerType>,
534 tags: Option<Vec<Ustr>>,
535 reduce_primary: bool,
536 ) -> LimitOrder
537 where
538 Self: ExecutionAlgorithmNative,
539 {
540 let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
542 let client_order_id = core.spawn_client_order_id(&primary.client_order_id());
543 let ts_init = core.clock_mut().timestamp_ns();
544 let exec_algorithm_id = core.exec_algorithm_id;
545
546 if reduce_primary {
547 self.reduce_primary_order(primary, quantity);
548 ExecutionAlgorithmNative::exec_algorithm_core_mut(self).track_pending_spawn_reduction(
549 client_order_id,
550 primary.client_order_id(),
551 quantity,
552 primary.is_quote_quantity(),
553 );
554 }
555
556 LimitOrder::new(
557 primary.trader_id(),
558 primary.strategy_id(),
559 primary.instrument_id(),
560 client_order_id,
561 primary.order_side(),
562 quantity,
563 price,
564 time_in_force,
565 expire_time,
566 post_only,
567 reduce_only,
568 primary.is_quote_quantity(),
569 display_qty,
570 emulation_trigger,
571 None, primary.contingency_type(),
573 primary.order_list_id(),
574 primary.linked_order_ids().map(<[ClientOrderId]>::to_vec),
575 primary.parent_order_id(),
576 Some(exec_algorithm_id),
577 primary.exec_algorithm_params().cloned(),
578 Some(primary.client_order_id()),
579 tags.or_else(|| primary.tags().map(<[Ustr]>::to_vec)),
580 UUID4::new(),
581 ts_init,
582 )
583 }
584
585 #[expect(clippy::too_many_arguments)]
604 fn spawn_market_to_limit(
605 &mut self,
606 primary: &mut OrderAny,
607 quantity: Quantity,
608 time_in_force: TimeInForce,
609 expire_time: Option<UnixNanos>,
610 reduce_only: bool,
611 display_qty: Option<Quantity>,
612 _emulation_trigger: Option<TriggerType>,
613 tags: Option<Vec<Ustr>>,
614 reduce_primary: bool,
615 ) -> MarketToLimitOrder
616 where
617 Self: ExecutionAlgorithmNative,
618 {
619 let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
621 let client_order_id = core.spawn_client_order_id(&primary.client_order_id());
622 let ts_init = core.clock_mut().timestamp_ns();
623 let exec_algorithm_id = core.exec_algorithm_id;
624
625 if reduce_primary {
626 self.reduce_primary_order(primary, quantity);
627 ExecutionAlgorithmNative::exec_algorithm_core_mut(self).track_pending_spawn_reduction(
628 client_order_id,
629 primary.client_order_id(),
630 quantity,
631 primary.is_quote_quantity(),
632 );
633 }
634
635 MarketToLimitOrder::new(
636 primary.trader_id(),
637 primary.strategy_id(),
638 primary.instrument_id(),
639 client_order_id,
640 primary.order_side(),
641 quantity,
642 time_in_force,
643 expire_time,
644 false, reduce_only,
646 primary.is_quote_quantity(),
647 display_qty,
648 primary.contingency_type(),
649 primary.order_list_id(),
650 primary.linked_order_ids().map(<[ClientOrderId]>::to_vec),
651 primary.parent_order_id(),
652 Some(exec_algorithm_id),
653 primary.exec_algorithm_params().cloned(),
654 Some(primary.client_order_id()),
655 tags.or_else(|| primary.tags().map(<[Ustr]>::to_vec)),
656 UUID4::new(),
657 ts_init,
658 )
659 }
660
661 fn reduce_primary_order(&mut self, primary: &mut OrderAny, spawn_qty: Quantity)
670 where
671 Self: ExecutionAlgorithmNative,
672 {
673 let leaves_qty = primary.leaves_qty();
674 assert!(
675 leaves_qty >= spawn_qty,
676 "Spawn quantity {spawn_qty} exceeds primary leaves_qty {leaves_qty}"
677 );
678
679 let primary_qty = primary.quantity();
680 let mut new_qty = primary_qty - spawn_qty;
681 new_qty.precision = primary_qty.precision;
682
683 let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
684 let ts_now = core.clock_mut().timestamp_ns();
685
686 let updated = OrderUpdated::new(
687 primary.trader_id(),
688 primary.strategy_id(),
689 primary.instrument_id(),
690 primary.client_order_id(),
691 new_qty,
692 UUID4::new(),
693 ts_now,
694 ts_now,
695 false, primary.venue_order_id(),
697 primary.account_id(),
698 None, None, None, primary.is_quote_quantity(),
702 );
703
704 let event = OrderEventAny::Updated(updated);
705
706 {
707 let cache_rc = core.cache_rc();
708 let mut cache = cache_rc.borrow_mut();
709 *primary = cache
710 .update_order(&event)
711 .expect("Failed to update order in cache");
712 }
713
714 publish_order_event(&event);
715 }
716
717 fn restore_primary_order_quantity(&mut self, order: &OrderAny, refused_before_submission: bool)
728 where
729 Self: ExecutionAlgorithmNative,
730 {
731 let Some(exec_spawn_id) = order.exec_spawn_id() else {
732 return;
733 };
734
735 let reduction = {
736 let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
737 core.spawn_reduction(order.client_order_id())
738 };
739
740 let Some(mut reduction) = reduction else {
741 return;
742 };
743
744 let primary = {
745 let cache = ExecutionAlgorithmNative::exec_algorithm_core_mut(self).cache_ref();
746 cache
747 .order(&exec_spawn_id)
748 .map(|o| <OrderAny as Clone>::clone(&o))
749 };
750
751 let Some(primary) = primary else {
752 ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
753 .take_pending_spawn_reduction(order.client_order_id());
754 log::warn!(
755 "Cannot restore primary order quantity: primary order {exec_spawn_id} not found",
756 );
757 return;
758 };
759
760 let handed_off = ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
761 .primary_was_handed_off(exec_spawn_id);
762
763 if !primary.is_active_local() || handed_off {
764 let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
765 core.take_pending_spawn_reduction(order.client_order_id());
766 core.discard_spawn_fill_debt(exec_spawn_id);
767 log::info!(
768 "Skipped restoring primary order {exec_spawn_id} after spawned order {}: primary is no longer locally mutable",
769 order.client_order_id(),
770 );
771 return;
772 }
773
774 let Some(unfilled_qty) =
775 spawn_unfilled_quantity(order, reduction, primary.quantity().precision)
776 else {
777 ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
778 .take_pending_spawn_reduction(order.client_order_id());
779 return;
780 };
781 let restored_qty = reduction
782 .restored_qty
783 .unwrap_or_else(|| Quantity::zero(unfilled_qty.precision));
784 let restore_qty = unfilled_qty.saturating_sub(restored_qty);
785 reduction.restored_qty = Some(unfilled_qty);
786
787 if restore_qty.is_zero() {
788 ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
789 .set_spawn_reduction(order.client_order_id(), reduction);
790 return;
791 }
792
793 let debt_qty = ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
796 .spawn_fill_debt(exec_spawn_id)
797 .unwrap_or_else(|| Quantity::zero(restore_qty.precision));
798 let discharge_qty = restore_qty.min(debt_qty);
799 let net_restore_qty = restore_qty - discharge_qty;
800
801 if net_restore_qty.is_zero() {
802 let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
805 core.set_spawn_reduction(order.client_order_id(), reduction);
806 core.set_spawn_fill_debt(exec_spawn_id, debt_qty - discharge_qty);
807 log::info!(
808 "Restoration from spawned order {} fully discharged late-fill debt on primary order {exec_spawn_id}",
809 order.client_order_id(),
810 );
811 return;
812 }
813
814 let mut restored_qty = primary.quantity() + net_restore_qty;
815 restored_qty.precision = primary.quantity().precision;
816
817 let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
818 let ts_now = core.clock_mut().timestamp_ns();
819
820 let updated = OrderUpdated::new(
821 primary.trader_id(),
822 primary.strategy_id(),
823 primary.instrument_id(),
824 primary.client_order_id(),
825 restored_qty,
826 UUID4::new(),
827 ts_now,
828 ts_now,
829 false, primary.venue_order_id(),
831 primary.account_id(),
832 None, None, None, primary.is_quote_quantity(),
836 );
837
838 let event = OrderEventAny::Updated(updated);
839
840 let primary = {
841 let cache_rc = core.cache_rc();
842 let mut cache = cache_rc.borrow_mut();
843 match cache.update_order(&event) {
844 Ok(primary) => primary,
845 Err(e) => {
846 log::warn!("Failed to update primary order in cache: {e}");
847 return;
848 }
849 }
850 };
851
852 let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
858 core.set_spawn_reduction(order.client_order_id(), reduction);
859 if !discharge_qty.is_zero() {
860 core.set_spawn_fill_debt(exec_spawn_id, debt_qty - discharge_qty);
861 }
862
863 publish_order_event(&event);
864
865 let outcome = if refused_before_submission {
866 "refused before submission"
867 } else {
868 "updated with unfilled quantity"
869 };
870 log::info!(
871 "Restored primary order {} quantity to {} after spawned order {} was {outcome}",
872 primary.client_order_id(),
873 restored_qty,
874 order.client_order_id()
875 );
876 }
877
878 fn rededuct_late_spawn_fill(&mut self, order: &OrderAny)
880 where
881 Self: ExecutionAlgorithmNative,
882 {
883 let spawn_id = order.client_order_id();
884 let Some(exec_spawn_id) = order.exec_spawn_id() else {
885 return;
886 };
887 let Some(reduction) =
888 ExecutionAlgorithmNative::exec_algorithm_core_mut(self).spawn_reduction(spawn_id)
889 else {
890 return;
891 };
892
893 let Some(restored_qty) = reduction.restored_qty else {
894 return;
895 };
896
897 let primary = {
898 let cache = ExecutionAlgorithmNative::exec_algorithm_core_mut(self).cache_ref();
899 cache
900 .order(&exec_spawn_id)
901 .map(|o| <OrderAny as Clone>::clone(&o))
902 };
903 let Some(primary) = primary else {
904 ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
905 .take_pending_spawn_reduction(spawn_id);
906 log::warn!(
907 "Cannot re-deduct late fill from primary order {exec_spawn_id}: order not found",
908 );
909 return;
910 };
911
912 let handed_off = ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
913 .primary_was_handed_off(exec_spawn_id);
914
915 if !primary.is_active_local() || handed_off {
916 let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
917 core.take_pending_spawn_reduction(spawn_id);
918 core.discard_spawn_fill_debt(exec_spawn_id);
919 log::info!(
920 "Skipped re-deducting late fill from primary order {exec_spawn_id}: primary is no longer locally mutable",
921 );
922 return;
923 }
924
925 let Some(unfilled_qty) =
926 spawn_unfilled_quantity(order, reduction, primary.quantity().precision)
927 else {
928 ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
929 .take_pending_spawn_reduction(spawn_id);
930 return;
931 };
932 let uncapped_qty = restored_qty.saturating_sub(unfilled_qty);
933 let primary_qty = primary.quantity();
934 let rededuct_qty = uncapped_qty.min(primary_qty);
938 let shortfall_qty = uncapped_qty - rededuct_qty;
939 if rededuct_qty.is_zero() {
940 if !uncapped_qty.is_zero() {
941 charge_spawn_reduction(
942 ExecutionAlgorithmNative::exec_algorithm_core_mut(self),
943 spawn_id,
944 exec_spawn_id,
945 reduction,
946 unfilled_qty,
947 shortfall_qty,
948 );
949 log::warn!(
950 "Cannot re-deduct late fill on spawned order {spawn_id} from primary order {exec_spawn_id}: primary quantity exhausted, shortfall recorded as debt",
951 );
952 }
953 return;
954 }
955 let mut new_qty = primary_qty - rededuct_qty;
956 new_qty.precision = primary_qty.precision;
957 let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
958 let ts_now = core.clock_mut().timestamp_ns();
959 let event = OrderEventAny::Updated(OrderUpdated::new(
960 primary.trader_id(),
961 primary.strategy_id(),
962 primary.instrument_id(),
963 primary.client_order_id(),
964 new_qty,
965 UUID4::new(),
966 ts_now,
967 ts_now,
968 false,
969 primary.venue_order_id(),
970 primary.account_id(),
971 None,
972 None,
973 None,
974 primary.is_quote_quantity(),
975 ));
976
977 if let Err(e) = core.cache_rc().borrow_mut().update_order(&event) {
978 log::warn!("Failed to update primary order in cache: {e}");
979 return;
980 }
981
982 charge_spawn_reduction(
985 ExecutionAlgorithmNative::exec_algorithm_core_mut(self),
986 spawn_id,
987 exec_spawn_id,
988 reduction,
989 unfilled_qty,
990 shortfall_qty,
991 );
992
993 if !shortfall_qty.is_zero() {
994 log::warn!(
995 "Late fill on spawned order {spawn_id} partially re-deducted from primary order {exec_spawn_id}: shortfall recorded as debt",
996 );
997 }
998
999 publish_order_event(&event);
1000 }
1001
1002 fn submit_order(
1013 &mut self,
1014 order: OrderAny,
1015 position_id: Option<PositionId>,
1016 client_id: Option<ClientId>,
1017 ) -> anyhow::Result<()>
1018 where
1019 Self: ExecutionAlgorithmNative,
1020 {
1021 let trader_id =
1022 registered_trader_id(ExecutionAlgorithmNative::exec_algorithm_core_mut(self))?;
1023
1024 if order.emulation_trigger().is_some() {
1025 let client_order_id = order.client_order_id();
1026 self.restore_primary_order_quantity(&order, true);
1027 return Err(EmulatedOrderSubmissionError { client_order_id }.into());
1028 }
1029
1030 let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
1031 let ts_init = core.clock_mut().timestamp_ns();
1032
1033 let strategy_id = order.strategy_id();
1035
1036 let primary_id = order
1037 .exec_spawn_id()
1038 .unwrap_or_else(|| order.client_order_id());
1039 let params = core.submit_params(&primary_id);
1040
1041 let order_exists = {
1042 let cache = core.cache_ref();
1043 cache.order_exists(&order.client_order_id())
1044 };
1045
1046 {
1047 let cache_rc = core.cache_rc();
1048 let mut cache = cache_rc.borrow_mut();
1049 cache.add_order(order.clone(), position_id, client_id, true)?;
1050 }
1051
1052 if !order_exists {
1053 publish_order_initialized(&order);
1054 }
1055
1056 let command = SubmitOrder::new(
1057 trader_id,
1058 client_id,
1059 strategy_id,
1060 order.instrument_id(),
1061 order.client_order_id(),
1062 order.init_event().clone(),
1063 order.exec_algorithm_id(),
1064 position_id,
1065 params,
1066 UUID4::new(),
1067 ts_init,
1068 None, );
1070
1071 if core.config.log_commands {
1072 let id = &core.actor.actor_id;
1073 log::info!("{id} {SEND}{CMD} {command}");
1074 }
1075
1076 if order.is_primary() {
1077 core.mark_primary_handed_off(order.client_order_id());
1078 }
1079
1080 msgbus::send_trading_command(
1081 MessagingSwitchboard::risk_engine_queue_execute(),
1082 TradingCommand::SubmitOrder(command),
1083 );
1084
1085 Ok(())
1086 }
1087
1088 fn modify_order(
1095 &mut self,
1096 order: &mut OrderAny,
1097 quantity: Option<Quantity>,
1098 price: Option<Price>,
1099 trigger_price: Option<Price>,
1100 client_id: Option<ClientId>,
1101 ) -> anyhow::Result<()>
1102 where
1103 Self: ExecutionAlgorithmNative,
1104 {
1105 let qty_changing = quantity.is_some_and(|q| q != order.quantity());
1106 let price_changing = price.is_some() && price != order.price();
1107 let trigger_changing = trigger_price.is_some() && trigger_price != order.trigger_price();
1108
1109 if !qty_changing && !price_changing && !trigger_changing {
1110 anyhow::bail!(
1111 "Cannot create command ModifyOrder: \
1112 quantity, price, and trigger were either None \
1113 or the same as existing values"
1114 );
1115 }
1116
1117 if order.is_closed() || order.is_pending_cancel() {
1118 log::warn!(
1119 "Cannot create command ModifyOrder: state is {:?}, {order:?}",
1120 order.status()
1121 );
1122 return Ok(());
1123 }
1124
1125 let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
1126 let trader_id = registered_trader_id(core)?;
1127 let strategy_id = order.strategy_id();
1128
1129 if !order.is_active_local() {
1130 required_account_id(order, "pending update")?;
1131 let event = self.generate_order_pending_update(order);
1132 let event = OrderEventAny::PendingUpdate(event);
1133
1134 {
1135 let cache_rc = ExecutionAlgorithmNative::exec_algorithm_core_mut(self).cache_rc();
1136 let mut cache = cache_rc.borrow_mut();
1137 match cache.update_order(&event) {
1138 Ok(updated) => *order = updated,
1139 Err(e)
1140 if matches!(
1141 e.downcast_ref::<OrderError>(),
1142 Some(OrderError::InvalidStateTransition)
1143 ) =>
1144 {
1145 log::warn!("InvalidStateTrigger: {e}, did not apply pending update event");
1146 return Ok(());
1147 }
1148 Err(e) => return Err(e),
1149 }
1150 }
1151
1152 let topic = format!("events.order.{strategy_id}");
1153 msgbus::publish_order_event(topic.into(), &event);
1154 msgbus::publish_order_event(
1155 msgbus::switchboard::get_order_pending_update_topic(order.instrument_id()),
1156 &event,
1157 );
1158 }
1159
1160 let ts_init = ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
1161 .clock_mut()
1162 .timestamp_ns();
1163 let command = ModifyOrder::new(
1164 trader_id,
1165 client_id,
1166 strategy_id,
1167 order.instrument_id(),
1168 order.client_order_id(),
1169 order.venue_order_id(),
1170 quantity,
1171 price,
1172 trigger_price,
1173 UUID4::new(),
1174 ts_init,
1175 None, None, );
1178
1179 if ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
1180 .config
1181 .log_commands
1182 {
1183 let id = &ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
1184 .actor
1185 .actor_id;
1186 log::info!("{id} {SEND}{CMD} {command}");
1187 }
1188
1189 let has_emulation_trigger = order.emulation_trigger().is_some();
1190
1191 if order.is_emulated() || has_emulation_trigger {
1192 msgbus::send_trading_command(
1193 MessagingSwitchboard::order_emulator_execute(),
1194 TradingCommand::ModifyOrder(command),
1195 );
1196 } else {
1197 msgbus::send_trading_command(
1198 MessagingSwitchboard::risk_engine_queue_execute(),
1199 TradingCommand::ModifyOrder(command),
1200 );
1201 }
1202
1203 Ok(())
1204 }
1205
1206 fn modify_order_in_place(
1218 &mut self,
1219 order: &mut OrderAny,
1220 quantity: Option<Quantity>,
1221 price: Option<Price>,
1222 trigger_price: Option<Price>,
1223 ) -> anyhow::Result<()>
1224 where
1225 Self: ExecutionAlgorithmNative,
1226 {
1227 let status = order.status();
1229 if status != OrderStatus::Initialized && status != OrderStatus::Released {
1230 anyhow::bail!(
1231 "Cannot modify order in place: status is {status:?}, expected INITIALIZED or RELEASED"
1232 );
1233 }
1234
1235 if price.is_some() && order.price().is_none() {
1237 anyhow::bail!(
1238 "Cannot modify order in place: {} orders do not have a LIMIT price",
1239 order.order_type()
1240 );
1241 }
1242
1243 if trigger_price.is_some() && order.trigger_price().is_none() {
1244 anyhow::bail!(
1245 "Cannot modify order in place: {} orders do not have a STOP trigger price",
1246 order.order_type()
1247 );
1248 }
1249
1250 let qty_changing = quantity.is_some_and(|q| q != order.quantity());
1252 let price_changing = price.is_some() && price != order.price();
1253 let trigger_changing = trigger_price.is_some() && trigger_price != order.trigger_price();
1254
1255 if !qty_changing && !price_changing && !trigger_changing {
1256 anyhow::bail!("Cannot modify order in place: no parameters differ from current values");
1257 }
1258
1259 let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
1260 let ts_now = core.clock_mut().timestamp_ns();
1261
1262 let updated = OrderUpdated::new(
1263 order.trader_id(),
1264 order.strategy_id(),
1265 order.instrument_id(),
1266 order.client_order_id(),
1267 quantity.unwrap_or_else(|| order.quantity()),
1268 UUID4::new(),
1269 ts_now,
1270 ts_now,
1271 false, order.venue_order_id(),
1273 order.account_id(),
1274 price,
1275 trigger_price,
1276 None, order.is_quote_quantity(),
1278 );
1279
1280 let event = OrderEventAny::Updated(updated);
1281
1282 {
1283 let cache_rc = core.cache_rc();
1284 let mut cache = cache_rc.borrow_mut();
1285 *order = cache.update_order(&event)?;
1286 }
1287
1288 publish_order_event(&event);
1289
1290 Ok(())
1291 }
1292
1293 fn cancel_order(
1299 &mut self,
1300 order: &mut OrderAny,
1301 client_id: Option<ClientId>,
1302 ) -> anyhow::Result<()>
1303 where
1304 Self: ExecutionAlgorithmNative,
1305 {
1306 if order.is_closed() || order.is_pending_cancel() {
1307 log::warn!(
1308 "Cannot cancel order: state is {:?}, {order:?}",
1309 order.status()
1310 );
1311 return Ok(());
1312 }
1313
1314 let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
1315 let trader_id = registered_trader_id(core)?;
1316 let strategy_id = order.strategy_id();
1317
1318 if !order.is_active_local() {
1319 required_account_id(order, "pending cancel")?;
1320 let event = self.generate_order_pending_cancel(order);
1321 let event = OrderEventAny::PendingCancel(event);
1322
1323 {
1324 let cache_rc = ExecutionAlgorithmNative::exec_algorithm_core_mut(self).cache_rc();
1325 let mut cache = cache_rc.borrow_mut();
1326 match cache.update_order(&event) {
1327 Ok(updated) => *order = updated,
1328 Err(e)
1329 if matches!(
1330 e.downcast_ref::<OrderError>(),
1331 Some(OrderError::InvalidStateTransition)
1332 ) =>
1333 {
1334 log::warn!("InvalidStateTrigger: {e}, did not apply pending cancel event");
1335 return Ok(());
1336 }
1337 Err(e) => return Err(e),
1338 }
1339 }
1340
1341 let topic = format!("events.order.{strategy_id}");
1342 msgbus::publish_order_event(topic.into(), &event);
1343 msgbus::publish_order_event(
1344 msgbus::switchboard::get_order_pending_cancel_topic(order.instrument_id()),
1345 &event,
1346 );
1347 }
1348
1349 let ts_init = ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
1350 .clock_mut()
1351 .timestamp_ns();
1352 let command = CancelOrder::new(
1353 trader_id,
1354 client_id,
1355 strategy_id,
1356 order.instrument_id(),
1357 order.client_order_id(),
1358 order.venue_order_id(),
1359 UUID4::new(),
1360 ts_init,
1361 None, None, );
1364
1365 if ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
1366 .config
1367 .log_commands
1368 {
1369 let id = &ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
1370 .actor
1371 .actor_id;
1372 log::info!("{id} {SEND}{CMD} {command}");
1373 }
1374
1375 let has_emulation_trigger = order.emulation_trigger().is_some();
1376
1377 if order.is_emulated() || order.status() == OrderStatus::Released || has_emulation_trigger {
1378 msgbus::send_trading_command(
1379 MessagingSwitchboard::order_emulator_execute(),
1380 TradingCommand::CancelOrder(command),
1381 );
1382 } else {
1383 msgbus::send_trading_command(
1384 MessagingSwitchboard::exec_engine_queue_execute(),
1385 TradingCommand::CancelOrder(command),
1386 );
1387 }
1388
1389 Ok(())
1390 }
1391
1392 fn subscribe_to_strategy_events(&mut self, strategy_id: StrategyId)
1396 where
1397 Self: ExecutionAlgorithmNative,
1398 Self: 'static + std::fmt::Debug + Sized,
1399 {
1400 let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
1401 if core.is_strategy_subscribed(&strategy_id) {
1402 return;
1403 }
1404
1405 let actor_id = core.actor.actor_id.inner();
1406
1407 let order_topic = format!("events.order.{strategy_id}");
1408 let order_actor_id = actor_id;
1409 let order_handler = TypedHandler::from(move |event: &OrderEventAny| {
1410 if let Some(mut algo) = try_get_actor_unchecked::<Self>(&order_actor_id) {
1411 algo.handle_order_event(event.clone());
1412 } else {
1413 log::error!(
1414 "ExecutionAlgorithm {order_actor_id} not found for order event handling"
1415 );
1416 }
1417 });
1418 msgbus::subscribe_order_events(order_topic.clone().into(), order_handler.clone(), None);
1419
1420 let position_topic = format!("events.position.{strategy_id}");
1421 let position_handler = TypedHandler::from(move |event: &PositionEvent| {
1422 if let Some(mut algo) = try_get_actor_unchecked::<Self>(&actor_id) {
1423 algo.handle_position_event(event.clone());
1424 } else {
1425 log::error!("ExecutionAlgorithm {actor_id} not found for position event handling");
1426 }
1427 });
1428 msgbus::subscribe_position_events(
1429 position_topic.clone().into(),
1430 position_handler.clone(),
1431 None,
1432 );
1433
1434 let handlers = StrategyEventHandlers {
1435 order_topic,
1436 order_handler,
1437 position_topic,
1438 position_handler,
1439 };
1440 core.store_strategy_event_handlers(strategy_id, handlers);
1441
1442 core.add_subscribed_strategy(strategy_id);
1443 log::info!("Subscribed to events for strategy {strategy_id}");
1444 }
1445
1446 fn unsubscribe_all_strategy_events(&mut self)
1450 where
1451 Self: ExecutionAlgorithmNative,
1452 {
1453 let handlers =
1454 ExecutionAlgorithmNative::exec_algorithm_core_mut(self).take_strategy_event_handlers();
1455
1456 for (strategy_id, h) in handlers {
1457 msgbus::unsubscribe_order_events(h.order_topic.into(), &h.order_handler);
1458 msgbus::unsubscribe_position_events(h.position_topic.into(), &h.position_handler);
1459 log::info!("Unsubscribed from events for strategy {strategy_id}");
1460 }
1461 ExecutionAlgorithmNative::exec_algorithm_core_mut(self).clear_subscribed_strategies();
1462 }
1463
1464 fn handle_order_event(&mut self, event: OrderEventAny)
1466 where
1467 Self: ExecutionAlgorithmNative,
1468 {
1469 if DataActorNative::core(ExecutionAlgorithmNative::exec_algorithm_core_mut(self)).state()
1470 != ComponentState::Running
1471 {
1472 return;
1473 }
1474
1475 let order = {
1476 let cache = ExecutionAlgorithmNative::exec_algorithm_core_mut(self).cache_ref();
1477 cache.order(&event.client_order_id()).map(|o| o.clone())
1478 };
1479
1480 let Some(order) = order else {
1481 return;
1482 };
1483
1484 let Some(order_algo_id) = order.exec_algorithm_id() else {
1485 return;
1486 };
1487
1488 if order_algo_id != self.id() {
1489 return;
1490 }
1491
1492 {
1493 let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
1494 if core.config.log_events {
1495 let id = &core.actor.actor_id;
1496 log::info!("{id} {RECV}{EVT} {event}");
1497 }
1498 }
1499
1500 if order.is_primary() && !order.is_active_local() {
1501 ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
1502 .clear_primary_spawn_state(order.client_order_id());
1503 }
1504
1505 match &event {
1506 OrderEventAny::Initialized(e) => self.on_order_initialized(e.clone()),
1507 OrderEventAny::Denied(e) => {
1508 self.restore_primary_order_quantity(&order, false);
1509 self.on_order_denied(*e);
1510 }
1511 OrderEventAny::Emulated(e) => self.on_order_emulated(*e),
1512 OrderEventAny::Released(e) => self.on_order_released(*e),
1513 OrderEventAny::Submitted(e) => self.on_order_submitted(*e),
1514 OrderEventAny::Rejected(e) => {
1515 self.restore_primary_order_quantity(&order, false);
1516 self.on_order_rejected(*e);
1517 }
1518 OrderEventAny::Accepted(e) => self.on_order_accepted(*e),
1519 OrderEventAny::Canceled(e) => {
1520 self.restore_primary_order_quantity(&order, false);
1521 self.on_algo_order_canceled(*e);
1522 }
1523 OrderEventAny::Expired(e) => {
1524 self.restore_primary_order_quantity(&order, false);
1525 self.on_order_expired(*e);
1526 }
1527 OrderEventAny::Triggered(e) => self.on_order_triggered(*e),
1528 OrderEventAny::PendingUpdate(e) => self.on_order_pending_update(*e),
1529 OrderEventAny::PendingCancel(e) => self.on_order_pending_cancel(*e),
1530 OrderEventAny::ModifyRejected(e) => self.on_order_modify_rejected(*e),
1531 OrderEventAny::CancelRejected(e) => self.on_order_cancel_rejected(*e),
1532 OrderEventAny::Updated(e) => self.on_order_updated(*e),
1533 OrderEventAny::Filled(e) => {
1534 self.rededuct_late_spawn_fill(&order);
1535 if order.leaves_qty().is_zero() {
1536 let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
1537 if core
1538 .spawn_reduction(order.client_order_id())
1539 .is_some_and(|reduction| reduction.restored_qty.is_none())
1540 {
1541 core.take_pending_spawn_reduction(order.client_order_id());
1542 }
1543 }
1544 self.on_algo_order_filled(e.clone());
1545 }
1546 OrderEventAny::FillVoided(e) => {
1547 if ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
1548 .spawn_reduction(order.client_order_id())
1549 .is_some_and(|reduction| reduction.restored_qty.is_some())
1550 {
1551 self.restore_primary_order_quantity(&order, false);
1552 }
1553 self.on_order_fill_voided(e);
1554 }
1555 }
1556
1557 self.on_order_event(event);
1558 }
1559
1560 fn handle_position_event(&mut self, event: PositionEvent)
1562 where
1563 Self: ExecutionAlgorithmNative,
1564 {
1565 if DataActorNative::core(ExecutionAlgorithmNative::exec_algorithm_core_mut(self)).state()
1566 != ComponentState::Running
1567 {
1568 return;
1569 }
1570
1571 {
1572 let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
1573 if core.config.log_events {
1574 let id = &core.actor.actor_id;
1575 log::info!("{id} {RECV}{EVT} {event:?}");
1576 }
1577 }
1578
1579 match &event {
1580 PositionEvent::PositionOpened(e) => self.on_position_opened(e.clone()),
1581 PositionEvent::PositionChanged(e) => self.on_position_changed(e.clone()),
1582 PositionEvent::PositionClosed(e) => self.on_position_closed(e.clone()),
1583 PositionEvent::PositionAdjusted(_) => {}
1584 }
1585
1586 self.on_position_event(event);
1587 }
1588
1589 fn on_start(&mut self) -> anyhow::Result<()>
1597 where
1598 Self: ExecutionAlgorithmNative,
1599 {
1600 let id = self.id();
1601 log::info!("Starting {id}");
1602 Ok(())
1603 }
1604
1605 fn on_stop(&mut self) -> anyhow::Result<()> {
1611 Ok(())
1612 }
1613
1614 fn on_resume(&mut self) -> anyhow::Result<()> {
1620 Ok(())
1621 }
1622
1623 fn on_reset(&mut self) -> anyhow::Result<()>
1629 where
1630 Self: ExecutionAlgorithmNative,
1631 {
1632 self.unsubscribe_all_strategy_events();
1633 ExecutionAlgorithmNative::exec_algorithm_core_mut(self).reset();
1634 Ok(())
1635 }
1636
1637 fn on_time_event(&mut self, _event: &TimeEvent) -> anyhow::Result<()> {
1645 Ok(())
1646 }
1647
1648 #[allow(unused_variables)]
1650 fn on_order_initialized(&mut self, event: OrderInitialized) {}
1651
1652 #[allow(unused_variables)]
1654 fn on_order_denied(&mut self, event: OrderDenied) {}
1655
1656 #[allow(unused_variables)]
1658 fn on_order_emulated(&mut self, event: OrderEmulated) {}
1659
1660 #[allow(unused_variables)]
1662 fn on_order_released(&mut self, event: OrderReleased) {}
1663
1664 #[allow(unused_variables)]
1666 fn on_order_submitted(&mut self, event: OrderSubmitted) {}
1667
1668 #[allow(unused_variables)]
1670 fn on_order_rejected(&mut self, event: OrderRejected) {}
1671
1672 #[allow(unused_variables)]
1674 fn on_order_accepted(&mut self, event: OrderAccepted) {}
1675
1676 #[allow(unused_variables)]
1678 fn on_algo_order_canceled(&mut self, event: OrderCanceled) {}
1679
1680 #[allow(unused_variables)]
1682 fn on_order_expired(&mut self, event: OrderExpired) {}
1683
1684 #[allow(unused_variables)]
1686 fn on_order_triggered(&mut self, event: OrderTriggered) {}
1687
1688 #[allow(unused_variables)]
1690 fn on_order_pending_update(&mut self, event: OrderPendingUpdate) {}
1691
1692 #[allow(unused_variables)]
1694 fn on_order_pending_cancel(&mut self, event: OrderPendingCancel) {}
1695
1696 #[allow(unused_variables)]
1698 fn on_order_modify_rejected(&mut self, event: OrderModifyRejected) {}
1699
1700 #[allow(unused_variables)]
1702 fn on_order_cancel_rejected(&mut self, event: OrderCancelRejected) {}
1703
1704 #[allow(unused_variables)]
1706 fn on_order_updated(&mut self, event: OrderUpdated) {}
1707
1708 #[allow(unused_variables)]
1710 fn on_algo_order_filled(&mut self, event: OrderFilled) {}
1711
1712 #[allow(unused_variables)]
1714 fn on_order_fill_voided(&mut self, event: &OrderFillVoided) {}
1715
1716 #[allow(unused_variables)]
1718 fn on_order_event(&mut self, event: OrderEventAny) {}
1719
1720 #[allow(unused_variables)]
1722 fn on_position_opened(&mut self, event: PositionOpened) {}
1723
1724 #[allow(unused_variables)]
1726 fn on_position_changed(&mut self, event: PositionChanged) {}
1727
1728 #[allow(unused_variables)]
1730 fn on_position_closed(&mut self, event: PositionClosed) {}
1731
1732 #[allow(unused_variables)]
1734 fn on_position_event(&mut self, event: PositionEvent) {}
1735}
1736
1737#[derive(Debug)]
1738pub(crate) struct EmulatedOrderSubmissionError {
1739 client_order_id: ClientOrderId,
1740}
1741
1742impl Display for EmulatedOrderSubmissionError {
1743 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1744 write!(
1745 f,
1746 "Execution algorithm cannot submit order {} with a live emulation trigger",
1747 self.client_order_id
1748 )
1749 }
1750}
1751
1752impl std::error::Error for EmulatedOrderSubmissionError {}
1753
1754fn spawn_unfilled_quantity(
1755 order: &OrderAny,
1756 reduction: SpawnReduction,
1757 primary_precision: u8,
1758) -> Option<Quantity> {
1759 if reduction.spawn_was_quote_quantity == order.is_quote_quantity() {
1760 return Some(
1761 order
1762 .quantity()
1763 .min(reduction.deducted_qty)
1764 .saturating_sub(order.filled_qty()),
1765 );
1766 }
1767
1768 if !reduction.spawn_was_quote_quantity {
1769 log::warn!(
1770 "Cannot account for spawned order {} quantity: denomination changed from base to quote",
1771 order.client_order_id(),
1772 );
1773 return None;
1774 }
1775
1776 let converted_total = order.quantity();
1777 if converted_total.is_zero() {
1778 log::warn!(
1779 "Cannot account for converted spawned order {} quantity: converted total is zero",
1780 order.client_order_id(),
1781 );
1782 return None;
1783 }
1784
1785 let child_qty = converted_total.saturating_sub(order.filled_qty());
1787
1788 let child_qty = child_qty.min(converted_total);
1791 #[allow(clippy::useless_conversion)]
1794 let proportional_raw = QuantityRaw::try_from(muldiv_floor_u128(
1795 u128::from(reduction.deducted_qty.raw()),
1796 u128::from(child_qty.raw()),
1797 u128::from(converted_total.raw()),
1798 ))
1799 .expect("Quotient bounded by the deducted quantity");
1800
1801 let precision_increment = Quantity::from_decimal_dp(
1802 rust_decimal::Decimal::new(1, u32::from(primary_precision)),
1803 primary_precision,
1804 )
1805 .expect("Primary quantity precision must be valid")
1806 .raw();
1807 let floored_raw = proportional_raw - proportional_raw % precision_increment;
1808 Some(Quantity::from_raw(floored_raw, primary_precision))
1809}
1810
1811fn muldiv_floor_u128(a: u128, b: u128, c: u128) -> u128 {
1818 if let Some(product) = a.checked_mul(b) {
1819 return product / c;
1820 }
1821
1822 let (hi, lo) = mul_wide_u128(a, b);
1823 assert!(hi < c, "muldiv_floor_u128 quotient exceeds u128");
1824 div_wide_u128(hi, lo, c)
1825}
1826
1827fn mul_wide_u128(a: u128, b: u128) -> (u128, u128) {
1829 const MASK: u128 = (1u128 << 64) - 1;
1830 let (a_hi, a_lo) = (a >> 64, a & MASK);
1831 let (b_hi, b_lo) = (b >> 64, b & MASK);
1832
1833 let ll = a_lo * b_lo;
1834 let lh = a_lo * b_hi;
1835 let hl = a_hi * b_lo;
1836 let hh = a_hi * b_hi;
1837
1838 let mid = (ll >> 64) + (lh & MASK) + (hl & MASK);
1839 let lo = (mid << 64) | (ll & MASK);
1840 let hi = hh + (lh >> 64) + (hl >> 64) + (mid >> 64);
1841 (hi, lo)
1842}
1843
1844fn div_wide_u128(hi: u128, lo: u128, c: u128) -> u128 {
1847 let mut rem = hi;
1848 let mut quotient = 0u128;
1849
1850 for i in (0..u128::BITS).rev() {
1851 let carry = rem >> 127;
1854 rem = (rem << 1) | ((lo >> i) & 1);
1855
1856 if carry == 1 || rem >= c {
1857 rem = rem.wrapping_sub(c);
1858 quotient |= 1 << i;
1859 }
1860 }
1861 quotient
1862}
1863
1864fn charge_spawn_reduction(
1867 core: &mut ExecutionAlgorithmCore,
1868 spawn_id: ClientOrderId,
1869 primary_id: ClientOrderId,
1870 mut reduction: SpawnReduction,
1871 unfilled_qty: Quantity,
1872 shortfall_qty: Quantity,
1873) {
1874 reduction.restored_qty = Some(unfilled_qty);
1875 core.set_spawn_reduction(spawn_id, reduction);
1876
1877 if !shortfall_qty.is_zero() {
1878 core.add_spawn_fill_debt(primary_id, shortfall_qty);
1879 }
1880}
1881
1882fn publish_order_initialized(order: &OrderAny) {
1883 let event = OrderEventAny::Initialized(order.init_event().clone());
1884 publish_order_event(&event);
1885}
1886
1887fn publish_order_event(event: &OrderEventAny) {
1888 let topic = format!("events.order.{}", event.strategy_id());
1889 msgbus::publish_order_event(topic.into(), event);
1890}
1891
1892fn registered_trader_id(core: &ExecutionAlgorithmCore) -> anyhow::Result<TraderId> {
1893 DataActorNative::core(core)
1894 .trader_id()
1895 .ok_or_else(|| anyhow::anyhow!("ExecutionAlgorithm not registered: trader_id is not set"))
1896}
1897
1898fn required_account_id(order: &OrderAny, operation: &str) -> anyhow::Result<AccountId> {
1899 order.account_id().ok_or_else(|| {
1900 anyhow::anyhow!(
1901 "Cannot generate {operation} event for {}: account_id is not set",
1902 order.client_order_id()
1903 )
1904 })
1905}
1906
1907#[cfg(test)]
1908mod tests {
1909 use std::{cell::RefCell, rc::Rc};
1910
1911 use nautilus_common::{
1912 actor::DataActor,
1913 cache::Cache,
1914 clock::VirtualClock,
1915 component::Component,
1916 enums::ComponentTrigger,
1917 msgbus::{
1918 self, TypedHandler,
1919 stubs::{TypedIntoMessageSavingHandler, get_typed_into_message_saving_handler},
1920 },
1921 };
1922 use nautilus_model::{
1923 enums::{LiquiditySide, OrderSide, OrderStatus, OrderType},
1924 events::{
1925 OrderAccepted, OrderCanceled, OrderDenied, OrderDeniedReason, OrderRejected,
1926 order::spec::{
1927 OrderAcceptedSpec, OrderCanceledSpec, OrderDeniedSpec, OrderExpiredSpec,
1928 OrderFillVoidedSpec, OrderFilledSpec, OrderRejectedSpec, OrderSubmittedSpec,
1929 OrderUpdatedSpec,
1930 },
1931 },
1932 identifiers::{
1933 AccountId, ActorId, ClientOrderId, ExecAlgorithmId, InstrumentId, StrategyId, TradeId,
1934 TraderId, VenueOrderId,
1935 },
1936 orders::{LimitOrder, MarketOrder, OrderAny, OrderTestBuilder, stubs::TestOrderStubs},
1937 types::{Currency, Price, Quantity},
1938 };
1939 use rstest::rstest;
1940
1941 use super::*;
1942 use crate::nautilus_execution_algorithm;
1943
1944 #[derive(Debug)]
1945 struct TestAlgorithm {
1946 core: ExecutionAlgorithmCore,
1947 order_client_ids: Vec<ClientOrderId>,
1948 }
1949
1950 #[derive(Debug)]
1951 struct ModifyDispatchAlgorithm {
1952 core: ExecutionAlgorithmCore,
1953 modify_client_order_ids: Vec<ClientOrderId>,
1954 }
1955
1956 #[derive(Debug)]
1957 struct CoreFreeExecutionAlgorithm {
1958 orders_seen: usize,
1959 }
1960
1961 #[derive(Debug)]
1962 struct MacroTestCustomField {
1963 inner: ExecutionAlgorithmCore,
1964 }
1965
1966 impl DataActor for CoreFreeExecutionAlgorithm {}
1967
1968 impl ExecutionAlgorithm for CoreFreeExecutionAlgorithm {
1969 fn on_order(&mut self, _order: OrderAny) -> anyhow::Result<()> {
1970 self.orders_seen += 1;
1971 Ok(())
1972 }
1973 }
1974
1975 impl DataActor for MacroTestCustomField {}
1976
1977 nautilus_execution_algorithm!(MacroTestCustomField, inner, {
1978 fn on_order(&mut self, _order: OrderAny) -> anyhow::Result<()> {
1979 Ok(())
1980 }
1981 });
1982
1983 impl TestAlgorithm {
1984 fn new(config: ExecutionAlgorithmConfig) -> Self {
1985 Self {
1986 core: ExecutionAlgorithmCore::new(config),
1987 order_client_ids: Vec::new(),
1988 }
1989 }
1990 }
1991
1992 impl DataActor for TestAlgorithm {}
1993
1994 nautilus_execution_algorithm!(TestAlgorithm, {
1995 fn on_order(&mut self, order: OrderAny) -> anyhow::Result<()> {
1996 self.order_client_ids.push(order.client_order_id());
1997 Ok(())
1998 }
1999 });
2000
2001 impl ModifyDispatchAlgorithm {
2002 fn new(config: ExecutionAlgorithmConfig) -> Self {
2003 Self {
2004 core: ExecutionAlgorithmCore::new(config),
2005 modify_client_order_ids: Vec::new(),
2006 }
2007 }
2008 }
2009
2010 impl DataActor for ModifyDispatchAlgorithm {}
2011
2012 nautilus_execution_algorithm!(ModifyDispatchAlgorithm, {
2013 fn on_order(&mut self, _order: OrderAny) -> anyhow::Result<()> {
2014 Ok(())
2015 }
2016
2017 fn handle_modify_order(&mut self, command: ModifyOrder) -> anyhow::Result<()> {
2018 self.modify_client_order_ids.push(command.client_order_id);
2019 Ok(())
2020 }
2021 });
2022
2023 fn create_test_algorithm() -> TestAlgorithm {
2024 let unique_id = format!("TEST-{}", UUID4::new());
2026 let config = ExecutionAlgorithmConfig {
2027 exec_algorithm_id: Some(ExecAlgorithmId::new(&unique_id)),
2028 ..Default::default()
2029 };
2030 TestAlgorithm::new(config)
2031 }
2032
2033 fn register_algorithm(algo: &mut TestAlgorithm) {
2034 let trader_id = TraderId::from("TRADER-001");
2035 let clock = Rc::new(RefCell::new(VirtualClock::new()));
2036 let cache = Rc::new(RefCell::new(Cache::default()));
2037
2038 algo.core.register(trader_id, clock, cache).unwrap();
2039
2040 algo.transition_state(ComponentTrigger::Initialize).unwrap();
2042 algo.transition_state(ComponentTrigger::Start).unwrap();
2043 algo.transition_state(ComponentTrigger::StartCompleted)
2044 .unwrap();
2045 }
2046
2047 fn subscribe_order_topic(
2048 strategy_id: StrategyId,
2049 ) -> (TypedHandler<OrderEventAny>, Rc<RefCell<Vec<OrderEventAny>>>) {
2050 let events = Rc::new(RefCell::new(Vec::new()));
2051 let handler = TypedHandler::from({
2052 let events = events.clone();
2053 move |event: &OrderEventAny| {
2054 events.borrow_mut().push(event.clone());
2055 }
2056 });
2057 msgbus::subscribe_order_events(
2058 format!("events.order.{strategy_id}").into(),
2059 handler.clone(),
2060 None,
2061 );
2062 (handler, events)
2063 }
2064
2065 fn setup_pending_spawn() -> (TestAlgorithm, ClientOrderId, OrderAny) {
2066 let mut algo = create_test_algorithm();
2067 register_algorithm(&mut algo);
2068 let client_order_id = ClientOrderId::from("O-001");
2069 let mut primary = OrderAny::Market(MarketOrder::new(
2070 TraderId::from("TRADER-001"),
2071 StrategyId::from("STRAT-001"),
2072 InstrumentId::from("BTC/USDT.BINANCE"),
2073 client_order_id,
2074 OrderSide::Buy,
2075 Quantity::from("1.0"),
2076 TimeInForce::Gtc,
2077 UUID4::new(),
2078 0.into(),
2079 false,
2080 false,
2081 None,
2082 None,
2083 None,
2084 None,
2085 Some(algo.id()),
2086 None,
2087 Some(client_order_id),
2088 None,
2089 ));
2090 algo.core
2091 .cache_rc()
2092 .borrow_mut()
2093 .add_order(primary.clone(), None, None, false)
2094 .unwrap();
2095 let spawned = algo.spawn_market(
2096 &mut primary,
2097 Quantity::from("0.5"),
2098 TimeInForce::Fok,
2099 false,
2100 None,
2101 true,
2102 );
2103 let spawned_order = OrderAny::Market(spawned);
2104 algo.core
2105 .cache_rc()
2106 .borrow_mut()
2107 .add_order(spawned_order.clone(), None, None, false)
2108 .unwrap();
2109 (algo, client_order_id, spawned_order)
2110 }
2111
2112 fn setup_accepted_spawn() -> (TestAlgorithm, ClientOrderId, OrderAny) {
2113 let (mut algo, client_order_id, mut spawned_order) = setup_pending_spawn();
2114 let accepted = OrderAcceptedSpec::builder()
2115 .trader_id(spawned_order.trader_id())
2116 .strategy_id(spawned_order.strategy_id())
2117 .instrument_id(spawned_order.instrument_id())
2118 .client_order_id(spawned_order.client_order_id())
2119 .venue_order_id(VenueOrderId::from("V-123"))
2120 .account_id(AccountId::from("BINANCE-001"))
2121 .build();
2122 spawned_order = algo
2123 .core
2124 .cache_rc()
2125 .borrow_mut()
2126 .update_order(&OrderEventAny::Accepted(accepted))
2127 .unwrap();
2128 algo.handle_order_event(OrderEventAny::Accepted(accepted));
2129 (algo, client_order_id, spawned_order)
2130 }
2131
2132 fn setup_accepted_quote_spawn(
2133 primary_qty: Quantity,
2134 spawn_qty: Quantity,
2135 ) -> (TestAlgorithm, ClientOrderId, OrderAny) {
2136 let mut algo = create_test_algorithm();
2137 register_algorithm(&mut algo);
2138 let client_order_id = ClientOrderId::from("O-QUOTE");
2139 let mut primary = OrderAny::Market(MarketOrder::new(
2140 TraderId::from("TRADER-001"),
2141 StrategyId::from("STRAT-001"),
2142 InstrumentId::from("BTC/USDT.BINANCE"),
2143 client_order_id,
2144 OrderSide::Buy,
2145 primary_qty,
2146 TimeInForce::Gtc,
2147 UUID4::new(),
2148 0.into(),
2149 false,
2150 true,
2151 None,
2152 None,
2153 None,
2154 None,
2155 Some(algo.id()),
2156 None,
2157 Some(client_order_id),
2158 None,
2159 ));
2160 algo.core
2161 .cache_rc()
2162 .borrow_mut()
2163 .add_order(primary.clone(), None, None, false)
2164 .unwrap();
2165 let mut spawned_order = OrderAny::Market(algo.spawn_market(
2166 &mut primary,
2167 spawn_qty,
2168 TimeInForce::Fok,
2169 false,
2170 None,
2171 true,
2172 ));
2173 algo.core
2174 .cache_rc()
2175 .borrow_mut()
2176 .add_order(spawned_order.clone(), None, None, false)
2177 .unwrap();
2178 accept_spawned_order(&mut algo, &mut spawned_order);
2179 (algo, client_order_id, spawned_order)
2180 }
2181
2182 fn fill_spawned_order(algo: &mut TestAlgorithm, order: &mut OrderAny, quantity: Quantity) {
2183 let venue_order_id = order
2184 .venue_order_id()
2185 .unwrap_or_else(|| VenueOrderId::from("V-PRIMARY"));
2186 let filled = OrderFilledSpec::builder()
2187 .trader_id(order.trader_id())
2188 .strategy_id(order.strategy_id())
2189 .instrument_id(order.instrument_id())
2190 .client_order_id(order.client_order_id())
2191 .venue_order_id(venue_order_id)
2192 .account_id(AccountId::from("BINANCE-001"))
2193 .trade_id(TradeId::new(UUID4::new().to_string()))
2194 .order_side(order.order_side())
2195 .order_type(order.order_type())
2196 .last_qty(quantity)
2197 .last_px(Price::from("50000.0"))
2198 .currency(Currency::USD())
2199 .liquidity_side(LiquiditySide::Taker)
2200 .build();
2201 *order = algo
2202 .core
2203 .cache_rc()
2204 .borrow_mut()
2205 .update_order(&OrderEventAny::Filled(filled.clone()))
2206 .unwrap();
2207 algo.handle_order_event(OrderEventAny::Filled(filled));
2208 }
2209
2210 fn submit_order_in_cache(algo: &mut TestAlgorithm, order: &mut OrderAny) {
2211 let submitted = OrderSubmittedSpec::builder()
2212 .trader_id(order.trader_id())
2213 .strategy_id(order.strategy_id())
2214 .instrument_id(order.instrument_id())
2215 .client_order_id(order.client_order_id())
2216 .account_id(AccountId::from("BINANCE-001"))
2217 .build();
2218 *order = algo
2219 .core
2220 .cache_rc()
2221 .borrow_mut()
2222 .update_order(&OrderEventAny::Submitted(submitted))
2223 .unwrap();
2224 algo.handle_order_event(OrderEventAny::Submitted(submitted));
2225 }
2226
2227 fn cancel_spawned_order(algo: &mut TestAlgorithm, order: &mut OrderAny) {
2228 let venue_order_id = order
2229 .venue_order_id()
2230 .unwrap_or_else(|| VenueOrderId::from("V-123"));
2231 let canceled = OrderCanceledSpec::builder()
2232 .trader_id(order.trader_id())
2233 .strategy_id(order.strategy_id())
2234 .instrument_id(order.instrument_id())
2235 .client_order_id(order.client_order_id())
2236 .venue_order_id(venue_order_id)
2237 .account_id(AccountId::from("BINANCE-001"))
2238 .build();
2239 *order = algo
2240 .core
2241 .cache_rc()
2242 .borrow_mut()
2243 .update_order(&OrderEventAny::Canceled(canceled))
2244 .unwrap();
2245 algo.handle_order_event(OrderEventAny::Canceled(canceled));
2246 }
2247
2248 fn convert_spawn_to_base(algo: &mut TestAlgorithm, order: &mut OrderAny, quantity: Quantity) {
2249 let updated = OrderUpdatedSpec::builder()
2250 .trader_id(order.trader_id())
2251 .strategy_id(order.strategy_id())
2252 .instrument_id(order.instrument_id())
2253 .client_order_id(order.client_order_id())
2254 .quantity(quantity)
2255 .maybe_venue_order_id(order.venue_order_id())
2256 .maybe_account_id(order.account_id())
2257 .is_quote_quantity(false)
2258 .build();
2259 *order = algo
2260 .core
2261 .cache_rc()
2262 .borrow_mut()
2263 .update_order(&OrderEventAny::Updated(updated))
2264 .unwrap();
2265 algo.handle_order_event(OrderEventAny::Updated(updated));
2266 }
2267
2268 fn expire_spawned_order(algo: &mut TestAlgorithm, order: &mut OrderAny) {
2269 let expired = OrderExpiredSpec::builder()
2270 .trader_id(order.trader_id())
2271 .strategy_id(order.strategy_id())
2272 .instrument_id(order.instrument_id())
2273 .client_order_id(order.client_order_id())
2274 .venue_order_id(VenueOrderId::from("V-123"))
2275 .account_id(AccountId::from("BINANCE-001"))
2276 .build();
2277 *order = algo
2278 .core
2279 .cache_rc()
2280 .borrow_mut()
2281 .update_order(&OrderEventAny::Expired(expired))
2282 .unwrap();
2283 algo.handle_order_event(OrderEventAny::Expired(expired));
2284 }
2285
2286 fn accept_spawned_order(algo: &mut TestAlgorithm, order: &mut OrderAny) {
2287 let venue_order_id = VenueOrderId::from(format!("V-{}", order.client_order_id()).as_str());
2288 let accepted = OrderAcceptedSpec::builder()
2289 .trader_id(order.trader_id())
2290 .strategy_id(order.strategy_id())
2291 .instrument_id(order.instrument_id())
2292 .client_order_id(order.client_order_id())
2293 .venue_order_id(venue_order_id)
2294 .account_id(AccountId::from("BINANCE-001"))
2295 .build();
2296 *order = algo
2297 .core
2298 .cache_rc()
2299 .borrow_mut()
2300 .update_order(&OrderEventAny::Accepted(accepted))
2301 .unwrap();
2302 algo.handle_order_event(OrderEventAny::Accepted(accepted));
2303 }
2304
2305 fn spawn_reduced_child(
2306 algo: &mut TestAlgorithm,
2307 primary_id: ClientOrderId,
2308 quantity: Quantity,
2309 ) -> OrderAny {
2310 let mut primary = algo.cache().order(&primary_id).unwrap();
2311 let child = OrderAny::Market(algo.spawn_market(
2312 &mut primary,
2313 quantity,
2314 TimeInForce::Fok,
2315 false,
2316 None,
2317 true,
2318 ));
2319 algo.core
2320 .cache_rc()
2321 .borrow_mut()
2322 .add_order(child.clone(), None, None, false)
2323 .unwrap();
2324 child
2325 }
2326
2327 #[rstest]
2328 fn test_algorithm_creation() {
2329 let algo = create_test_algorithm();
2330 assert!(algo.id().inner().starts_with("TEST-"));
2331 assert!(algo.order_client_ids.is_empty());
2332 }
2333
2334 #[rstest]
2335 fn test_algorithm_registration() {
2336 let mut algo = create_test_algorithm();
2337 register_algorithm(&mut algo);
2338
2339 assert_eq!(algo.trader_id(), Some(TraderId::from("TRADER-001")));
2340 }
2341
2342 #[rstest]
2343 fn test_algorithm_deny_order_updates_cache_and_publishes_once() {
2344 let mut algo = create_test_algorithm();
2345 register_algorithm(&mut algo);
2346
2347 let strategy_id = StrategyId::from("STRAT-ALGO-DENY");
2348 let order = OrderAny::Market(MarketOrder::new(
2349 TraderId::from("TRADER-001"),
2350 strategy_id,
2351 InstrumentId::from("BTC/USDT.BINANCE"),
2352 ClientOrderId::from("O-ALGO-DENY"),
2353 OrderSide::Buy,
2354 Quantity::from("1.0"),
2355 TimeInForce::Gtc,
2356 UUID4::new(),
2357 0.into(),
2358 false,
2359 false,
2360 None,
2361 None,
2362 None,
2363 None,
2364 None,
2365 None,
2366 None,
2367 None,
2368 ));
2369 {
2370 let cache_rc = algo.core.cache_rc();
2371 cache_rc
2372 .borrow_mut()
2373 .add_order(order.clone(), None, None, false)
2374 .unwrap();
2375 }
2376 let reason = OrderDeniedReason::ValidationFailed {
2377 detail: "invalid execution schedule".to_string(),
2378 }
2379 .to_string();
2380 let reason = Ustr::from(&reason);
2381 let (handler, events) = subscribe_order_topic(strategy_id);
2382
2383 algo.deny_order(&order, reason).unwrap();
2384 algo.deny_order(&order, reason).unwrap();
2385
2386 msgbus::unsubscribe_order_events(format!("events.order.{strategy_id}").into(), &handler);
2387 let cached_order = algo.cache().order(&order.client_order_id()).unwrap();
2388 let events = events.borrow();
2389
2390 assert_eq!(cached_order.status(), OrderStatus::Denied);
2391 assert_eq!(events.len(), 1);
2392 assert!(matches!(
2393 &events[0],
2394 OrderEventAny::Denied(event)
2395 if event.reason == reason
2396 && event.strategy_id == strategy_id
2397 && event.client_order_id == order.client_order_id()
2398 ));
2399 }
2400
2401 #[rstest]
2402 fn test_algorithm_deny_order_initializes_missing_order_once() {
2403 let mut algo = create_test_algorithm();
2404 register_algorithm(&mut algo);
2405
2406 let strategy_id = StrategyId::from("STRAT-ALGO-DENY-MISSING");
2407 let order = OrderTestBuilder::new(OrderType::Market)
2408 .strategy_id(strategy_id)
2409 .instrument_id(InstrumentId::from("BTC/USDT.BINANCE"))
2410 .client_order_id(ClientOrderId::from("O-ALGO-DENY-MISSING"))
2411 .quantity(Quantity::from("1.0"))
2412 .build();
2413 let reason = Ustr::from("VALIDATION_FAILED: invalid execution schedule");
2414 let (handler, events) = subscribe_order_topic(strategy_id);
2415
2416 algo.deny_order(&order, reason).unwrap();
2417 algo.deny_order(&order, reason).unwrap();
2418
2419 msgbus::unsubscribe_order_events(format!("events.order.{strategy_id}").into(), &handler);
2420 let cached_order = algo.cache().order(&order.client_order_id()).unwrap();
2421 let events = events.borrow();
2422
2423 assert_eq!(cached_order.status(), OrderStatus::Denied);
2424 assert_eq!(cached_order.event_count(), 2);
2425 assert_eq!(events.len(), 2);
2426 assert!(matches!(
2427 &events[0],
2428 OrderEventAny::Initialized(event)
2429 if event.strategy_id == strategy_id
2430 && event.client_order_id == order.client_order_id()
2431 ));
2432 assert!(matches!(
2433 &events[1],
2434 OrderEventAny::Denied(event)
2435 if event.reason == reason
2436 && event.strategy_id == strategy_id
2437 && event.client_order_id == order.client_order_id()
2438 ));
2439 }
2440
2441 #[rstest]
2442 fn test_algorithm_deny_order_does_not_publish_when_apply_fails() {
2443 let mut algo = create_test_algorithm();
2444 register_algorithm(&mut algo);
2445
2446 let strategy_id = StrategyId::from("STRAT-ALGO-DENY-APPLY");
2447 let order = OrderTestBuilder::new(OrderType::Market)
2448 .strategy_id(strategy_id)
2449 .instrument_id(InstrumentId::from("BTC/USDT.BINANCE"))
2450 .client_order_id(ClientOrderId::from("O-ALGO-DENY-APPLY"))
2451 .quantity(Quantity::from("1.0"))
2452 .build();
2453 let order = TestOrderStubs::make_accepted_order(&order);
2454 {
2455 let cache_rc = algo.core.cache_rc();
2456 cache_rc
2457 .borrow_mut()
2458 .add_order(order.clone(), None, None, false)
2459 .unwrap();
2460 }
2461 let (handler, events) = subscribe_order_topic(strategy_id);
2462
2463 let params = nautilus_core::Params::new(
2464 [(
2465 "route".to_string(),
2466 serde_json::Value::String("A".to_string()),
2467 )]
2468 .into(),
2469 );
2470 algo.core
2471 .remember_submit_params(order.client_order_id(), Some(params));
2472
2473 let error = algo
2474 .deny_order(
2475 &order,
2476 Ustr::from("VALIDATION_FAILED: invalid execution schedule"),
2477 )
2478 .unwrap_err();
2479
2480 msgbus::unsubscribe_order_events(format!("events.order.{strategy_id}").into(), &handler);
2481 let cached_order = algo.cache().order(&order.client_order_id()).unwrap();
2482
2483 assert!(matches!(
2484 error.downcast_ref::<OrderError>(),
2485 Some(OrderError::InvalidStateTransition)
2486 ));
2487 assert_eq!(cached_order.status(), OrderStatus::Accepted);
2488 assert!(events.borrow().is_empty());
2489 assert!(algo.core.submit_params(&order.client_order_id()).is_some());
2491 }
2492
2493 #[rstest]
2494 fn test_algorithm_deny_order_removes_submit_params() {
2495 let mut algo = create_test_algorithm();
2496 register_algorithm(&mut algo);
2497
2498 let strategy_id = StrategyId::from("STRAT-ALGO-DENY-PARAMS");
2499 let order = OrderTestBuilder::new(OrderType::Market)
2500 .strategy_id(strategy_id)
2501 .instrument_id(InstrumentId::from("BTC/USDT.BINANCE"))
2502 .client_order_id(ClientOrderId::from("O-ALGO-DENY-PARAMS"))
2503 .quantity(Quantity::from("1.0"))
2504 .build();
2505 {
2506 let cache_rc = algo.core.cache_rc();
2507 cache_rc
2508 .borrow_mut()
2509 .add_order(order.clone(), None, None, false)
2510 .unwrap();
2511 }
2512
2513 let params = nautilus_core::Params::new(
2514 [(
2515 "route".to_string(),
2516 serde_json::Value::String("A".to_string()),
2517 )]
2518 .into(),
2519 );
2520 algo.core
2521 .remember_submit_params(order.client_order_id(), Some(params));
2522 assert!(algo.core.submit_params(&order.client_order_id()).is_some());
2523
2524 algo.deny_order(&order, Ustr::from("VALIDATION_FAILED: test"))
2525 .unwrap();
2526
2527 assert!(algo.core.submit_params(&order.client_order_id()).is_none());
2528 }
2529
2530 #[rstest]
2531 fn test_submit_order_errors_when_algorithm_not_registered() {
2532 let mut algo = create_test_algorithm();
2533 let order = OrderAny::Market(MarketOrder::new(
2534 TraderId::from("TRADER-001"),
2535 StrategyId::from("STRAT-001"),
2536 InstrumentId::from("BTC/USDT.BINANCE"),
2537 ClientOrderId::from("O-UNREGISTERED-001"),
2538 OrderSide::Buy,
2539 Quantity::from("1.0"),
2540 TimeInForce::Gtc,
2541 UUID4::new(),
2542 0.into(),
2543 false,
2544 false,
2545 None,
2546 None,
2547 None,
2548 None,
2549 None,
2550 None,
2551 None,
2552 None,
2553 ));
2554
2555 let err = algo
2556 .submit_order(order, None, None)
2557 .unwrap_err()
2558 .to_string();
2559
2560 assert_eq!(
2561 err,
2562 "ExecutionAlgorithm not registered: trader_id is not set"
2563 );
2564 }
2565
2566 #[rstest]
2567 fn test_required_account_id_errors_when_missing_for_algorithm_event() {
2568 let order = OrderAny::Market(MarketOrder::new(
2569 TraderId::from("TRADER-001"),
2570 StrategyId::from("STRAT-001"),
2571 InstrumentId::from("BTC/USDT.BINANCE"),
2572 ClientOrderId::from("O-NO-ACCOUNT-001"),
2573 OrderSide::Buy,
2574 Quantity::from("1.0"),
2575 TimeInForce::Gtc,
2576 UUID4::new(),
2577 0.into(),
2578 false,
2579 false,
2580 None,
2581 None,
2582 None,
2583 None,
2584 None,
2585 None,
2586 None,
2587 None,
2588 ));
2589
2590 let err = required_account_id(&order, "pending update")
2591 .unwrap_err()
2592 .to_string();
2593
2594 assert_eq!(
2595 err,
2596 "Cannot generate pending update event for O-NO-ACCOUNT-001: account_id is not set"
2597 );
2598 }
2599
2600 #[rstest]
2601 fn test_algorithm_id() {
2602 let algo = create_test_algorithm();
2603 assert!(algo.id().inner().starts_with("TEST-"));
2604 }
2605
2606 #[rstest]
2607 fn test_execution_algorithm_behavior_does_not_require_native_core_access() {
2608 fn assert_execution_algorithm<T: ExecutionAlgorithm + DataActor>() {}
2609
2610 assert_execution_algorithm::<CoreFreeExecutionAlgorithm>();
2611
2612 let mut algorithm = CoreFreeExecutionAlgorithm { orders_seen: 0 };
2613 let order = OrderTestBuilder::new(OrderType::Market)
2614 .instrument_id(InstrumentId::from("BTC/USDT.BINANCE"))
2615 .quantity(Quantity::from("1.0"))
2616 .build();
2617
2618 algorithm.on_order(order).unwrap();
2619
2620 assert_eq!(algorithm.orders_seen, 1);
2621 }
2622
2623 #[rstest]
2624 fn test_nautilus_execution_algorithm_macro_custom_field() {
2625 let exec_algorithm_id = ExecAlgorithmId::from("MACRO-001");
2626 let algorithm = MacroTestCustomField {
2627 inner: ExecutionAlgorithmCore::new(ExecutionAlgorithmConfig {
2628 exec_algorithm_id: Some(exec_algorithm_id),
2629 ..Default::default()
2630 }),
2631 };
2632
2633 assert_eq!(algorithm.id(), exec_algorithm_id);
2634 assert_eq!(algorithm.actor_id(), ActorId::from("MACRO-001"));
2635 }
2636
2637 #[rstest]
2638 fn test_algorithm_spawn_market_creates_valid_order() {
2639 let mut algo = create_test_algorithm();
2640 register_algorithm(&mut algo);
2641
2642 let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
2643 let mut primary = OrderAny::Market(MarketOrder::new(
2644 TraderId::from("TRADER-001"),
2645 StrategyId::from("STRAT-001"),
2646 instrument_id,
2647 ClientOrderId::from("O-001"),
2648 OrderSide::Buy,
2649 Quantity::from("1.0"),
2650 TimeInForce::Gtc,
2651 UUID4::new(),
2652 0.into(),
2653 false, false, None, None, None, None, None, None, None, None, ));
2664
2665 let spawned = algo.spawn_market(
2666 &mut primary,
2667 Quantity::from("0.5"),
2668 TimeInForce::Ioc,
2669 false,
2670 None, false, );
2673
2674 assert_eq!(spawned.client_order_id.as_str(), "O-001-E1");
2675 assert_eq!(spawned.instrument_id, instrument_id);
2676 assert_eq!(spawned.order_side(), OrderSide::Buy);
2677 assert_eq!(spawned.quantity, Quantity::from("0.5"));
2678 assert_eq!(spawned.time_in_force, TimeInForce::Ioc);
2679 assert_eq!(spawned.exec_algorithm_id, Some(algo.id()));
2680 assert_eq!(spawned.exec_spawn_id, Some(ClientOrderId::from("O-001")));
2681 }
2682
2683 #[rstest]
2684 fn test_algorithm_spawn_increments_sequence() {
2685 let mut algo = create_test_algorithm();
2686 register_algorithm(&mut algo);
2687
2688 let mut primary = OrderAny::Market(MarketOrder::new(
2689 TraderId::from("TRADER-001"),
2690 StrategyId::from("STRAT-001"),
2691 InstrumentId::from("BTC/USDT.BINANCE"),
2692 ClientOrderId::from("O-001"),
2693 OrderSide::Buy,
2694 Quantity::from("1.0"),
2695 TimeInForce::Gtc,
2696 UUID4::new(),
2697 0.into(),
2698 false,
2699 false,
2700 None,
2701 None,
2702 None,
2703 None,
2704 None,
2705 None,
2706 None,
2707 None,
2708 ));
2709
2710 let spawned1 = algo.spawn_market(
2711 &mut primary,
2712 Quantity::from("0.25"),
2713 TimeInForce::Ioc,
2714 false,
2715 None,
2716 false,
2717 );
2718 let spawned2 = algo.spawn_market(
2719 &mut primary,
2720 Quantity::from("0.25"),
2721 TimeInForce::Ioc,
2722 false,
2723 None,
2724 false,
2725 );
2726 let spawned3 = algo.spawn_market(
2727 &mut primary,
2728 Quantity::from("0.25"),
2729 TimeInForce::Ioc,
2730 false,
2731 None,
2732 false,
2733 );
2734
2735 assert_eq!(spawned1.client_order_id.as_str(), "O-001-E1");
2736 assert_eq!(spawned2.client_order_id.as_str(), "O-001-E2");
2737 assert_eq!(spawned3.client_order_id.as_str(), "O-001-E3");
2738 }
2739
2740 #[rstest]
2741 fn test_algorithm_default_handlers_do_not_panic() {
2742 let mut algo = create_test_algorithm();
2743
2744 algo.on_order_initialized(OrderInitialized::default());
2745 algo.on_order_denied(OrderDenied::default());
2746 algo.on_order_emulated(OrderEmulated::default());
2747 algo.on_order_released(OrderReleased::default());
2748 algo.on_order_submitted(OrderSubmitted::default());
2749 algo.on_order_rejected(OrderRejected::default());
2750 algo.on_order_accepted(OrderAccepted::default());
2751 algo.on_algo_order_canceled(OrderCanceled::default());
2752 algo.on_order_expired(OrderExpired::default());
2753 algo.on_order_triggered(OrderTriggered::default());
2754 algo.on_order_pending_update(OrderPendingUpdate::default());
2755 algo.on_order_pending_cancel(OrderPendingCancel::default());
2756 algo.on_order_modify_rejected(OrderModifyRejected::default());
2757 algo.on_order_cancel_rejected(OrderCancelRejected::default());
2758 algo.on_order_updated(OrderUpdated::default());
2759 algo.on_algo_order_filled(OrderFilledSpec::builder().build());
2760 algo.on_order_fill_voided(&OrderFillVoidedSpec::builder().build());
2761 }
2762
2763 #[rstest]
2764 fn test_strategy_subscription_tracking() {
2765 let mut algo = create_test_algorithm();
2766 let strategy_id = StrategyId::from("STRAT-001");
2767
2768 assert!(!algo.core.is_strategy_subscribed(&strategy_id));
2769
2770 algo.subscribe_to_strategy_events(strategy_id);
2771 assert!(algo.core.is_strategy_subscribed(&strategy_id));
2772
2773 algo.subscribe_to_strategy_events(strategy_id);
2775 assert!(algo.core.is_strategy_subscribed(&strategy_id));
2776 }
2777
2778 #[rstest]
2779 fn test_algorithm_reset() {
2780 let mut algo = create_test_algorithm();
2781 let strategy_id = StrategyId::from("STRAT-001");
2782 let primary_id = ClientOrderId::new("O-001");
2783
2784 let _ = algo.core.spawn_client_order_id(&primary_id);
2785 algo.core.add_subscribed_strategy(strategy_id);
2786
2787 assert!(algo.core.spawn_sequence(&primary_id).is_some());
2788 assert!(algo.core.is_strategy_subscribed(&strategy_id));
2789
2790 ExecutionAlgorithm::on_reset(&mut algo).unwrap();
2791
2792 assert!(algo.core.spawn_sequence(&primary_id).is_none());
2793 assert!(!algo.core.is_strategy_subscribed(&strategy_id));
2794 }
2795
2796 #[rstest]
2797 fn test_algorithm_spawn_limit_creates_valid_order() {
2798 let mut algo = create_test_algorithm();
2799 register_algorithm(&mut algo);
2800
2801 let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
2802 let mut primary = OrderAny::Market(MarketOrder::new(
2803 TraderId::from("TRADER-001"),
2804 StrategyId::from("STRAT-001"),
2805 instrument_id,
2806 ClientOrderId::from("O-001"),
2807 OrderSide::Buy,
2808 Quantity::from("1.0"),
2809 TimeInForce::Gtc,
2810 UUID4::new(),
2811 0.into(),
2812 false,
2813 false,
2814 None,
2815 None,
2816 None,
2817 None,
2818 None,
2819 None,
2820 None,
2821 None,
2822 ));
2823
2824 let price = Price::from("50000.0");
2825 let spawned = algo.spawn_limit(
2826 &mut primary,
2827 Quantity::from("0.5"),
2828 price,
2829 TimeInForce::Gtc,
2830 None, false, false, None, None, None, false, );
2838
2839 assert_eq!(spawned.client_order_id.as_str(), "O-001-E1");
2840 assert_eq!(spawned.instrument_id, instrument_id);
2841 assert_eq!(spawned.order_side(), OrderSide::Buy);
2842 assert_eq!(spawned.quantity, Quantity::from("0.5"));
2843 assert_eq!(spawned.price, price);
2844 assert_eq!(spawned.time_in_force, TimeInForce::Gtc);
2845 assert_eq!(spawned.exec_algorithm_id, Some(algo.id()));
2846 assert_eq!(spawned.exec_spawn_id, Some(ClientOrderId::from("O-001")));
2847 }
2848
2849 #[rstest]
2850 fn test_algorithm_spawn_market_to_limit_creates_valid_order() {
2851 let mut algo = create_test_algorithm();
2852 register_algorithm(&mut algo);
2853
2854 let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
2855 let mut primary = OrderAny::Market(MarketOrder::new(
2856 TraderId::from("TRADER-001"),
2857 StrategyId::from("STRAT-001"),
2858 instrument_id,
2859 ClientOrderId::from("O-001"),
2860 OrderSide::Buy,
2861 Quantity::from("1.0"),
2862 TimeInForce::Gtc,
2863 UUID4::new(),
2864 0.into(),
2865 false,
2866 false,
2867 None,
2868 None,
2869 None,
2870 None,
2871 None,
2872 None,
2873 None,
2874 None,
2875 ));
2876
2877 let spawned = algo.spawn_market_to_limit(
2878 &mut primary,
2879 Quantity::from("0.5"),
2880 TimeInForce::Gtc,
2881 None, false, None, None, None, false, );
2888
2889 assert_eq!(spawned.client_order_id.as_str(), "O-001-E1");
2890 assert_eq!(spawned.instrument_id, instrument_id);
2891 assert_eq!(spawned.order_side(), OrderSide::Buy);
2892 assert_eq!(spawned.quantity, Quantity::from("0.5"));
2893 assert_eq!(spawned.time_in_force, TimeInForce::Gtc);
2894 assert_eq!(spawned.exec_algorithm_id, Some(algo.id()));
2895 assert_eq!(spawned.exec_spawn_id, Some(ClientOrderId::from("O-001")));
2896 }
2897
2898 #[rstest]
2899 fn test_algorithm_spawn_market_with_tags() {
2900 let mut algo = create_test_algorithm();
2901 register_algorithm(&mut algo);
2902
2903 let mut primary = OrderAny::Market(MarketOrder::new(
2904 TraderId::from("TRADER-001"),
2905 StrategyId::from("STRAT-001"),
2906 InstrumentId::from("BTC/USDT.BINANCE"),
2907 ClientOrderId::from("O-001"),
2908 OrderSide::Buy,
2909 Quantity::from("1.0"),
2910 TimeInForce::Gtc,
2911 UUID4::new(),
2912 0.into(),
2913 false,
2914 false,
2915 None,
2916 None,
2917 None,
2918 None,
2919 None,
2920 None,
2921 None,
2922 None,
2923 ));
2924
2925 let tags = vec![ustr::Ustr::from("TAG1"), ustr::Ustr::from("TAG2")];
2926 let spawned = algo.spawn_market(
2927 &mut primary,
2928 Quantity::from("0.5"),
2929 TimeInForce::Ioc,
2930 false,
2931 Some(tags.clone()),
2932 false,
2933 );
2934
2935 assert_eq!(spawned.tags, Some(tags));
2936 }
2937
2938 #[rstest]
2939 fn test_algorithm_spawn_propagates_primary_fields() {
2940 let mut algo = create_test_algorithm();
2941 register_algorithm(&mut algo);
2942
2943 let mut params = indexmap::IndexMap::new();
2944 params.insert(ustr::Ustr::from("horizon_secs"), ustr::Ustr::from("30"));
2945 params.insert(ustr::Ustr::from("interval_secs"), ustr::Ustr::from("10"));
2946 let primary_tags = vec![ustr::Ustr::from("PRIMARY_TAG")];
2947 let linked_order_ids = vec![ClientOrderId::from("LINK-1")];
2948 let client_order_id = ClientOrderId::from("O-001");
2949
2950 let mut primary = OrderAny::Market(MarketOrder::new(
2951 TraderId::from("TRADER-001"),
2952 StrategyId::from("STRAT-001"),
2953 InstrumentId::from("BTC/USDT.BINANCE"),
2954 client_order_id,
2955 OrderSide::Buy,
2956 Quantity::from("1.0"),
2957 TimeInForce::Gtc,
2958 UUID4::new(),
2959 0.into(),
2960 false, true, None, None, Some(linked_order_ids.clone()),
2965 None, Some(algo.id()),
2967 Some(params.clone()),
2968 Some(client_order_id),
2969 Some(primary_tags.clone()),
2970 ));
2971
2972 let spawned_market = algo.spawn_market(
2973 &mut primary,
2974 Quantity::from("0.25"),
2975 TimeInForce::Ioc,
2976 false,
2977 None, false,
2979 );
2980 assert!(spawned_market.is_quote_quantity);
2981 assert_eq!(spawned_market.exec_algorithm_params, Some(params.clone()));
2982 assert_eq!(spawned_market.tags, Some(primary_tags.clone()));
2983 assert_eq!(
2984 spawned_market.linked_order_ids,
2985 Some(linked_order_ids.clone())
2986 );
2987
2988 let spawned_limit = algo.spawn_limit(
2989 &mut primary,
2990 Quantity::from("0.25"),
2991 Price::from("50000.0"),
2992 TimeInForce::Gtc,
2993 None, false, false, None, None, None, false,
3000 );
3001 assert!(spawned_limit.is_quote_quantity);
3002 assert_eq!(spawned_limit.exec_algorithm_params, Some(params.clone()));
3003 assert_eq!(spawned_limit.tags, Some(primary_tags.clone()));
3004 assert_eq!(
3005 spawned_limit.linked_order_ids,
3006 Some(linked_order_ids.clone())
3007 );
3008
3009 let spawned_mtl = algo.spawn_market_to_limit(
3010 &mut primary,
3011 Quantity::from("0.25"),
3012 TimeInForce::Gtc,
3013 None, false, None, None, None, false,
3019 );
3020 assert!(spawned_mtl.is_quote_quantity);
3021 assert_eq!(spawned_mtl.exec_algorithm_params, Some(params));
3022 assert_eq!(spawned_mtl.tags, Some(primary_tags));
3023 assert_eq!(spawned_mtl.linked_order_ids, Some(linked_order_ids));
3024 }
3025
3026 #[rstest]
3027 fn test_muldiv_floor_u128_narrow_path_truncates() {
3028 assert_eq!(muldiv_floor_u128(50, 3, 5), 30);
3029 assert_eq!(muldiv_floor_u128(10, 2, 3), 6);
3030 assert_eq!(muldiv_floor_u128(u128::MAX, 7, 7), u128::MAX);
3031 }
3032
3033 #[rstest]
3034 fn test_muldiv_floor_u128_wide_path_floors_exactly() {
3035 let a = 10u128.pow(30);
3039 let b = 7 * 10u128.pow(30);
3040 let c = b + 1;
3041 assert!(a.checked_mul(b).is_none());
3042 assert_eq!(muldiv_floor_u128(a, b, c), a - 1);
3043 }
3044
3045 #[rstest]
3046 fn test_algorithm_reduce_primary_order() {
3047 let mut algo = create_test_algorithm();
3048 register_algorithm(&mut algo);
3049
3050 let order = OrderAny::Market(MarketOrder::new(
3051 TraderId::from("TRADER-001"),
3052 StrategyId::from("STRAT-001"),
3053 InstrumentId::from("BTC/USDT.BINANCE"),
3054 ClientOrderId::from("O-001"),
3055 OrderSide::Buy,
3056 Quantity::from("1.0"),
3057 TimeInForce::Gtc,
3058 UUID4::new(),
3059 0.into(),
3060 false,
3061 false,
3062 None,
3063 None,
3064 None,
3065 None,
3066 None,
3067 None,
3068 None,
3069 None,
3070 ));
3071
3072 let mut primary = TestOrderStubs::make_accepted_order(&order);
3074
3075 {
3076 let cache_rc = algo.core.cache_rc();
3077 let mut cache = cache_rc.borrow_mut();
3078 cache.add_order(primary.clone(), None, None, false).unwrap();
3079 }
3080
3081 let spawn_qty = Quantity::from("0.3");
3082 algo.reduce_primary_order(&mut primary, spawn_qty);
3083
3084 assert_eq!(primary.quantity(), Quantity::from("0.7"));
3085 }
3086
3087 #[rstest]
3088 fn test_algorithm_reduce_primary_order_publishes_updated_event() {
3089 let mut algo = create_test_algorithm();
3090 register_algorithm(&mut algo);
3091
3092 let strategy_id = StrategyId::from("STRAT-ALGO-REDUCE-PUBLISH");
3093 let order = OrderAny::Market(MarketOrder::new(
3094 TraderId::from("TRADER-001"),
3095 strategy_id,
3096 InstrumentId::from("BTC/USDT.BINANCE"),
3097 ClientOrderId::from("O-ALGO-REDUCE"),
3098 OrderSide::Buy,
3099 Quantity::from("1.0"),
3100 TimeInForce::Gtc,
3101 UUID4::new(),
3102 0.into(),
3103 false,
3104 false,
3105 None,
3106 None,
3107 None,
3108 None,
3109 None,
3110 None,
3111 None,
3112 None,
3113 ));
3114 let mut primary = TestOrderStubs::make_accepted_order(&order);
3115
3116 {
3117 let cache_rc = algo.core.cache_rc();
3118 let mut cache = cache_rc.borrow_mut();
3119 cache.add_order(primary.clone(), None, None, false).unwrap();
3120 }
3121
3122 let (handler, events) = subscribe_order_topic(strategy_id);
3123
3124 algo.reduce_primary_order(&mut primary, Quantity::from("0.3"));
3125
3126 msgbus::unsubscribe_order_events(format!("events.order.{strategy_id}").into(), &handler);
3127 let events = events.borrow();
3128
3129 assert_eq!(events.len(), 1);
3130 assert!(matches!(
3131 &events[0],
3132 OrderEventAny::Updated(event) if event.quantity == Quantity::from("0.7")
3133 ));
3134 }
3135
3136 #[rstest]
3137 fn test_algorithm_submit_order_publishes_initialized_for_new_order() {
3138 let mut algo = create_test_algorithm();
3139 register_algorithm(&mut algo);
3140
3141 let strategy_id = StrategyId::from("STRAT-ALGO-INIT-PUBLISH");
3142 let order = OrderAny::Market(MarketOrder::new(
3143 TraderId::from("TRADER-001"),
3144 strategy_id,
3145 InstrumentId::from("BTC/USDT.BINANCE"),
3146 ClientOrderId::from("O-ALGO-INIT"),
3147 OrderSide::Buy,
3148 Quantity::from("1.0"),
3149 TimeInForce::Gtc,
3150 UUID4::new(),
3151 0.into(),
3152 false,
3153 false,
3154 None,
3155 None,
3156 None,
3157 None,
3158 None,
3159 None,
3160 None,
3161 None,
3162 ));
3163 let (handler, events) = subscribe_order_topic(strategy_id);
3164
3165 algo.submit_order(order.clone(), None, None).unwrap();
3166
3167 msgbus::unsubscribe_order_events(format!("events.order.{strategy_id}").into(), &handler);
3168 let events = events.borrow();
3169
3170 assert_eq!(events.len(), 1);
3171 assert!(matches!(
3172 &events[0],
3173 OrderEventAny::Initialized(event) if event.client_order_id == order.client_order_id()
3174 ));
3175 }
3176
3177 #[rstest]
3178 fn test_algorithm_submit_order_does_not_republish_initialized_for_existing_order() {
3179 let mut algo = create_test_algorithm();
3180 register_algorithm(&mut algo);
3181
3182 let strategy_id = StrategyId::from("STRAT-ALGO-INIT-EXISTING");
3183 let order = OrderAny::Market(MarketOrder::new(
3184 TraderId::from("TRADER-001"),
3185 strategy_id,
3186 InstrumentId::from("BTC/USDT.BINANCE"),
3187 ClientOrderId::from("O-ALGO-INIT-EXISTING"),
3188 OrderSide::Buy,
3189 Quantity::from("1.0"),
3190 TimeInForce::Gtc,
3191 UUID4::new(),
3192 0.into(),
3193 false,
3194 false,
3195 None,
3196 None,
3197 None,
3198 None,
3199 None,
3200 None,
3201 None,
3202 None,
3203 ));
3204 {
3205 let cache_rc = algo.core.cache_rc();
3206 let mut cache = cache_rc.borrow_mut();
3207 cache.add_order(order.clone(), None, None, true).unwrap();
3208 }
3209 let (handler, events) = subscribe_order_topic(strategy_id);
3210
3211 algo.submit_order(order, None, None).unwrap();
3212
3213 msgbus::unsubscribe_order_events(format!("events.order.{strategy_id}").into(), &handler);
3214 assert!(events.borrow().is_empty());
3215 }
3216
3217 #[rstest]
3218 fn test_algorithm_submit_order_refuses_emulated_limit_spawn() {
3219 let mut algo = create_test_algorithm();
3220 register_algorithm(&mut algo);
3221
3222 let strategy_id = StrategyId::from("STRAT-ALGO-EMULATED-LIMIT");
3223 let order = OrderTestBuilder::new(OrderType::Market)
3224 .strategy_id(strategy_id)
3225 .instrument_id(InstrumentId::from("BTC/USDT.BINANCE"))
3226 .client_order_id(ClientOrderId::from("O-ALGO-EMULATED-LIMIT"))
3227 .quantity(Quantity::from("1.0"))
3228 .build();
3229 let mut primary = TestOrderStubs::make_accepted_order(&order);
3230 {
3231 let cache_rc = algo.core.cache_rc();
3232 let mut cache = cache_rc.borrow_mut();
3233 cache.add_order(primary.clone(), None, None, false).unwrap();
3234 }
3235 let (event_handler, events) = subscribe_order_topic(strategy_id);
3236 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
3237 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
3238 msgbus::register_trading_command_endpoint(
3239 MessagingSwitchboard::risk_engine_queue_execute(),
3240 risk_handler,
3241 );
3242 let (emulator_handler, emulator_messages): (
3243 _,
3244 TypedIntoMessageSavingHandler<TradingCommand>,
3245 ) = get_typed_into_message_saving_handler(Some(Ustr::from("OrderEmulator.execute")));
3246 msgbus::register_trading_command_endpoint(
3247 MessagingSwitchboard::order_emulator_execute(),
3248 emulator_handler,
3249 );
3250
3251 let spawned = algo.spawn_limit(
3252 &mut primary,
3253 Quantity::from("0.4"),
3254 Price::from("50000.0"),
3255 TimeInForce::Gtc,
3256 None,
3257 false,
3258 false,
3259 None,
3260 Some(TriggerType::BidAsk),
3261 None,
3262 true,
3263 );
3264 let spawned = OrderAny::Limit(spawned);
3265 let client_order_id = spawned.client_order_id();
3266 let result = algo.submit_order(spawned, None, None);
3267
3268 msgbus::unsubscribe_order_events(
3269 format!("events.order.{strategy_id}").into(),
3270 &event_handler,
3271 );
3272 let cache = algo.core.cache_ref();
3273 let error = result.unwrap_err();
3274 assert!(
3275 error
3276 .downcast_ref::<EmulatedOrderSubmissionError>()
3277 .is_some()
3278 );
3279 let error = error.to_string();
3280 assert!(error.contains("live emulation trigger"), "{error}");
3281 assert!(error.contains(client_order_id.as_str()), "{error}");
3282 assert!(risk_messages.get_messages().is_empty());
3283 assert!(emulator_messages.get_messages().is_empty());
3284 assert!(!cache.order_exists(&client_order_id));
3285 assert!(!events.borrow().iter().any(|event| matches!(
3286 event,
3287 OrderEventAny::Initialized(initialized)
3288 if initialized.client_order_id == client_order_id
3289 )));
3290 assert_eq!(
3294 cache.order(&primary.client_order_id()).unwrap().quantity(),
3295 Quantity::from("0.6"),
3296 );
3297 drop(cache);
3298 assert!(
3299 algo.core
3300 .take_pending_spawn_reduction(client_order_id)
3301 .is_none()
3302 );
3303 }
3304
3305 #[rstest]
3306 fn test_algorithm_submit_order_routes_unemulated_spawn_to_risk() {
3307 let mut algo = create_test_algorithm();
3308 register_algorithm(&mut algo);
3309
3310 let mut primary = OrderTestBuilder::new(OrderType::Market)
3311 .instrument_id(InstrumentId::from("BTC/USDT.BINANCE"))
3312 .client_order_id(ClientOrderId::from("O-ALGO-UNEMULATED"))
3313 .quantity(Quantity::from("1.0"))
3314 .build();
3315 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
3316 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
3317 msgbus::register_trading_command_endpoint(
3318 MessagingSwitchboard::risk_engine_queue_execute(),
3319 risk_handler,
3320 );
3321
3322 let spawned = algo.spawn_limit(
3323 &mut primary,
3324 Quantity::from("0.4"),
3325 Price::from("50000.0"),
3326 TimeInForce::Gtc,
3327 None,
3328 false,
3329 false,
3330 None,
3331 None,
3332 None,
3333 false,
3334 );
3335 let client_order_id = spawned.client_order_id;
3336 algo.submit_order(OrderAny::Limit(spawned), None, None)
3337 .unwrap();
3338
3339 let risk_messages = risk_messages.get_messages();
3340 assert_eq!(risk_messages.len(), 1);
3341 assert!(matches!(
3342 risk_messages.first(),
3343 Some(TradingCommand::SubmitOrder(command))
3344 if command.client_order_id == client_order_id
3345 ));
3346 }
3347
3348 #[rstest]
3349 fn test_algorithm_spawn_market_with_reduce_primary() {
3350 let mut algo = create_test_algorithm();
3351 register_algorithm(&mut algo);
3352
3353 let order = OrderAny::Market(MarketOrder::new(
3354 TraderId::from("TRADER-001"),
3355 StrategyId::from("STRAT-001"),
3356 InstrumentId::from("BTC/USDT.BINANCE"),
3357 ClientOrderId::from("O-001"),
3358 OrderSide::Buy,
3359 Quantity::from("1.0"),
3360 TimeInForce::Gtc,
3361 UUID4::new(),
3362 0.into(),
3363 false,
3364 false,
3365 None,
3366 None,
3367 None,
3368 None,
3369 None,
3370 None,
3371 None,
3372 None,
3373 ));
3374
3375 let mut primary = TestOrderStubs::make_accepted_order(&order);
3377
3378 {
3379 let cache_rc = algo.core.cache_rc();
3380 let mut cache = cache_rc.borrow_mut();
3381 cache.add_order(primary.clone(), None, None, false).unwrap();
3382 }
3383
3384 let spawned = algo.spawn_market(
3385 &mut primary,
3386 Quantity::from("0.4"),
3387 TimeInForce::Ioc,
3388 false,
3389 None,
3390 true, );
3392
3393 assert_eq!(spawned.quantity, Quantity::from("0.4"));
3394 assert_eq!(primary.quantity(), Quantity::from("0.6"));
3395 }
3396 #[rstest]
3397 fn test_algorithm_forwards_captured_params_to_spawned_order() {
3398 let mut algo = create_test_algorithm();
3399 register_algorithm(&mut algo);
3400
3401 let strategy_id = StrategyId::from("STRAT-FWD-001");
3402 let mut primary = OrderAny::Market(MarketOrder::new(
3403 TraderId::from("TRADER-001"),
3404 strategy_id,
3405 InstrumentId::from("BTC/USDT.BINANCE"),
3406 ClientOrderId::from("O-FWD-001"),
3407 OrderSide::Buy,
3408 Quantity::from("1.0"),
3409 TimeInForce::Gtc,
3410 UUID4::new(),
3411 0.into(),
3412 false,
3413 false,
3414 None,
3415 None,
3416 None,
3417 None,
3418 None,
3419 None,
3420 None,
3421 None,
3422 ));
3423 {
3424 let cache_rc = algo.core.cache_rc();
3425 let mut cache = cache_rc.borrow_mut();
3426 cache.add_order(primary.clone(), None, None, true).unwrap();
3427 }
3428
3429 let params = nautilus_core::Params::new(
3430 [("is_leverage".to_string(), serde_json::Value::Bool(true))].into(),
3431 );
3432
3433 let command = SubmitOrder::new(
3434 TraderId::from("TRADER-001"),
3435 None,
3436 strategy_id,
3437 primary.instrument_id(),
3438 primary.client_order_id(),
3439 primary.init_event().clone(),
3440 primary.exec_algorithm_id(),
3441 None,
3442 Some(params),
3443 UUID4::new(),
3444 0.into(),
3445 None,
3446 );
3447 algo.execute(TradingCommand::SubmitOrder(command)).unwrap();
3448
3449 let received = Rc::new(RefCell::new(None::<SubmitOrder>));
3450 let handler = msgbus::TypedIntoHandler::from({
3451 let captured = received.clone();
3452 move |cmd: TradingCommand| {
3453 if let TradingCommand::SubmitOrder(cmd) = cmd {
3454 *captured.borrow_mut() = Some(cmd);
3455 }
3456 }
3457 });
3458 msgbus::register_trading_command_endpoint(
3459 MessagingSwitchboard::risk_engine_queue_execute(),
3460 handler,
3461 );
3462
3463 let spawned = algo.spawn_market(
3464 &mut primary,
3465 Quantity::from("0.4"),
3466 TimeInForce::Ioc,
3467 false,
3468 None,
3469 false, );
3471 algo.submit_order(OrderAny::Market(spawned), None, None)
3472 .unwrap();
3473
3474 let captured = received.borrow();
3475 let cmd = captured.as_ref().expect("expected a forwarded SubmitOrder");
3476 assert_eq!(cmd.client_order_id, ClientOrderId::from("O-FWD-001-E1"));
3477 assert_eq!(
3478 cmd.params.as_ref().and_then(|p| p.get_bool("is_leverage")),
3479 Some(true),
3480 );
3481 }
3482
3483 #[rstest]
3484 fn test_algorithm_routes_modify_and_cancel_commands_through_engine_queues() {
3485 let mut modify_algo = create_test_algorithm();
3486 let mut cancel_algo = create_test_algorithm();
3487 register_algorithm(&mut modify_algo);
3488 register_algorithm(&mut cancel_algo);
3489
3490 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
3491 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
3492 msgbus::register_trading_command_endpoint(
3493 MessagingSwitchboard::risk_engine_queue_execute(),
3494 risk_handler,
3495 );
3496 let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
3497 get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
3498 msgbus::register_trading_command_endpoint(
3499 MessagingSwitchboard::exec_engine_queue_execute(),
3500 exec_handler,
3501 );
3502
3503 let mut modify_order = TestOrderStubs::make_accepted_order(
3504 &OrderTestBuilder::new(OrderType::Limit)
3505 .strategy_id(StrategyId::from("STRAT-ALGO-ROUTING"))
3506 .instrument_id(InstrumentId::from("BTC/USDT.BINANCE"))
3507 .client_order_id(ClientOrderId::from("O-ALGO-MODIFY"))
3508 .quantity(Quantity::from("1.0"))
3509 .price(Price::from("50000.0"))
3510 .build(),
3511 );
3512 let mut cancel_order = TestOrderStubs::make_accepted_order(
3513 &OrderTestBuilder::new(OrderType::Market)
3514 .strategy_id(StrategyId::from("STRAT-ALGO-ROUTING"))
3515 .instrument_id(InstrumentId::from("BTC/USDT.BINANCE"))
3516 .client_order_id(ClientOrderId::from("O-ALGO-CANCEL"))
3517 .quantity(Quantity::from("1.0"))
3518 .build(),
3519 );
3520 {
3521 let cache_rc = modify_algo.core.cache_rc();
3522 let mut cache = cache_rc.borrow_mut();
3523 cache
3524 .add_order(modify_order.clone(), None, None, false)
3525 .unwrap();
3526 }
3527 {
3528 let cache_rc = cancel_algo.core.cache_rc();
3529 let mut cache = cache_rc.borrow_mut();
3530 cache
3531 .add_order(cancel_order.clone(), None, None, false)
3532 .unwrap();
3533 }
3534
3535 modify_algo
3536 .modify_order(
3537 &mut modify_order,
3538 None,
3539 Some(Price::from("51000.0")),
3540 None,
3541 None,
3542 )
3543 .unwrap();
3544 cancel_algo.cancel_order(&mut cancel_order, None).unwrap();
3545
3546 let risk_messages = risk_messages.get_messages();
3547 let exec_messages = exec_messages.get_messages();
3548 assert_eq!(risk_messages.len(), 1);
3549 assert!(matches!(
3550 risk_messages.first(),
3551 Some(TradingCommand::ModifyOrder(command))
3552 if command.client_order_id == modify_order.client_order_id()
3553 ));
3554 assert_eq!(exec_messages.len(), 1);
3555 assert!(matches!(
3556 exec_messages.first(),
3557 Some(TradingCommand::CancelOrder(command))
3558 if command.client_order_id == cancel_order.client_order_id()
3559 ));
3560 }
3561
3562 #[rstest]
3563 fn test_algorithm_submit_order_list_captures_params_per_order() {
3564 use nautilus_common::messages::execution::SubmitOrderList;
3565 use nautilus_model::identifiers::OrderListId;
3566
3567 let mut algo = create_test_algorithm();
3568 register_algorithm(&mut algo);
3569
3570 let strategy_id = StrategyId::from("STRAT-LIST-001");
3571 let order1 = OrderAny::Market(MarketOrder::new(
3572 TraderId::from("TRADER-001"),
3573 strategy_id,
3574 InstrumentId::from("BTC/USDT.BINANCE"),
3575 ClientOrderId::from("O-LIST-001"),
3576 OrderSide::Buy,
3577 Quantity::from("1.0"),
3578 TimeInForce::Gtc,
3579 UUID4::new(),
3580 0.into(),
3581 false,
3582 false,
3583 None,
3584 None,
3585 None,
3586 None,
3587 None,
3588 None,
3589 None,
3590 None,
3591 ));
3592 let order2 = OrderAny::Market(MarketOrder::new(
3593 TraderId::from("TRADER-001"),
3594 strategy_id,
3595 InstrumentId::from("BTC/USDT.BINANCE"),
3596 ClientOrderId::from("O-LIST-002"),
3597 OrderSide::Buy,
3598 Quantity::from("1.0"),
3599 TimeInForce::Gtc,
3600 UUID4::new(),
3601 0.into(),
3602 false,
3603 false,
3604 None,
3605 None,
3606 None,
3607 None,
3608 None,
3609 None,
3610 None,
3611 None,
3612 ));
3613 {
3614 let cache_rc = algo.core.cache_rc();
3615 let mut cache = cache_rc.borrow_mut();
3616 cache.add_order(order1.clone(), None, None, true).unwrap();
3617 cache.add_order(order2.clone(), None, None, true).unwrap();
3618 }
3619
3620 let order_list = OrderList::new(
3621 OrderListId::from("OL-001"),
3622 order1.instrument_id(),
3623 strategy_id,
3624 vec![order1.client_order_id(), order2.client_order_id()],
3625 0.into(),
3626 );
3627
3628 let params = nautilus_core::Params::new(
3629 [("is_leverage".to_string(), serde_json::Value::Bool(true))].into(),
3630 );
3631
3632 let command = SubmitOrderList::new(
3633 TraderId::from("TRADER-001"),
3634 None,
3635 strategy_id,
3636 order_list,
3637 vec![order1.init_event().clone(), order2.init_event().clone()],
3638 order1.exec_algorithm_id(),
3639 None,
3640 Some(params),
3641 UUID4::new(),
3642 0.into(),
3643 None,
3644 );
3645 algo.execute(TradingCommand::SubmitOrderList(command))
3646 .unwrap();
3647
3648 assert_eq!(
3649 algo.order_client_ids,
3650 [
3651 ClientOrderId::from("O-LIST-001"),
3652 ClientOrderId::from("O-LIST-002"),
3653 ],
3654 );
3655
3656 for id in ["O-LIST-001", "O-LIST-002"] {
3657 assert_eq!(
3658 algo.core
3659 .submit_params(&ClientOrderId::from(id))
3660 .and_then(|p| p.get_bool("is_leverage")),
3661 Some(true),
3662 "expected forwarded params for {id}",
3663 );
3664 }
3665 }
3666
3667 #[rstest]
3668 fn test_algorithm_generate_order_canceled() {
3669 let mut algo = create_test_algorithm();
3670 register_algorithm(&mut algo);
3671
3672 let order = OrderAny::Market(MarketOrder::new(
3673 TraderId::from("TRADER-001"),
3674 StrategyId::from("STRAT-001"),
3675 InstrumentId::from("BTC/USDT.BINANCE"),
3676 ClientOrderId::from("O-001"),
3677 OrderSide::Buy,
3678 Quantity::from("1.0"),
3679 TimeInForce::Gtc,
3680 UUID4::new(),
3681 0.into(),
3682 false,
3683 false,
3684 None,
3685 None,
3686 None,
3687 None,
3688 None,
3689 None,
3690 None,
3691 None,
3692 ));
3693
3694 let event = algo.generate_order_canceled(&order);
3695
3696 assert_eq!(event.trader_id, TraderId::from("TRADER-001"));
3697 assert_eq!(event.strategy_id, StrategyId::from("STRAT-001"));
3698 assert_eq!(event.instrument_id, InstrumentId::from("BTC/USDT.BINANCE"));
3699 assert_eq!(event.client_order_id, ClientOrderId::from("O-001"));
3700 }
3701
3702 #[rstest]
3703 fn test_algorithm_handle_cancel_order_publishes_instrument_canceled_topic() {
3704 let mut algo = create_test_algorithm();
3705 register_algorithm(&mut algo);
3706
3707 let strategy_id = StrategyId::from("STRAT-ALGO-CANCEL-PUBLISH");
3708 let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
3709 let order = OrderAny::Market(MarketOrder::new(
3710 TraderId::from("TRADER-001"),
3711 strategy_id,
3712 instrument_id,
3713 ClientOrderId::from("O-ALGO-CANCEL"),
3714 OrderSide::Buy,
3715 Quantity::from("1.0"),
3716 TimeInForce::Gtc,
3717 UUID4::new(),
3718 0.into(),
3719 false,
3720 false,
3721 None,
3722 None,
3723 None,
3724 None,
3725 None,
3726 None,
3727 None,
3728 None,
3729 ));
3730 let order = TestOrderStubs::make_accepted_order(&order);
3731
3732 {
3733 let cache_rc = algo.core.cache_rc();
3734 let mut cache = cache_rc.borrow_mut();
3735 cache.add_order(order.clone(), None, None, false).unwrap();
3736 }
3737
3738 let received = Rc::new(RefCell::new(Vec::<OrderEventAny>::new()));
3739 let handler = TypedHandler::from({
3740 let received = received.clone();
3741 move |event: &OrderEventAny| {
3742 received.borrow_mut().push(event.clone());
3743 }
3744 });
3745 let topic = msgbus::switchboard::get_order_canceled_topic(instrument_id);
3746 msgbus::subscribe_order_events(topic.into(), handler.clone(), None);
3747
3748 let command = CancelOrder::new(
3749 order.trader_id(),
3750 None,
3751 strategy_id,
3752 instrument_id,
3753 order.client_order_id(),
3754 order.venue_order_id(),
3755 UUID4::new(),
3756 0.into(),
3757 None,
3758 None,
3759 );
3760 algo.handle_cancel_order(command).unwrap();
3761
3762 msgbus::unsubscribe_order_events(topic.into(), &handler);
3763 let received = received.borrow();
3764 assert_eq!(received.len(), 1);
3765 assert!(matches!(received[0], OrderEventAny::Canceled(_)));
3766 assert_eq!(received[0].client_order_id(), order.client_order_id());
3767 assert_eq!(received[0].instrument_id(), instrument_id);
3768 }
3769
3770 #[rstest]
3771 fn test_algorithm_execute_dispatches_modify_order_to_handler() {
3772 let unique_id = format!("TEST-{}", UUID4::new());
3773 let config = ExecutionAlgorithmConfig {
3774 exec_algorithm_id: Some(ExecAlgorithmId::new(&unique_id)),
3775 ..Default::default()
3776 };
3777 let mut algo = ModifyDispatchAlgorithm::new(config);
3778 algo.core
3779 .register(
3780 TraderId::from("TRADER-001"),
3781 Rc::new(RefCell::new(VirtualClock::new())),
3782 Rc::new(RefCell::new(Cache::default())),
3783 )
3784 .unwrap();
3785 algo.transition_state(ComponentTrigger::Initialize).unwrap();
3786 algo.transition_state(ComponentTrigger::Start).unwrap();
3787 algo.transition_state(ComponentTrigger::StartCompleted)
3788 .unwrap();
3789
3790 let client_order_id = ClientOrderId::from("O-ALGO-DISPATCH");
3791 let command = ModifyOrder::new(
3792 TraderId::from("TRADER-001"),
3793 None,
3794 StrategyId::from("STRAT-ALGO-DISPATCH"),
3795 InstrumentId::from("BTC/USDT.BINANCE"),
3796 client_order_id,
3797 None,
3798 Some(Quantity::from("0.5")),
3799 None,
3800 None,
3801 UUID4::new(),
3802 0.into(),
3803 None,
3804 None,
3805 );
3806
3807 algo.execute(TradingCommand::ModifyOrder(command)).unwrap();
3808
3809 assert_eq!(algo.modify_client_order_ids, vec![client_order_id]);
3810 }
3811
3812 #[rstest]
3813 fn test_algorithm_handle_modify_order_refuses_active_local_order_without_events() {
3814 let mut algo = create_test_algorithm();
3815 register_algorithm(&mut algo);
3816
3817 let strategy_id = StrategyId::from("STRAT-ALGO-MODIFY");
3818 let order = OrderTestBuilder::new(OrderType::Market)
3819 .trader_id(TraderId::from("TRADER-001"))
3820 .strategy_id(strategy_id)
3821 .instrument_id(InstrumentId::from("BTC/USDT.BINANCE"))
3822 .client_order_id(ClientOrderId::from("O-ALGO-MODIFY"))
3823 .quantity(Quantity::from("1.0"))
3824 .exec_algorithm_id(algo.id())
3825 .exec_spawn_id(ClientOrderId::from("O-ALGO-MODIFY"))
3826 .build();
3827 {
3828 let cache_rc = algo.core.cache_rc();
3829 cache_rc
3830 .borrow_mut()
3831 .add_order(order.clone(), None, None, false)
3832 .unwrap();
3833 }
3834 let (handler, events) = subscribe_order_topic(strategy_id);
3835 let command = ModifyOrder::new(
3836 order.trader_id(),
3837 None,
3838 strategy_id,
3839 order.instrument_id(),
3840 order.client_order_id(),
3841 None,
3842 Some(Quantity::from("0.5")),
3843 None,
3844 None,
3845 UUID4::new(),
3846 0.into(),
3847 None,
3848 None,
3849 );
3850
3851 algo.execute(TradingCommand::ModifyOrder(command)).unwrap();
3852
3853 msgbus::unsubscribe_order_events(format!("events.order.{strategy_id}").into(), &handler);
3854 let cached_order = algo.cache().order(&order.client_order_id()).unwrap();
3855 assert_eq!(cached_order.status(), OrderStatus::Initialized);
3856 assert_eq!(cached_order.quantity(), Quantity::from("1.0"));
3857 assert!(events.borrow().is_empty());
3858 }
3859
3860 #[rstest]
3861 fn test_algorithm_modify_order_in_place_updates_quantity() {
3862 let mut algo = create_test_algorithm();
3863 register_algorithm(&mut algo);
3864
3865 let strategy_id = StrategyId::from("STRAT-ALGO-MODIFY-IN-PLACE");
3866 let mut order = OrderAny::Limit(LimitOrder::new(
3867 TraderId::from("TRADER-001"),
3868 strategy_id,
3869 InstrumentId::from("BTC/USDT.BINANCE"),
3870 ClientOrderId::from("O-001"),
3871 OrderSide::Buy,
3872 Quantity::from("1.0"),
3873 Price::from("50000.0"),
3874 TimeInForce::Gtc,
3875 None, false, false, false, None, None, None, None, None, None, None, None, None, None, None, UUID4::new(),
3891 0.into(),
3892 ));
3893
3894 {
3895 let cache_rc = algo.core.cache_rc();
3896 let mut cache = cache_rc.borrow_mut();
3897 cache.add_order(order.clone(), None, None, false).unwrap();
3898 }
3899
3900 let new_qty = Quantity::from("0.5");
3901 let (handler, events) = subscribe_order_topic(strategy_id);
3902
3903 algo.modify_order_in_place(&mut order, Some(new_qty), None, None)
3904 .unwrap();
3905
3906 msgbus::unsubscribe_order_events(format!("events.order.{strategy_id}").into(), &handler);
3907 let events = events.borrow();
3908
3909 assert_eq!(order.quantity(), new_qty);
3910 assert_eq!(events.len(), 1);
3911 assert!(matches!(
3912 &events[0],
3913 OrderEventAny::Updated(event) if event.quantity == new_qty
3914 ));
3915 }
3916
3917 #[rstest]
3918 fn test_algorithm_modify_order_in_place_rejects_no_changes() {
3919 let mut algo = create_test_algorithm();
3920 register_algorithm(&mut algo);
3921
3922 let mut order = OrderAny::Limit(LimitOrder::new(
3923 TraderId::from("TRADER-001"),
3924 StrategyId::from("STRAT-001"),
3925 InstrumentId::from("BTC/USDT.BINANCE"),
3926 ClientOrderId::from("O-001"),
3927 OrderSide::Buy,
3928 Quantity::from("1.0"),
3929 Price::from("50000.0"),
3930 TimeInForce::Gtc,
3931 None,
3932 false,
3933 false,
3934 false,
3935 None,
3936 None,
3937 None,
3938 None,
3939 None,
3940 None,
3941 None,
3942 None,
3943 None,
3944 None,
3945 None,
3946 UUID4::new(),
3947 0.into(),
3948 ));
3949
3950 let result =
3952 algo.modify_order_in_place(&mut order, Some(Quantity::from("1.0")), None, None);
3953
3954 assert!(result.is_err());
3955 assert!(
3956 result
3957 .unwrap_err()
3958 .to_string()
3959 .contains("no parameters differ")
3960 );
3961 }
3962
3963 #[rstest]
3964 fn test_spawned_order_denied_restores_primary_quantity() {
3965 let mut algo = create_test_algorithm();
3966 register_algorithm(&mut algo);
3967
3968 let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
3969 let exec_algorithm_id = algo.id();
3970 let client_order_id = ClientOrderId::from("O-001");
3971
3972 let mut primary = OrderAny::Market(MarketOrder::new(
3973 TraderId::from("TRADER-001"),
3974 StrategyId::from("STRAT-001"),
3975 instrument_id,
3976 client_order_id,
3977 OrderSide::Buy,
3978 Quantity::from("1.0"),
3979 TimeInForce::Gtc,
3980 UUID4::new(),
3981 0.into(),
3982 false,
3983 false,
3984 None,
3985 None,
3986 None,
3987 None,
3988 Some(exec_algorithm_id),
3989 None,
3990 Some(client_order_id),
3991 None,
3992 ));
3993
3994 {
3995 let cache_rc = algo.core.cache_rc();
3996 let mut cache = cache_rc.borrow_mut();
3997 cache.add_order(primary.clone(), None, None, false).unwrap();
3998 }
3999
4000 let spawned = algo.spawn_market(
4001 &mut primary,
4002 Quantity::from("0.5"),
4003 TimeInForce::Fok,
4004 false,
4005 None,
4006 true,
4007 );
4008
4009 assert_eq!(primary.quantity(), Quantity::from("0.5"));
4010
4011 let spawned_order = OrderAny::Market(spawned);
4012 {
4013 let cache_rc = algo.core.cache_rc();
4014 let mut cache = cache_rc.borrow_mut();
4015 cache
4016 .add_order(spawned_order.clone(), None, None, false)
4017 .unwrap();
4018 }
4019
4020 let denied = OrderDeniedSpec::builder()
4021 .trader_id(spawned_order.trader_id())
4022 .strategy_id(spawned_order.strategy_id())
4023 .instrument_id(spawned_order.instrument_id())
4024 .client_order_id(spawned_order.client_order_id())
4025 .reason("TEST_DENIAL".into())
4026 .build();
4027
4028 {
4029 let cache_rc = algo.core.cache_rc();
4030 let mut cache = cache_rc.borrow_mut();
4031 cache.update_order(&OrderEventAny::Denied(denied)).unwrap();
4032 }
4033
4034 algo.handle_order_event(OrderEventAny::Denied(denied));
4035
4036 let restored_primary = algo.cache().order(&client_order_id).unwrap();
4037 assert_eq!(restored_primary.quantity(), Quantity::from("1.0"));
4038 }
4039
4040 #[rstest]
4041 fn test_spawned_order_rejected_restores_primary_quantity() {
4042 let mut algo = create_test_algorithm();
4043 register_algorithm(&mut algo);
4044
4045 let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
4046 let exec_algorithm_id = algo.id();
4047 let client_order_id = ClientOrderId::from("O-001");
4048
4049 let mut primary = OrderAny::Market(MarketOrder::new(
4050 TraderId::from("TRADER-001"),
4051 StrategyId::from("STRAT-001"),
4052 instrument_id,
4053 client_order_id,
4054 OrderSide::Buy,
4055 Quantity::from("1.0"),
4056 TimeInForce::Gtc,
4057 UUID4::new(),
4058 0.into(),
4059 false,
4060 false,
4061 None,
4062 None,
4063 None,
4064 None,
4065 Some(exec_algorithm_id),
4066 None,
4067 Some(client_order_id),
4068 None,
4069 ));
4070
4071 {
4072 let cache_rc = algo.core.cache_rc();
4073 let mut cache = cache_rc.borrow_mut();
4074 cache.add_order(primary.clone(), None, None, false).unwrap();
4075 }
4076
4077 let spawned = algo.spawn_market(
4078 &mut primary,
4079 Quantity::from("0.5"),
4080 TimeInForce::Fok,
4081 false,
4082 None,
4083 true,
4084 );
4085
4086 assert_eq!(primary.quantity(), Quantity::from("0.5"));
4087
4088 let spawned_order = OrderAny::Market(spawned);
4089 {
4090 let cache_rc = algo.core.cache_rc();
4091 let mut cache = cache_rc.borrow_mut();
4092 cache
4093 .add_order(spawned_order.clone(), None, None, false)
4094 .unwrap();
4095 }
4096
4097 let rejected = OrderRejectedSpec::builder()
4098 .trader_id(spawned_order.trader_id())
4099 .strategy_id(spawned_order.strategy_id())
4100 .instrument_id(spawned_order.instrument_id())
4101 .client_order_id(spawned_order.client_order_id())
4102 .account_id(AccountId::from("BINANCE-001"))
4103 .reason("TEST_REJECTION".into())
4104 .build();
4105
4106 {
4107 let cache_rc = algo.core.cache_rc();
4108 let mut cache = cache_rc.borrow_mut();
4109 cache
4110 .update_order(&OrderEventAny::Rejected(rejected))
4111 .unwrap();
4112 }
4113
4114 algo.handle_order_event(OrderEventAny::Rejected(rejected));
4115
4116 let restored_primary = algo.cache().order(&client_order_id).unwrap();
4117 assert_eq!(restored_primary.quantity(), Quantity::from("1.0"));
4118 }
4119
4120 #[rstest]
4121 fn test_spawned_order_with_reduce_primary_false_does_not_restore() {
4122 let mut algo = create_test_algorithm();
4123 register_algorithm(&mut algo);
4124
4125 let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
4126 let exec_algorithm_id = algo.id();
4127 let client_order_id = ClientOrderId::from("O-001");
4128
4129 let mut primary = OrderAny::Market(MarketOrder::new(
4130 TraderId::from("TRADER-001"),
4131 StrategyId::from("STRAT-001"),
4132 instrument_id,
4133 client_order_id,
4134 OrderSide::Buy,
4135 Quantity::from("1.0"),
4136 TimeInForce::Gtc,
4137 UUID4::new(),
4138 0.into(),
4139 false,
4140 false,
4141 None,
4142 None,
4143 None,
4144 None,
4145 Some(exec_algorithm_id),
4146 None,
4147 Some(client_order_id),
4148 None,
4149 ));
4150
4151 {
4152 let cache_rc = algo.core.cache_rc();
4153 let mut cache = cache_rc.borrow_mut();
4154 cache.add_order(primary.clone(), None, None, false).unwrap();
4155 }
4156
4157 let spawned = algo.spawn_market(
4158 &mut primary,
4159 Quantity::from("0.5"),
4160 TimeInForce::Fok,
4161 false,
4162 None,
4163 false,
4164 );
4165
4166 assert_eq!(primary.quantity(), Quantity::from("1.0"));
4167
4168 let spawned_order = OrderAny::Market(spawned);
4169 {
4170 let cache_rc = algo.core.cache_rc();
4171 let mut cache = cache_rc.borrow_mut();
4172 cache
4173 .add_order(spawned_order.clone(), None, None, false)
4174 .unwrap();
4175 }
4176
4177 let denied = OrderDeniedSpec::builder()
4178 .trader_id(spawned_order.trader_id())
4179 .strategy_id(spawned_order.strategy_id())
4180 .instrument_id(spawned_order.instrument_id())
4181 .client_order_id(spawned_order.client_order_id())
4182 .reason("TEST_DENIAL".into())
4183 .build();
4184
4185 {
4186 let cache_rc = algo.core.cache_rc();
4187 let mut cache = cache_rc.borrow_mut();
4188 cache.update_order(&OrderEventAny::Denied(denied)).unwrap();
4189 }
4190
4191 algo.handle_order_event(OrderEventAny::Denied(denied));
4192
4193 let final_primary = algo.cache().order(&client_order_id).unwrap();
4194 assert_eq!(final_primary.quantity(), Quantity::from("1.0"));
4195 }
4196
4197 #[rstest]
4198 fn test_multiple_spawns_with_one_denied_restores_correctly() {
4199 let mut algo = create_test_algorithm();
4200 register_algorithm(&mut algo);
4201
4202 let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
4203 let exec_algorithm_id = algo.id();
4204 let client_order_id = ClientOrderId::from("O-001");
4205
4206 let mut primary = OrderAny::Market(MarketOrder::new(
4207 TraderId::from("TRADER-001"),
4208 StrategyId::from("STRAT-001"),
4209 instrument_id,
4210 client_order_id,
4211 OrderSide::Buy,
4212 Quantity::from("1.0"),
4213 TimeInForce::Gtc,
4214 UUID4::new(),
4215 0.into(),
4216 false,
4217 false,
4218 None,
4219 None,
4220 None,
4221 None,
4222 Some(exec_algorithm_id),
4223 None,
4224 Some(client_order_id),
4225 None,
4226 ));
4227
4228 {
4229 let cache_rc = algo.core.cache_rc();
4230 let mut cache = cache_rc.borrow_mut();
4231 cache.add_order(primary.clone(), None, None, false).unwrap();
4232 }
4233
4234 let spawned1 = algo.spawn_market(
4235 &mut primary,
4236 Quantity::from("0.3"),
4237 TimeInForce::Fok,
4238 false,
4239 None,
4240 true,
4241 );
4242 let spawned2 = algo.spawn_market(
4243 &mut primary,
4244 Quantity::from("0.4"),
4245 TimeInForce::Fok,
4246 false,
4247 None,
4248 true,
4249 );
4250 assert_eq!(primary.quantity(), Quantity::from("0.3"));
4251
4252 let spawned_order1 = OrderAny::Market(spawned1);
4253 let spawned_order2 = OrderAny::Market(spawned2);
4254 {
4255 let cache_rc = algo.core.cache_rc();
4256 let mut cache = cache_rc.borrow_mut();
4257 cache.add_order(spawned_order1, None, None, false).unwrap();
4258 cache
4259 .add_order(spawned_order2.clone(), None, None, false)
4260 .unwrap();
4261 }
4262
4263 let denied = OrderDeniedSpec::builder()
4264 .trader_id(spawned_order2.trader_id())
4265 .strategy_id(spawned_order2.strategy_id())
4266 .instrument_id(spawned_order2.instrument_id())
4267 .client_order_id(spawned_order2.client_order_id())
4268 .reason("TEST_DENIAL".into())
4269 .build();
4270
4271 {
4272 let cache_rc = algo.core.cache_rc();
4273 let mut cache = cache_rc.borrow_mut();
4274 cache.update_order(&OrderEventAny::Denied(denied)).unwrap();
4275 }
4276
4277 let (handler, events) = subscribe_order_topic(spawned_order2.strategy_id());
4278
4279 algo.handle_order_event(OrderEventAny::Denied(denied));
4280
4281 msgbus::unsubscribe_order_events(
4282 format!("events.order.{}", spawned_order2.strategy_id()).into(),
4283 &handler,
4284 );
4285 let events = events.borrow();
4286
4287 let restored_primary = algo.cache().order(&client_order_id).unwrap();
4288 assert_eq!(restored_primary.quantity(), Quantity::from("0.7"));
4289 assert_eq!(events.len(), 1);
4290 assert!(matches!(
4291 &events[0],
4292 OrderEventAny::Updated(event) if event.quantity == Quantity::from("0.7")
4293 ));
4294 }
4295
4296 #[rstest]
4297 fn test_spawned_order_accepted_then_canceled_restores_reduction() {
4298 let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
4299
4300 let primary_after_accept = algo.cache().order(&client_order_id).unwrap();
4301 assert_eq!(primary_after_accept.quantity(), Quantity::from("0.5"));
4302
4303 cancel_spawned_order(&mut algo, &mut spawned_order);
4305
4306 let final_primary = algo.cache().order(&client_order_id).unwrap();
4307 assert_eq!(final_primary.quantity(), Quantity::from("1.0"));
4308 }
4309
4310 #[rstest]
4311 fn test_spawned_order_canceled_after_primary_submission_does_not_restore_reduction() {
4312 let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
4313 let mut primary = algo.cache().order(&client_order_id).unwrap();
4314 submit_order_in_cache(&mut algo, &mut primary);
4315
4316 cancel_spawned_order(&mut algo, &mut spawned_order);
4317
4318 let mut submitted_primary = algo.cache().order(&client_order_id).unwrap();
4319 assert_eq!(submitted_primary.quantity(), Quantity::from("0.5"));
4320 assert!(
4321 algo.core
4322 .take_pending_spawn_reduction(spawned_order.client_order_id())
4323 .is_none()
4324 );
4325 fill_spawned_order(&mut algo, &mut submitted_primary, Quantity::from("0.5"));
4326 assert!(submitted_primary.is_closed());
4327 assert_eq!(submitted_primary.status(), OrderStatus::Filled);
4328 }
4329
4330 #[rstest]
4331 fn test_spawned_order_canceled_during_primary_submission_handoff_does_not_restore() {
4332 let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
4333 let primary = algo.cache().order(&client_order_id).unwrap();
4334 algo.core
4335 .add_spawn_fill_debt(client_order_id, Quantity::from("0.1"));
4336 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4337 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4338 msgbus::register_trading_command_endpoint(
4339 MessagingSwitchboard::risk_engine_queue_execute(),
4340 risk_handler,
4341 );
4342 let strategy_id = primary.strategy_id();
4343 let (event_handler, events) = subscribe_order_topic(strategy_id);
4344
4345 algo.submit_order(primary, None, None).unwrap();
4346 assert_eq!(
4347 algo.cache().order(&client_order_id).unwrap().status(),
4348 OrderStatus::Initialized,
4349 );
4350 cancel_spawned_order(&mut algo, &mut spawned_order);
4351
4352 msgbus::unsubscribe_order_events(
4353 format!("events.order.{strategy_id}").into(),
4354 &event_handler,
4355 );
4356 let risk_messages = risk_messages.get_messages();
4357 let [TradingCommand::SubmitOrder(command)] = risk_messages.as_slice() else {
4358 panic!("Expected exactly one SubmitOrder command");
4359 };
4360 assert_eq!(command.client_order_id, client_order_id);
4361 assert_eq!(command.order_init.quantity, Quantity::from("1.0"));
4365 assert_eq!(
4366 algo.cache().order(&client_order_id).unwrap().quantity(),
4367 Quantity::from("0.5"),
4368 );
4369 assert!(!events.borrow().iter().any(|event| matches!(
4370 event,
4371 OrderEventAny::Updated(updated) if updated.client_order_id == client_order_id
4372 )));
4373 assert!(
4374 algo.core
4375 .take_pending_spawn_reduction(spawned_order.client_order_id())
4376 .is_none()
4377 );
4378 assert!(algo.core.spawn_fill_debt(client_order_id).is_none());
4379 }
4380
4381 #[rstest]
4382 fn test_late_spawn_fill_rededucts_restored_primary_before_final_submission() {
4383 let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
4384 cancel_spawned_order(&mut algo, &mut spawned_order);
4385 assert_eq!(
4386 algo.cache().order(&client_order_id).unwrap().quantity(),
4387 Quantity::from("1.0"),
4388 );
4389
4390 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.5"));
4391
4392 let mut primary = algo.cache().order(&client_order_id).unwrap();
4393 assert_eq!(primary.quantity(), Quantity::from("0.5"));
4394 submit_order_in_cache(&mut algo, &mut primary);
4395 assert_eq!(
4396 spawned_order.filled_qty() + primary.quantity(),
4397 Quantity::from("1.0"),
4398 );
4399 }
4400
4401 #[rstest]
4402 fn test_late_spawn_fill_during_primary_submission_handoff_does_not_rededuct() {
4403 let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
4404 cancel_spawned_order(&mut algo, &mut spawned_order);
4405 let primary = algo.cache().order(&client_order_id).unwrap();
4406 algo.core
4407 .add_spawn_fill_debt(client_order_id, Quantity::from("0.1"));
4408 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4409 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4410 msgbus::register_trading_command_endpoint(
4411 MessagingSwitchboard::risk_engine_queue_execute(),
4412 risk_handler,
4413 );
4414 let strategy_id = primary.strategy_id();
4415 let (event_handler, events) = subscribe_order_topic(strategy_id);
4416
4417 algo.submit_order(primary, None, None).unwrap();
4418 assert_eq!(
4419 algo.cache().order(&client_order_id).unwrap().status(),
4420 OrderStatus::Initialized,
4421 );
4422 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.5"));
4423
4424 msgbus::unsubscribe_order_events(
4425 format!("events.order.{strategy_id}").into(),
4426 &event_handler,
4427 );
4428 let risk_messages = risk_messages.get_messages();
4429 let [TradingCommand::SubmitOrder(command)] = risk_messages.as_slice() else {
4430 panic!("Expected exactly one SubmitOrder command");
4431 };
4432 assert_eq!(command.client_order_id, client_order_id);
4433 assert_eq!(command.order_init.quantity, Quantity::from("1.0"));
4436 assert_eq!(
4437 algo.cache().order(&client_order_id).unwrap().quantity(),
4438 Quantity::from("1.0"),
4439 );
4440 assert!(!events.borrow().iter().any(|event| matches!(
4441 event,
4442 OrderEventAny::Updated(updated) if updated.client_order_id == client_order_id
4443 )));
4444 assert!(
4445 algo.core
4446 .take_pending_spawn_reduction(spawned_order.client_order_id())
4447 .is_none()
4448 );
4449 assert!(algo.core.spawn_fill_debt(client_order_id).is_none());
4450 }
4451
4452 #[rstest]
4453 fn test_converted_quote_spawn_canceled_unfilled_restores_full_quote_quantity() {
4454 let (mut algo, client_order_id, mut spawned_order) =
4455 setup_accepted_quote_spawn(Quantity::from("100"), Quantity::from("50"));
4456 convert_spawn_to_base(&mut algo, &mut spawned_order, Quantity::from("5"));
4457
4458 cancel_spawned_order(&mut algo, &mut spawned_order);
4459
4460 assert_eq!(
4461 algo.cache().order(&client_order_id).unwrap().quantity(),
4462 Quantity::from("100"),
4463 );
4464 assert!(
4465 algo.cache()
4466 .order(&client_order_id)
4467 .unwrap()
4468 .is_quote_quantity()
4469 );
4470 assert_eq!(
4471 algo.core
4472 .spawn_reduction(spawned_order.client_order_id())
4473 .unwrap()
4474 .restored_qty
4475 .unwrap(),
4476 Quantity::from("50"),
4477 );
4478 }
4479
4480 #[rstest]
4481 fn test_converted_quote_spawn_partial_fill_restores_proportional_quote_quantity() {
4482 let (mut algo, client_order_id, mut spawned_order) =
4483 setup_accepted_quote_spawn(Quantity::from("100"), Quantity::from("50"));
4484 convert_spawn_to_base(&mut algo, &mut spawned_order, Quantity::from("5"));
4485 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("2"));
4486
4487 cancel_spawned_order(&mut algo, &mut spawned_order);
4488
4489 assert_eq!(
4490 algo.cache().order(&client_order_id).unwrap().quantity(),
4491 Quantity::from("80"),
4492 );
4493 assert!(
4494 algo.cache()
4495 .order(&client_order_id)
4496 .unwrap()
4497 .is_quote_quantity()
4498 );
4499 assert_eq!(
4500 algo.core
4501 .spawn_reduction(spawned_order.client_order_id())
4502 .unwrap()
4503 .restored_qty
4504 .unwrap(),
4505 Quantity::from("30"),
4506 );
4507 }
4508
4509 #[rstest]
4510 fn test_converted_quote_spawn_late_fill_charges_proportional_quote_quantity() {
4511 let (mut algo, client_order_id, mut spawned_order) =
4512 setup_accepted_quote_spawn(Quantity::from("100"), Quantity::from("50"));
4513 convert_spawn_to_base(&mut algo, &mut spawned_order, Quantity::from("5"));
4514 cancel_spawned_order(&mut algo, &mut spawned_order);
4515
4516 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("1"));
4517
4518 assert_eq!(
4519 algo.cache().order(&client_order_id).unwrap().quantity(),
4520 Quantity::from("90"),
4521 );
4522 assert!(
4523 algo.cache()
4524 .order(&client_order_id)
4525 .unwrap()
4526 .is_quote_quantity()
4527 );
4528 assert_eq!(
4529 algo.core
4530 .spawn_reduction(spawned_order.client_order_id())
4531 .unwrap()
4532 .restored_qty
4533 .unwrap(),
4534 Quantity::from("40"),
4535 );
4536 }
4537
4538 #[rstest]
4539 fn test_converted_quote_spawn_restoration_rounds_down_to_primary_precision() {
4540 let (mut algo, client_order_id, mut spawned_order) =
4541 setup_accepted_quote_spawn(Quantity::from("20.00"), Quantity::from("10.00"));
4542 convert_spawn_to_base(&mut algo, &mut spawned_order, Quantity::from("3.000"));
4543 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("1.000"));
4544
4545 cancel_spawned_order(&mut algo, &mut spawned_order);
4546
4547 assert_eq!(
4548 algo.cache().order(&client_order_id).unwrap().quantity(),
4549 Quantity::from("16.66"),
4550 );
4551 assert!(
4552 algo.cache()
4553 .order(&client_order_id)
4554 .unwrap()
4555 .is_quote_quantity()
4556 );
4557 assert_eq!(
4558 algo.core
4559 .spawn_reduction(spawned_order.client_order_id())
4560 .unwrap()
4561 .restored_qty
4562 .unwrap(),
4563 Quantity::from("6.66"),
4564 );
4565 }
4566
4567 #[rstest]
4568 fn test_converted_quote_spawn_repeated_fractional_late_fills_conserve_budget() {
4569 let (mut algo, client_order_id, mut spawned_order) =
4570 setup_accepted_quote_spawn(Quantity::from("20.00"), Quantity::from("10.00"));
4571 convert_spawn_to_base(&mut algo, &mut spawned_order, Quantity::from("3.000"));
4572 cancel_spawned_order(&mut algo, &mut spawned_order);
4573
4574 for (primary_qty, restored_qty) in [
4575 (Quantity::from("16.66"), Quantity::from("6.66")),
4576 (Quantity::from("13.33"), Quantity::from("3.33")),
4577 (Quantity::from("10.00"), Quantity::from("0.00")),
4578 ] {
4579 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("1.000"));
4580 assert_eq!(
4581 algo.cache().order(&client_order_id).unwrap().quantity(),
4582 primary_qty,
4583 );
4584 assert_eq!(
4585 algo.core
4586 .spawn_reduction(spawned_order.client_order_id())
4587 .unwrap()
4588 .restored_qty
4589 .unwrap(),
4590 restored_qty,
4591 );
4592 }
4593 }
4594
4595 #[rstest]
4596 #[case::single_fill(Quantity::from("3.0"), 1)]
4597 #[case::split_fills(Quantity::from("0.1"), 30)]
4598 fn test_converted_quote_spawn_fill_partition_preserves_total(
4599 #[case] fill_qty: Quantity,
4600 #[case] fills: usize,
4601 ) {
4602 let (mut algo, primary_id, mut child) =
4603 setup_accepted_quote_spawn(Quantity::from("20"), Quantity::from("10"));
4604 convert_spawn_to_base(&mut algo, &mut child, Quantity::from("3.0"));
4605 cancel_spawned_order(&mut algo, &mut child);
4606
4607 for _ in 0..fills {
4608 fill_spawned_order(&mut algo, &mut child, fill_qty);
4609 }
4610
4611 assert_eq!(child.filled_qty(), Quantity::from("3.0"));
4612 assert_eq!(
4613 algo.cache().order(&primary_id).unwrap().quantity(),
4614 Quantity::from("10")
4615 );
4616 assert_eq!(
4617 algo.core
4618 .spawn_reduction(child.client_order_id())
4619 .unwrap()
4620 .restored_qty,
4621 Some(Quantity::from("0"))
4622 );
4623 assert!(algo.core.spawn_fill_debt(primary_id).is_none());
4624 }
4625
4626 #[rstest]
4627 fn test_increased_spawn_late_fill_debits_original_restoration() {
4628 let (mut algo, primary_id, mut child) = setup_pending_spawn();
4629 algo.modify_order_in_place(&mut child, Some(Quantity::from("0.8")), None, None)
4630 .unwrap();
4631 accept_spawned_order(&mut algo, &mut child);
4632 cancel_spawned_order(&mut algo, &mut child);
4633
4634 fill_spawned_order(&mut algo, &mut child, Quantity::from("0.1"));
4635
4636 assert_eq!(child.quantity(), Quantity::from("0.8"));
4637 assert_eq!(child.filled_qty(), Quantity::from("0.1"));
4638 assert_eq!(
4639 algo.cache().order(&primary_id).unwrap().quantity(),
4640 Quantity::from("0.9")
4641 );
4642 assert_eq!(
4643 algo.core
4644 .spawn_reduction(child.client_order_id())
4645 .unwrap()
4646 .restored_qty,
4647 Some(Quantity::from("0.4"))
4648 );
4649 assert!(algo.core.spawn_fill_debt(primary_id).is_none());
4650
4651 void_last_spawn_fill(&mut algo, &mut child, Quantity::from("0.1"));
4652
4653 assert_eq!(
4654 algo.cache().order(&primary_id).unwrap().quantity(),
4655 Quantity::from("1.0")
4656 );
4657 }
4658
4659 #[rstest]
4660 #[case::partial_fill(Quantity::from("0.1"))]
4661 #[case::full_fill(Quantity::from("0.5"))]
4662 fn test_voided_late_spawn_fill_restores_primary_quantity(#[case] fill_qty: Quantity) {
4663 let (mut algo, primary_id, mut child) = setup_accepted_spawn();
4664 cancel_spawned_order(&mut algo, &mut child);
4665 fill_spawned_order(&mut algo, &mut child, fill_qty);
4666
4667 void_last_spawn_fill(&mut algo, &mut child, fill_qty);
4668
4669 assert_eq!(child.filled_qty(), Quantity::from("0.0"));
4670 assert_eq!(
4671 algo.cache().order(&primary_id).unwrap().quantity(),
4672 Quantity::from("1.0")
4673 );
4674 assert!(algo.core.spawn_fill_debt(primary_id).is_none());
4675 assert_eq!(
4676 algo.core
4677 .spawn_reduction(child.client_order_id())
4678 .unwrap()
4679 .restored_qty,
4680 Some(Quantity::from("0.5"))
4681 );
4682 }
4683
4684 #[rstest]
4685 #[case::debt_only(
4686 Quantity::from("0.1"),
4687 Quantity::from("0.0"),
4688 Some(Quantity::from("0.1"))
4689 )]
4690 #[case::debt_and_quantity(Quantity::from("0.3"), Quantity::from("0.1"), None)]
4691 fn test_voided_late_spawn_fill_discharges_debt_before_restoring_quantity(
4692 #[case] voided_qty: Quantity,
4693 #[case] primary_qty: Quantity,
4694 #[case] debt_qty: Option<Quantity>,
4695 ) {
4696 let (mut algo, primary_id, mut child) = setup_accepted_spawn();
4697 cancel_spawned_order(&mut algo, &mut child);
4698 let _second = spawn_reduced_child(&mut algo, primary_id, Quantity::from("0.8"));
4699 fill_spawned_order(&mut algo, &mut child, Quantity::from("0.4"));
4700 assert_eq!(
4701 algo.core.spawn_fill_debt(primary_id),
4702 Some(Quantity::from("0.2"))
4703 );
4704
4705 void_last_spawn_fill(&mut algo, &mut child, voided_qty);
4706
4707 assert_eq!(
4708 algo.cache().order(&primary_id).unwrap().quantity(),
4709 primary_qty
4710 );
4711 assert_eq!(algo.core.spawn_fill_debt(primary_id), debt_qty);
4712 assert_eq!(child.filled_qty(), Quantity::from("0.4") - voided_qty);
4713 assert_eq!(
4714 algo.core
4715 .spawn_reduction(child.client_order_id())
4716 .unwrap()
4717 .restored_qty,
4718 Some(Quantity::from("0.1") + voided_qty)
4719 );
4720 }
4721
4722 #[rstest]
4723 fn test_spawn_fill_void_before_restoration_preserves_reserved_quantity() {
4724 let (mut algo, primary_id, mut child) = setup_accepted_spawn();
4725 fill_spawned_order(&mut algo, &mut child, Quantity::from("0.1"));
4726
4727 void_last_spawn_fill(&mut algo, &mut child, Quantity::from("0.1"));
4728
4729 assert_eq!(
4730 algo.cache().order(&primary_id).unwrap().quantity(),
4731 Quantity::from("0.5")
4732 );
4733 assert_eq!(
4734 algo.core
4735 .spawn_reduction(child.client_order_id())
4736 .unwrap()
4737 .restored_qty,
4738 None
4739 );
4740 cancel_spawned_order(&mut algo, &mut child);
4741 assert_eq!(
4742 algo.cache().order(&primary_id).unwrap().quantity(),
4743 Quantity::from("1.0")
4744 );
4745 }
4746
4747 #[rstest]
4748 fn test_spawn_fill_void_after_primary_handoff_preserves_submitted_quantity() {
4749 let (mut algo, primary_id, mut child) = setup_accepted_spawn();
4750 cancel_spawned_order(&mut algo, &mut child);
4751 fill_spawned_order(&mut algo, &mut child, Quantity::from("0.1"));
4752 let primary = algo.cache().order(&primary_id).unwrap();
4753 let (handler, _messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4754 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4755 msgbus::register_trading_command_endpoint(
4756 MessagingSwitchboard::risk_engine_queue_execute(),
4757 handler,
4758 );
4759 algo.submit_order(primary, None, None).unwrap();
4760
4761 void_last_spawn_fill(&mut algo, &mut child, Quantity::from("0.1"));
4762
4763 assert_eq!(
4764 algo.cache().order(&primary_id).unwrap().quantity(),
4765 Quantity::from("0.9")
4766 );
4767 assert_eq!(
4768 algo.cache().order(&primary_id).unwrap().status(),
4769 OrderStatus::Initialized
4770 );
4771 assert!(algo.core.primary_was_handed_off(primary_id));
4772 assert!(algo.core.spawn_reduction(child.client_order_id()).is_none());
4773 assert!(algo.core.spawn_fill_debt(primary_id).is_none());
4774 }
4775
4776 #[rstest]
4777 fn test_converted_quote_spawn_fill_void_restores_cumulative_budget_once() {
4778 let (mut algo, primary_id, mut child) =
4779 setup_accepted_quote_spawn(Quantity::from("20.00"), Quantity::from("10.00"));
4780 convert_spawn_to_base(&mut algo, &mut child, Quantity::from("3.000"));
4781 cancel_spawned_order(&mut algo, &mut child);
4782 fill_spawned_order(&mut algo, &mut child, Quantity::from("1.000"));
4783 fill_spawned_order(&mut algo, &mut child, Quantity::from("1.000"));
4784
4785 void_last_spawn_fill(&mut algo, &mut child, Quantity::from("0.500"));
4786 assert_eq!(
4787 algo.cache().order(&primary_id).unwrap().quantity(),
4788 Quantity::from("15.00")
4789 );
4790 void_last_spawn_fill(&mut algo, &mut child, Quantity::from("1.000"));
4791 let duplicate = child.events().into_iter().last().unwrap().clone();
4792 algo.handle_order_event(duplicate);
4793
4794 assert_eq!(child.filled_qty(), Quantity::from("1.000"));
4795 assert_eq!(
4796 algo.cache().order(&primary_id).unwrap().quantity(),
4797 Quantity::from("16.66")
4798 );
4799 assert_eq!(
4800 algo.core
4801 .spawn_reduction(child.client_order_id())
4802 .unwrap()
4803 .restored_qty,
4804 Some(Quantity::from("6.66"))
4805 );
4806 assert!(algo.core.spawn_fill_debt(primary_id).is_none());
4807 }
4808
4809 fn void_last_spawn_fill(algo: &mut TestAlgorithm, order: &mut OrderAny, quantity: Quantity) {
4810 let fill = order
4811 .events()
4812 .into_iter()
4813 .rev()
4814 .find_map(|event| match event {
4815 OrderEventAny::Filled(fill) => Some(fill.clone()),
4816 _ => None,
4817 })
4818 .unwrap();
4819 let voided = OrderFillVoidedSpec::builder()
4820 .trader_id(fill.trader_id)
4821 .strategy_id(fill.strategy_id)
4822 .instrument_id(fill.instrument_id)
4823 .client_order_id(fill.client_order_id)
4824 .venue_order_id(fill.venue_order_id)
4825 .account_id(fill.account_id)
4826 .trade_id(fill.trade_id)
4827 .voided_qty(quantity)
4828 .order_side(fill.order_side)
4829 .order_type(fill.order_type)
4830 .last_px(fill.last_px)
4831 .currency(fill.currency)
4832 .liquidity_side(fill.liquidity_side)
4833 .build();
4834 *order = algo
4835 .core
4836 .cache_rc()
4837 .borrow_mut()
4838 .update_order(&OrderEventAny::FillVoided(voided.clone()))
4839 .unwrap();
4840 algo.handle_order_event(OrderEventAny::FillVoided(voided));
4841 }
4842
4843 #[rstest]
4844 fn test_primary_handoff_clears_spawn_accounting_without_child_events() {
4845 let (mut algo, primary_id, mut child) = setup_accepted_spawn();
4846 cancel_spawned_order(&mut algo, &mut child);
4847 let primary = algo.cache().order(&primary_id).unwrap();
4848 let (handler, _messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4849 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4850 msgbus::register_trading_command_endpoint(
4851 MessagingSwitchboard::risk_engine_queue_execute(),
4852 handler,
4853 );
4854
4855 algo.submit_order(primary, None, None).unwrap();
4856
4857 assert!(algo.core.spawn_reduction(child.client_order_id()).is_none());
4858 assert!(algo.core.spawn_fill_debt(primary_id).is_none());
4859 assert!(algo.core.primary_was_handed_off(primary_id));
4860
4861 let mut primary = algo.cache().order(&primary_id).unwrap();
4862 submit_order_in_cache(&mut algo, &mut primary);
4863 assert!(!algo.core.primary_was_handed_off(primary_id));
4864 }
4865
4866 #[rstest]
4867 fn test_primary_cancellation_clears_spawn_accounting() {
4868 let (mut algo, primary_id, mut child) = setup_accepted_spawn();
4869 cancel_spawned_order(&mut algo, &mut child);
4870 let second = spawn_reduced_child(&mut algo, primary_id, Quantity::from("0.8"));
4871 fill_spawned_order(&mut algo, &mut child, Quantity::from("0.4"));
4872 let mut primary = algo.cache().order(&primary_id).unwrap();
4873
4874 cancel_spawned_order(&mut algo, &mut primary);
4875
4876 assert_eq!(primary.status(), OrderStatus::Canceled);
4877 assert!(algo.core.spawn_reduction(child.client_order_id()).is_none());
4878 assert!(
4879 algo.core
4880 .spawn_reduction(second.client_order_id())
4881 .is_none()
4882 );
4883 assert!(algo.core.spawn_fill_debt(primary_id).is_none());
4884 assert!(!algo.core.primary_was_handed_off(primary_id));
4885 }
4886
4887 #[rstest]
4888 fn test_converted_quote_spawn_partial_cancel_then_late_fill_charges_restored_budget() {
4889 let (mut algo, client_order_id, mut spawned_order) =
4890 setup_accepted_quote_spawn(Quantity::from("100"), Quantity::from("50"));
4891 convert_spawn_to_base(&mut algo, &mut spawned_order, Quantity::from("5"));
4892 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("2"));
4893 cancel_spawned_order(&mut algo, &mut spawned_order);
4894
4895 assert_eq!(
4896 algo.cache().order(&client_order_id).unwrap().quantity(),
4897 Quantity::from("80"),
4898 );
4899 assert_eq!(
4900 algo.core
4901 .spawn_reduction(spawned_order.client_order_id())
4902 .unwrap()
4903 .restored_qty
4904 .unwrap(),
4905 Quantity::from("30"),
4906 );
4907
4908 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("1"));
4909
4910 assert_eq!(
4911 algo.cache().order(&client_order_id).unwrap().quantity(),
4912 Quantity::from("70"),
4913 );
4914 assert_eq!(
4915 algo.core
4916 .spawn_reduction(spawned_order.client_order_id())
4917 .unwrap()
4918 .restored_qty
4919 .unwrap(),
4920 Quantity::from("20"),
4921 );
4922 }
4923
4924 #[rstest]
4925 fn test_unmarked_submitted_primary_cancellation_discards_reduction_and_debt() {
4926 let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
4927 let mut primary = algo.cache().order(&client_order_id).unwrap();
4928 algo.core
4929 .add_spawn_fill_debt(client_order_id, Quantity::from("0.1"));
4930
4931 submit_order_in_cache(&mut algo, &mut primary);
4932 assert!(algo.core.spawn_fill_debt(client_order_id).is_none());
4933 cancel_spawned_order(&mut algo, &mut spawned_order);
4934
4935 assert_eq!(
4936 algo.cache().order(&client_order_id).unwrap().quantity(),
4937 Quantity::from("0.5"),
4938 );
4939 assert!(
4940 algo.core
4941 .spawn_reduction(spawned_order.client_order_id())
4942 .is_none()
4943 );
4944 assert!(algo.core.spawn_fill_debt(client_order_id).is_none());
4945 }
4946
4947 #[rstest]
4948 #[case::denied(true)]
4949 #[case::rejected(false)]
4950 fn test_spawn_refusal_does_not_restore_submitted_primary(#[case] denied: bool) {
4951 let (mut algo, client_order_id, spawned_order) = setup_pending_spawn();
4952 let spawned_id = spawned_order.client_order_id();
4953 let mut primary = algo.cache().order(&client_order_id).unwrap();
4954 submit_order_in_cache(&mut algo, &mut primary);
4955
4956 if denied {
4957 let event = OrderDeniedSpec::builder()
4958 .trader_id(spawned_order.trader_id())
4959 .strategy_id(spawned_order.strategy_id())
4960 .instrument_id(spawned_order.instrument_id())
4961 .client_order_id(spawned_id)
4962 .reason("TEST_DENIAL".into())
4963 .build();
4964 algo.handle_order_event(OrderEventAny::Denied(event));
4965 } else {
4966 let event = OrderRejectedSpec::builder()
4967 .trader_id(spawned_order.trader_id())
4968 .strategy_id(spawned_order.strategy_id())
4969 .instrument_id(spawned_order.instrument_id())
4970 .client_order_id(spawned_id)
4971 .account_id(AccountId::from("BINANCE-001"))
4972 .reason("TEST_REJECTION".into())
4973 .build();
4974 algo.handle_order_event(OrderEventAny::Rejected(event));
4975 }
4976
4977 assert_eq!(
4978 algo.cache().order(&client_order_id).unwrap().quantity(),
4979 Quantity::from("0.5"),
4980 );
4981 assert!(algo.core.take_pending_spawn_reduction(spawned_id).is_none());
4982 }
4983
4984 #[rstest]
4985 fn test_multiple_late_partial_fills_net_only_the_restored_quantity() {
4986 let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
4987 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.2"));
4988 cancel_spawned_order(&mut algo, &mut spawned_order);
4989
4990 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.1"));
4991 assert_eq!(
4992 algo.cache().order(&client_order_id).unwrap().quantity(),
4993 Quantity::from("0.7"),
4994 );
4995
4996 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.2"));
4997 assert_eq!(
4998 algo.cache().order(&client_order_id).unwrap().quantity(),
4999 Quantity::from("0.5"),
5000 );
5001 assert_eq!(
5002 algo.core
5003 .spawn_reduction(spawned_order.client_order_id())
5004 .unwrap()
5005 .restored_qty,
5006 Some(Quantity::from("0.0")),
5007 );
5008 }
5009
5010 #[rstest]
5011 fn test_late_spawn_fill_after_restored_quantity_reused_caps_at_primary_quantity() {
5012 let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
5013 cancel_spawned_order(&mut algo, &mut spawned_order);
5014 assert_eq!(
5015 algo.cache().order(&client_order_id).unwrap().quantity(),
5016 Quantity::from("1.0"),
5017 );
5018
5019 let mut primary = algo.cache().order(&client_order_id).unwrap();
5021 let second_order = OrderAny::Market(algo.spawn_market(
5022 &mut primary,
5023 Quantity::from("0.8"),
5024 TimeInForce::Fok,
5025 false,
5026 None,
5027 true,
5028 ));
5029 algo.core
5030 .cache_rc()
5031 .borrow_mut()
5032 .add_order(second_order.clone(), None, None, false)
5033 .unwrap();
5034 assert_eq!(
5035 algo.cache().order(&client_order_id).unwrap().quantity(),
5036 Quantity::from("0.2"),
5037 );
5038
5039 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.5"));
5042 assert_eq!(
5043 algo.cache().order(&client_order_id).unwrap().quantity(),
5044 Quantity::from("0.0"),
5045 );
5046 assert_eq!(
5047 algo.core
5048 .spawn_reduction(spawned_order.client_order_id())
5049 .unwrap()
5050 .restored_qty,
5051 Some(Quantity::from("0.0")),
5052 );
5053 assert_eq!(
5054 algo.core.spawn_fill_debt(client_order_id),
5055 Some(Quantity::from("0.3")),
5056 );
5057
5058 let rejected = OrderRejectedSpec::builder()
5061 .trader_id(second_order.trader_id())
5062 .strategy_id(second_order.strategy_id())
5063 .instrument_id(second_order.instrument_id())
5064 .client_order_id(second_order.client_order_id())
5065 .account_id(AccountId::from("BINANCE-001"))
5066 .reason("TEST_REJECTION".into())
5067 .build();
5068 algo.handle_order_event(OrderEventAny::Rejected(rejected));
5069
5070 let final_primary = algo.cache().order(&client_order_id).unwrap();
5071 assert_eq!(final_primary.quantity(), Quantity::from("0.5"));
5072 assert!(algo.core.spawn_fill_debt(client_order_id).is_none());
5073 assert_eq!(
5074 spawned_order.filled_qty() + final_primary.quantity(),
5075 Quantity::from("1.0"),
5076 );
5077 }
5078
5079 #[rstest]
5080 fn test_late_fill_on_child_that_fully_discharged_debt_recreates_debt() {
5081 let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
5082 cancel_spawned_order(&mut algo, &mut spawned_order);
5083
5084 let mut child_b = spawn_reduced_child(&mut algo, client_order_id, Quantity::from("0.3"));
5085 let mut child_c = spawn_reduced_child(&mut algo, client_order_id, Quantity::from("0.5"));
5086 accept_spawned_order(&mut algo, &mut child_b);
5087 accept_spawned_order(&mut algo, &mut child_c);
5088 assert_eq!(
5089 algo.cache().order(&client_order_id).unwrap().quantity(),
5090 Quantity::from("0.2"),
5091 );
5092
5093 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.5"));
5094 assert_eq!(
5095 algo.cache().order(&client_order_id).unwrap().quantity(),
5096 Quantity::from("0.0"),
5097 );
5098 assert_eq!(
5099 algo.core.spawn_fill_debt(client_order_id),
5100 Some(Quantity::from("0.3")),
5101 );
5102
5103 cancel_spawned_order(&mut algo, &mut child_b);
5106 assert_eq!(
5107 algo.cache().order(&client_order_id).unwrap().quantity(),
5108 Quantity::from("0.0"),
5109 );
5110 assert!(algo.core.spawn_fill_debt(client_order_id).is_none());
5111 assert_eq!(
5112 algo.core
5113 .spawn_reduction(child_b.client_order_id())
5114 .unwrap()
5115 .restored_qty
5116 .unwrap(),
5117 Quantity::from("0.3"),
5118 );
5119
5120 fill_spawned_order(&mut algo, &mut child_b, Quantity::from("0.2"));
5122 assert_eq!(
5123 algo.cache().order(&client_order_id).unwrap().quantity(),
5124 Quantity::from("0.0"),
5125 );
5126 assert_eq!(
5127 algo.core.spawn_fill_debt(client_order_id),
5128 Some(Quantity::from("0.2")),
5129 );
5130
5131 cancel_spawned_order(&mut algo, &mut child_c);
5132 let final_primary = algo.cache().order(&client_order_id).unwrap();
5133 assert_eq!(final_primary.quantity(), Quantity::from("0.3"));
5134 assert!(algo.core.spawn_fill_debt(client_order_id).is_none());
5135 assert_eq!(
5136 spawned_order.filled_qty() + child_b.filled_qty() + final_primary.quantity(),
5137 Quantity::from("1.0"),
5138 );
5139 }
5140
5141 #[rstest]
5142 fn test_late_fill_on_child_after_partial_debt_discharge_nets_from_primary() {
5143 let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
5144 cancel_spawned_order(&mut algo, &mut spawned_order);
5145
5146 let mut child_b = spawn_reduced_child(&mut algo, client_order_id, Quantity::from("0.8"));
5147 accept_spawned_order(&mut algo, &mut child_b);
5148 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.5"));
5149 assert_eq!(
5150 algo.core.spawn_fill_debt(client_order_id),
5151 Some(Quantity::from("0.3")),
5152 );
5153
5154 cancel_spawned_order(&mut algo, &mut child_b);
5157 assert_eq!(
5158 algo.cache().order(&client_order_id).unwrap().quantity(),
5159 Quantity::from("0.5"),
5160 );
5161 assert!(algo.core.spawn_fill_debt(client_order_id).is_none());
5162 assert_eq!(
5163 algo.core
5164 .spawn_reduction(child_b.client_order_id())
5165 .unwrap()
5166 .restored_qty
5167 .unwrap(),
5168 Quantity::from("0.8"),
5169 );
5170
5171 fill_spawned_order(&mut algo, &mut child_b, Quantity::from("0.3"));
5172 let final_primary = algo.cache().order(&client_order_id).unwrap();
5173 assert_eq!(final_primary.quantity(), Quantity::from("0.2"));
5174 assert_eq!(
5175 spawned_order.filled_qty() + child_b.filled_qty() + final_primary.quantity(),
5176 Quantity::from("1.0"),
5177 );
5178 }
5179
5180 #[rstest]
5181 fn test_emulated_spawn_refusal_restores_initialized_primary() {
5182 let (mut algo, client_order_id, _spawned_order) = setup_pending_spawn();
5183 let mut primary = algo.cache().order(&client_order_id).unwrap();
5184 let spawned = algo.spawn_limit(
5185 &mut primary,
5186 Quantity::from("0.3"),
5187 Price::from("50000.0"),
5188 TimeInForce::Gtc,
5189 None,
5190 false,
5191 false,
5192 None,
5193 Some(TriggerType::BidAsk),
5194 None,
5195 true,
5196 );
5197 assert_eq!(
5198 algo.cache().order(&client_order_id).unwrap().quantity(),
5199 Quantity::from("0.2"),
5200 );
5201
5202 let result = algo.submit_order(OrderAny::Limit(spawned), None, None);
5203
5204 assert!(result.is_err());
5205 assert_eq!(
5206 algo.cache().order(&client_order_id).unwrap().quantity(),
5207 Quantity::from("0.5"),
5208 );
5209 }
5210
5211 #[rstest]
5212 fn test_spawned_order_accepted_then_expired_restores_reduction() {
5213 let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
5214
5215 expire_spawned_order(&mut algo, &mut spawned_order);
5216
5217 let final_primary = algo.cache().order(&client_order_id).unwrap();
5218 assert_eq!(final_primary.quantity(), Quantity::from("1.0"));
5219 }
5220
5221 #[rstest]
5222 fn test_partially_filled_spawned_order_canceled_restores_leaves_quantity() {
5223 let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
5224 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.2"));
5225
5226 cancel_spawned_order(&mut algo, &mut spawned_order);
5227
5228 let final_primary = algo.cache().order(&client_order_id).unwrap();
5229 assert_eq!(final_primary.quantity(), Quantity::from("0.8"));
5230 }
5231
5232 #[rstest]
5233 fn test_partially_filled_spawned_order_expired_restores_leaves_quantity() {
5234 let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
5235 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.2"));
5236
5237 expire_spawned_order(&mut algo, &mut spawned_order);
5238
5239 let final_primary = algo.cache().order(&client_order_id).unwrap();
5240 assert_eq!(final_primary.quantity(), Quantity::from("0.8"));
5241 }
5242
5243 #[rstest]
5244 fn test_fully_filled_spawned_order_consumes_reduction_without_restoration() {
5245 let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
5246 let spawned_id = spawned_order.client_order_id();
5247
5248 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.5"));
5249
5250 let final_primary = algo.cache().order(&client_order_id).unwrap();
5251 assert_eq!(final_primary.quantity(), Quantity::from("0.5"));
5252 assert!(algo.core.take_pending_spawn_reduction(spawned_id).is_none());
5253 }
5254
5255 #[rstest]
5256 #[case::denied(true)]
5257 #[case::rejected(false)]
5258 fn test_spawned_order_refusal_then_terminal_event_restores_only_once(#[case] denied: bool) {
5259 let (mut algo, client_order_id, spawned_order) = setup_pending_spawn();
5264
5265 if denied {
5266 let event = OrderDeniedSpec::builder()
5267 .trader_id(spawned_order.trader_id())
5268 .strategy_id(spawned_order.strategy_id())
5269 .instrument_id(spawned_order.instrument_id())
5270 .client_order_id(spawned_order.client_order_id())
5271 .reason("TEST_DENIAL".into())
5272 .build();
5273 algo.core
5274 .cache_rc()
5275 .borrow_mut()
5276 .update_order(&OrderEventAny::Denied(event))
5277 .unwrap();
5278 algo.handle_order_event(OrderEventAny::Denied(event));
5279 } else {
5280 let event = OrderRejectedSpec::builder()
5281 .trader_id(spawned_order.trader_id())
5282 .strategy_id(spawned_order.strategy_id())
5283 .instrument_id(spawned_order.instrument_id())
5284 .client_order_id(spawned_order.client_order_id())
5285 .account_id(AccountId::from("BINANCE-001"))
5286 .reason("TEST_REJECTION".into())
5287 .build();
5288 algo.core
5289 .cache_rc()
5290 .borrow_mut()
5291 .update_order(&OrderEventAny::Rejected(event))
5292 .unwrap();
5293 algo.handle_order_event(OrderEventAny::Rejected(event));
5294 }
5295
5296 assert_eq!(
5297 algo.cache().order(&client_order_id).unwrap().quantity(),
5298 Quantity::from("1.0"),
5299 );
5300
5301 let canceled = OrderCanceledSpec::builder()
5302 .trader_id(spawned_order.trader_id())
5303 .strategy_id(spawned_order.strategy_id())
5304 .instrument_id(spawned_order.instrument_id())
5305 .client_order_id(spawned_order.client_order_id())
5306 .build();
5307 algo.handle_order_event(OrderEventAny::Canceled(canceled));
5308
5309 let final_primary = algo.cache().order(&client_order_id).unwrap();
5310 assert_eq!(final_primary.quantity(), Quantity::from("1.0"));
5311 }
5312
5313 #[rstest]
5314 #[should_panic(expected = "exceeds primary leaves_qty")]
5315 fn test_spawn_quantity_exceeds_leaves_qty_panics() {
5316 let mut algo = create_test_algorithm();
5317 register_algorithm(&mut algo);
5318
5319 let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
5320 let exec_algorithm_id = algo.id();
5321 let client_order_id = ClientOrderId::from("O-001");
5322
5323 let mut primary = OrderAny::Market(MarketOrder::new(
5324 TraderId::from("TRADER-001"),
5325 StrategyId::from("STRAT-001"),
5326 instrument_id,
5327 client_order_id,
5328 OrderSide::Buy,
5329 Quantity::from("1.0"),
5330 TimeInForce::Gtc,
5331 UUID4::new(),
5332 0.into(),
5333 false,
5334 false,
5335 None,
5336 None,
5337 None,
5338 None,
5339 Some(exec_algorithm_id),
5340 None,
5341 Some(client_order_id),
5342 None,
5343 ));
5344
5345 {
5346 let cache_rc = algo.core.cache_rc();
5347 let mut cache = cache_rc.borrow_mut();
5348 cache.add_order(primary.clone(), None, None, false).unwrap();
5349 }
5350
5351 let _ = algo.spawn_market(
5352 &mut primary,
5353 Quantity::from("0.8"),
5354 TimeInForce::Fok,
5355 false,
5356 None,
5357 true,
5358 );
5359
5360 assert_eq!(primary.quantity(), Quantity::from("0.2"));
5361 assert_eq!(primary.leaves_qty(), Quantity::from("0.2"));
5362
5363 let _ = algo.spawn_market(
5365 &mut primary,
5366 Quantity::from("0.5"),
5367 TimeInForce::Fok,
5368 false,
5369 None,
5370 true,
5371 );
5372 }
5373}