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(
1094 &mut self,
1095 order: &mut OrderAny,
1096 quantity: Option<Quantity>,
1097 price: Option<Price>,
1098 trigger_price: Option<Price>,
1099 client_id: Option<ClientId>,
1100 ) -> anyhow::Result<()>
1101 where
1102 Self: ExecutionAlgorithmNative,
1103 {
1104 let qty_changing = quantity.is_some_and(|q| q != order.quantity());
1105 let price_changing = price.is_some() && price != order.price();
1106 let trigger_changing = trigger_price.is_some() && trigger_price != order.trigger_price();
1107
1108 if !qty_changing && !price_changing && !trigger_changing {
1109 log::error!(
1110 "Cannot create command ModifyOrder: \
1111 quantity, price, and trigger were either None \
1112 or the same as existing values"
1113 );
1114 return Ok(());
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::TestClock,
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(TestClock::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 mut params = nautilus_core::Params::new();
2464 params.insert(
2465 "route".to_string(),
2466 serde_json::Value::String("A".to_string()),
2467 );
2468 algo.core
2469 .remember_submit_params(order.client_order_id(), Some(params));
2470
2471 let error = algo
2472 .deny_order(
2473 &order,
2474 Ustr::from("VALIDATION_FAILED: invalid execution schedule"),
2475 )
2476 .unwrap_err();
2477
2478 msgbus::unsubscribe_order_events(format!("events.order.{strategy_id}").into(), &handler);
2479 let cached_order = algo.cache().order(&order.client_order_id()).unwrap();
2480
2481 assert!(matches!(
2482 error.downcast_ref::<OrderError>(),
2483 Some(OrderError::InvalidStateTransition)
2484 ));
2485 assert_eq!(cached_order.status(), OrderStatus::Accepted);
2486 assert!(events.borrow().is_empty());
2487 assert!(algo.core.submit_params(&order.client_order_id()).is_some());
2489 }
2490
2491 #[rstest]
2492 fn test_algorithm_deny_order_removes_submit_params() {
2493 let mut algo = create_test_algorithm();
2494 register_algorithm(&mut algo);
2495
2496 let strategy_id = StrategyId::from("STRAT-ALGO-DENY-PARAMS");
2497 let order = OrderTestBuilder::new(OrderType::Market)
2498 .strategy_id(strategy_id)
2499 .instrument_id(InstrumentId::from("BTC/USDT.BINANCE"))
2500 .client_order_id(ClientOrderId::from("O-ALGO-DENY-PARAMS"))
2501 .quantity(Quantity::from("1.0"))
2502 .build();
2503 {
2504 let cache_rc = algo.core.cache_rc();
2505 cache_rc
2506 .borrow_mut()
2507 .add_order(order.clone(), None, None, false)
2508 .unwrap();
2509 }
2510
2511 let mut params = nautilus_core::Params::new();
2512 params.insert(
2513 "route".to_string(),
2514 serde_json::Value::String("A".to_string()),
2515 );
2516 algo.core
2517 .remember_submit_params(order.client_order_id(), Some(params));
2518 assert!(algo.core.submit_params(&order.client_order_id()).is_some());
2519
2520 algo.deny_order(&order, Ustr::from("VALIDATION_FAILED: test"))
2521 .unwrap();
2522
2523 assert!(algo.core.submit_params(&order.client_order_id()).is_none());
2524 }
2525
2526 #[rstest]
2527 fn test_submit_order_errors_when_algorithm_not_registered() {
2528 let mut algo = create_test_algorithm();
2529 let order = OrderAny::Market(MarketOrder::new(
2530 TraderId::from("TRADER-001"),
2531 StrategyId::from("STRAT-001"),
2532 InstrumentId::from("BTC/USDT.BINANCE"),
2533 ClientOrderId::from("O-UNREGISTERED-001"),
2534 OrderSide::Buy,
2535 Quantity::from("1.0"),
2536 TimeInForce::Gtc,
2537 UUID4::new(),
2538 0.into(),
2539 false,
2540 false,
2541 None,
2542 None,
2543 None,
2544 None,
2545 None,
2546 None,
2547 None,
2548 None,
2549 ));
2550
2551 let err = algo
2552 .submit_order(order, None, None)
2553 .unwrap_err()
2554 .to_string();
2555
2556 assert_eq!(
2557 err,
2558 "ExecutionAlgorithm not registered: trader_id is not set"
2559 );
2560 }
2561
2562 #[rstest]
2563 fn test_required_account_id_errors_when_missing_for_algorithm_event() {
2564 let order = OrderAny::Market(MarketOrder::new(
2565 TraderId::from("TRADER-001"),
2566 StrategyId::from("STRAT-001"),
2567 InstrumentId::from("BTC/USDT.BINANCE"),
2568 ClientOrderId::from("O-NO-ACCOUNT-001"),
2569 OrderSide::Buy,
2570 Quantity::from("1.0"),
2571 TimeInForce::Gtc,
2572 UUID4::new(),
2573 0.into(),
2574 false,
2575 false,
2576 None,
2577 None,
2578 None,
2579 None,
2580 None,
2581 None,
2582 None,
2583 None,
2584 ));
2585
2586 let err = required_account_id(&order, "pending update")
2587 .unwrap_err()
2588 .to_string();
2589
2590 assert_eq!(
2591 err,
2592 "Cannot generate pending update event for O-NO-ACCOUNT-001: account_id is not set"
2593 );
2594 }
2595
2596 #[rstest]
2597 fn test_algorithm_id() {
2598 let algo = create_test_algorithm();
2599 assert!(algo.id().inner().starts_with("TEST-"));
2600 }
2601
2602 #[rstest]
2603 fn test_execution_algorithm_behavior_does_not_require_native_core_access() {
2604 fn assert_execution_algorithm<T: ExecutionAlgorithm + DataActor>() {}
2605
2606 assert_execution_algorithm::<CoreFreeExecutionAlgorithm>();
2607
2608 let mut algorithm = CoreFreeExecutionAlgorithm { orders_seen: 0 };
2609 let order = OrderTestBuilder::new(OrderType::Market)
2610 .instrument_id(InstrumentId::from("BTC/USDT.BINANCE"))
2611 .quantity(Quantity::from("1.0"))
2612 .build();
2613
2614 algorithm.on_order(order).unwrap();
2615
2616 assert_eq!(algorithm.orders_seen, 1);
2617 }
2618
2619 #[rstest]
2620 fn test_nautilus_execution_algorithm_macro_custom_field() {
2621 let exec_algorithm_id = ExecAlgorithmId::from("MACRO-001");
2622 let algorithm = MacroTestCustomField {
2623 inner: ExecutionAlgorithmCore::new(ExecutionAlgorithmConfig {
2624 exec_algorithm_id: Some(exec_algorithm_id),
2625 ..Default::default()
2626 }),
2627 };
2628
2629 assert_eq!(algorithm.id(), exec_algorithm_id);
2630 assert_eq!(algorithm.actor_id(), ActorId::from("MACRO-001"));
2631 }
2632
2633 #[rstest]
2634 fn test_algorithm_spawn_market_creates_valid_order() {
2635 let mut algo = create_test_algorithm();
2636 register_algorithm(&mut algo);
2637
2638 let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
2639 let mut primary = OrderAny::Market(MarketOrder::new(
2640 TraderId::from("TRADER-001"),
2641 StrategyId::from("STRAT-001"),
2642 instrument_id,
2643 ClientOrderId::from("O-001"),
2644 OrderSide::Buy,
2645 Quantity::from("1.0"),
2646 TimeInForce::Gtc,
2647 UUID4::new(),
2648 0.into(),
2649 false, false, None, None, None, None, None, None, None, None, ));
2660
2661 let spawned = algo.spawn_market(
2662 &mut primary,
2663 Quantity::from("0.5"),
2664 TimeInForce::Ioc,
2665 false,
2666 None, false, );
2669
2670 assert_eq!(spawned.client_order_id.as_str(), "O-001-E1");
2671 assert_eq!(spawned.instrument_id, instrument_id);
2672 assert_eq!(spawned.order_side(), OrderSide::Buy);
2673 assert_eq!(spawned.quantity, Quantity::from("0.5"));
2674 assert_eq!(spawned.time_in_force, TimeInForce::Ioc);
2675 assert_eq!(spawned.exec_algorithm_id, Some(algo.id()));
2676 assert_eq!(spawned.exec_spawn_id, Some(ClientOrderId::from("O-001")));
2677 }
2678
2679 #[rstest]
2680 fn test_algorithm_spawn_increments_sequence() {
2681 let mut algo = create_test_algorithm();
2682 register_algorithm(&mut algo);
2683
2684 let mut primary = OrderAny::Market(MarketOrder::new(
2685 TraderId::from("TRADER-001"),
2686 StrategyId::from("STRAT-001"),
2687 InstrumentId::from("BTC/USDT.BINANCE"),
2688 ClientOrderId::from("O-001"),
2689 OrderSide::Buy,
2690 Quantity::from("1.0"),
2691 TimeInForce::Gtc,
2692 UUID4::new(),
2693 0.into(),
2694 false,
2695 false,
2696 None,
2697 None,
2698 None,
2699 None,
2700 None,
2701 None,
2702 None,
2703 None,
2704 ));
2705
2706 let spawned1 = algo.spawn_market(
2707 &mut primary,
2708 Quantity::from("0.25"),
2709 TimeInForce::Ioc,
2710 false,
2711 None,
2712 false,
2713 );
2714 let spawned2 = algo.spawn_market(
2715 &mut primary,
2716 Quantity::from("0.25"),
2717 TimeInForce::Ioc,
2718 false,
2719 None,
2720 false,
2721 );
2722 let spawned3 = algo.spawn_market(
2723 &mut primary,
2724 Quantity::from("0.25"),
2725 TimeInForce::Ioc,
2726 false,
2727 None,
2728 false,
2729 );
2730
2731 assert_eq!(spawned1.client_order_id.as_str(), "O-001-E1");
2732 assert_eq!(spawned2.client_order_id.as_str(), "O-001-E2");
2733 assert_eq!(spawned3.client_order_id.as_str(), "O-001-E3");
2734 }
2735
2736 #[rstest]
2737 fn test_algorithm_default_handlers_do_not_panic() {
2738 let mut algo = create_test_algorithm();
2739
2740 algo.on_order_initialized(OrderInitialized::default());
2741 algo.on_order_denied(OrderDenied::default());
2742 algo.on_order_emulated(OrderEmulated::default());
2743 algo.on_order_released(OrderReleased::default());
2744 algo.on_order_submitted(OrderSubmitted::default());
2745 algo.on_order_rejected(OrderRejected::default());
2746 algo.on_order_accepted(OrderAccepted::default());
2747 algo.on_algo_order_canceled(OrderCanceled::default());
2748 algo.on_order_expired(OrderExpired::default());
2749 algo.on_order_triggered(OrderTriggered::default());
2750 algo.on_order_pending_update(OrderPendingUpdate::default());
2751 algo.on_order_pending_cancel(OrderPendingCancel::default());
2752 algo.on_order_modify_rejected(OrderModifyRejected::default());
2753 algo.on_order_cancel_rejected(OrderCancelRejected::default());
2754 algo.on_order_updated(OrderUpdated::default());
2755 algo.on_algo_order_filled(OrderFilledSpec::builder().build());
2756 algo.on_order_fill_voided(&OrderFillVoidedSpec::builder().build());
2757 }
2758
2759 #[rstest]
2760 fn test_strategy_subscription_tracking() {
2761 let mut algo = create_test_algorithm();
2762 let strategy_id = StrategyId::from("STRAT-001");
2763
2764 assert!(!algo.core.is_strategy_subscribed(&strategy_id));
2765
2766 algo.subscribe_to_strategy_events(strategy_id);
2767 assert!(algo.core.is_strategy_subscribed(&strategy_id));
2768
2769 algo.subscribe_to_strategy_events(strategy_id);
2771 assert!(algo.core.is_strategy_subscribed(&strategy_id));
2772 }
2773
2774 #[rstest]
2775 fn test_algorithm_reset() {
2776 let mut algo = create_test_algorithm();
2777 let strategy_id = StrategyId::from("STRAT-001");
2778 let primary_id = ClientOrderId::new("O-001");
2779
2780 let _ = algo.core.spawn_client_order_id(&primary_id);
2781 algo.core.add_subscribed_strategy(strategy_id);
2782
2783 assert!(algo.core.spawn_sequence(&primary_id).is_some());
2784 assert!(algo.core.is_strategy_subscribed(&strategy_id));
2785
2786 ExecutionAlgorithm::on_reset(&mut algo).unwrap();
2787
2788 assert!(algo.core.spawn_sequence(&primary_id).is_none());
2789 assert!(!algo.core.is_strategy_subscribed(&strategy_id));
2790 }
2791
2792 #[rstest]
2793 fn test_algorithm_spawn_limit_creates_valid_order() {
2794 let mut algo = create_test_algorithm();
2795 register_algorithm(&mut algo);
2796
2797 let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
2798 let mut primary = OrderAny::Market(MarketOrder::new(
2799 TraderId::from("TRADER-001"),
2800 StrategyId::from("STRAT-001"),
2801 instrument_id,
2802 ClientOrderId::from("O-001"),
2803 OrderSide::Buy,
2804 Quantity::from("1.0"),
2805 TimeInForce::Gtc,
2806 UUID4::new(),
2807 0.into(),
2808 false,
2809 false,
2810 None,
2811 None,
2812 None,
2813 None,
2814 None,
2815 None,
2816 None,
2817 None,
2818 ));
2819
2820 let price = Price::from("50000.0");
2821 let spawned = algo.spawn_limit(
2822 &mut primary,
2823 Quantity::from("0.5"),
2824 price,
2825 TimeInForce::Gtc,
2826 None, false, false, None, None, None, false, );
2834
2835 assert_eq!(spawned.client_order_id.as_str(), "O-001-E1");
2836 assert_eq!(spawned.instrument_id, instrument_id);
2837 assert_eq!(spawned.order_side(), OrderSide::Buy);
2838 assert_eq!(spawned.quantity, Quantity::from("0.5"));
2839 assert_eq!(spawned.price, price);
2840 assert_eq!(spawned.time_in_force, TimeInForce::Gtc);
2841 assert_eq!(spawned.exec_algorithm_id, Some(algo.id()));
2842 assert_eq!(spawned.exec_spawn_id, Some(ClientOrderId::from("O-001")));
2843 }
2844
2845 #[rstest]
2846 fn test_algorithm_spawn_market_to_limit_creates_valid_order() {
2847 let mut algo = create_test_algorithm();
2848 register_algorithm(&mut algo);
2849
2850 let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
2851 let mut primary = OrderAny::Market(MarketOrder::new(
2852 TraderId::from("TRADER-001"),
2853 StrategyId::from("STRAT-001"),
2854 instrument_id,
2855 ClientOrderId::from("O-001"),
2856 OrderSide::Buy,
2857 Quantity::from("1.0"),
2858 TimeInForce::Gtc,
2859 UUID4::new(),
2860 0.into(),
2861 false,
2862 false,
2863 None,
2864 None,
2865 None,
2866 None,
2867 None,
2868 None,
2869 None,
2870 None,
2871 ));
2872
2873 let spawned = algo.spawn_market_to_limit(
2874 &mut primary,
2875 Quantity::from("0.5"),
2876 TimeInForce::Gtc,
2877 None, false, None, None, None, false, );
2884
2885 assert_eq!(spawned.client_order_id.as_str(), "O-001-E1");
2886 assert_eq!(spawned.instrument_id, instrument_id);
2887 assert_eq!(spawned.order_side(), OrderSide::Buy);
2888 assert_eq!(spawned.quantity, Quantity::from("0.5"));
2889 assert_eq!(spawned.time_in_force, TimeInForce::Gtc);
2890 assert_eq!(spawned.exec_algorithm_id, Some(algo.id()));
2891 assert_eq!(spawned.exec_spawn_id, Some(ClientOrderId::from("O-001")));
2892 }
2893
2894 #[rstest]
2895 fn test_algorithm_spawn_market_with_tags() {
2896 let mut algo = create_test_algorithm();
2897 register_algorithm(&mut algo);
2898
2899 let mut primary = OrderAny::Market(MarketOrder::new(
2900 TraderId::from("TRADER-001"),
2901 StrategyId::from("STRAT-001"),
2902 InstrumentId::from("BTC/USDT.BINANCE"),
2903 ClientOrderId::from("O-001"),
2904 OrderSide::Buy,
2905 Quantity::from("1.0"),
2906 TimeInForce::Gtc,
2907 UUID4::new(),
2908 0.into(),
2909 false,
2910 false,
2911 None,
2912 None,
2913 None,
2914 None,
2915 None,
2916 None,
2917 None,
2918 None,
2919 ));
2920
2921 let tags = vec![ustr::Ustr::from("TAG1"), ustr::Ustr::from("TAG2")];
2922 let spawned = algo.spawn_market(
2923 &mut primary,
2924 Quantity::from("0.5"),
2925 TimeInForce::Ioc,
2926 false,
2927 Some(tags.clone()),
2928 false,
2929 );
2930
2931 assert_eq!(spawned.tags, Some(tags));
2932 }
2933
2934 #[rstest]
2935 fn test_algorithm_spawn_propagates_primary_fields() {
2936 let mut algo = create_test_algorithm();
2937 register_algorithm(&mut algo);
2938
2939 let mut params = indexmap::IndexMap::new();
2940 params.insert(ustr::Ustr::from("horizon_secs"), ustr::Ustr::from("30"));
2941 params.insert(ustr::Ustr::from("interval_secs"), ustr::Ustr::from("10"));
2942 let primary_tags = vec![ustr::Ustr::from("PRIMARY_TAG")];
2943 let linked_order_ids = vec![ClientOrderId::from("LINK-1")];
2944 let client_order_id = ClientOrderId::from("O-001");
2945
2946 let mut primary = OrderAny::Market(MarketOrder::new(
2947 TraderId::from("TRADER-001"),
2948 StrategyId::from("STRAT-001"),
2949 InstrumentId::from("BTC/USDT.BINANCE"),
2950 client_order_id,
2951 OrderSide::Buy,
2952 Quantity::from("1.0"),
2953 TimeInForce::Gtc,
2954 UUID4::new(),
2955 0.into(),
2956 false, true, None, None, Some(linked_order_ids.clone()),
2961 None, Some(algo.id()),
2963 Some(params.clone()),
2964 Some(client_order_id),
2965 Some(primary_tags.clone()),
2966 ));
2967
2968 let spawned_market = algo.spawn_market(
2969 &mut primary,
2970 Quantity::from("0.25"),
2971 TimeInForce::Ioc,
2972 false,
2973 None, false,
2975 );
2976 assert!(spawned_market.is_quote_quantity);
2977 assert_eq!(spawned_market.exec_algorithm_params, Some(params.clone()));
2978 assert_eq!(spawned_market.tags, Some(primary_tags.clone()));
2979 assert_eq!(
2980 spawned_market.linked_order_ids,
2981 Some(linked_order_ids.clone())
2982 );
2983
2984 let spawned_limit = algo.spawn_limit(
2985 &mut primary,
2986 Quantity::from("0.25"),
2987 Price::from("50000.0"),
2988 TimeInForce::Gtc,
2989 None, false, false, None, None, None, false,
2996 );
2997 assert!(spawned_limit.is_quote_quantity);
2998 assert_eq!(spawned_limit.exec_algorithm_params, Some(params.clone()));
2999 assert_eq!(spawned_limit.tags, Some(primary_tags.clone()));
3000 assert_eq!(
3001 spawned_limit.linked_order_ids,
3002 Some(linked_order_ids.clone())
3003 );
3004
3005 let spawned_mtl = algo.spawn_market_to_limit(
3006 &mut primary,
3007 Quantity::from("0.25"),
3008 TimeInForce::Gtc,
3009 None, false, None, None, None, false,
3015 );
3016 assert!(spawned_mtl.is_quote_quantity);
3017 assert_eq!(spawned_mtl.exec_algorithm_params, Some(params));
3018 assert_eq!(spawned_mtl.tags, Some(primary_tags));
3019 assert_eq!(spawned_mtl.linked_order_ids, Some(linked_order_ids));
3020 }
3021
3022 #[rstest]
3023 fn test_muldiv_floor_u128_narrow_path_truncates() {
3024 assert_eq!(muldiv_floor_u128(50, 3, 5), 30);
3025 assert_eq!(muldiv_floor_u128(10, 2, 3), 6);
3026 assert_eq!(muldiv_floor_u128(u128::MAX, 7, 7), u128::MAX);
3027 }
3028
3029 #[rstest]
3030 fn test_muldiv_floor_u128_wide_path_floors_exactly() {
3031 let a = 10u128.pow(30);
3035 let b = 7 * 10u128.pow(30);
3036 let c = b + 1;
3037 assert!(a.checked_mul(b).is_none());
3038 assert_eq!(muldiv_floor_u128(a, b, c), a - 1);
3039 }
3040
3041 #[rstest]
3042 fn test_algorithm_reduce_primary_order() {
3043 let mut algo = create_test_algorithm();
3044 register_algorithm(&mut algo);
3045
3046 let order = OrderAny::Market(MarketOrder::new(
3047 TraderId::from("TRADER-001"),
3048 StrategyId::from("STRAT-001"),
3049 InstrumentId::from("BTC/USDT.BINANCE"),
3050 ClientOrderId::from("O-001"),
3051 OrderSide::Buy,
3052 Quantity::from("1.0"),
3053 TimeInForce::Gtc,
3054 UUID4::new(),
3055 0.into(),
3056 false,
3057 false,
3058 None,
3059 None,
3060 None,
3061 None,
3062 None,
3063 None,
3064 None,
3065 None,
3066 ));
3067
3068 let mut primary = TestOrderStubs::make_accepted_order(&order);
3070
3071 {
3072 let cache_rc = algo.core.cache_rc();
3073 let mut cache = cache_rc.borrow_mut();
3074 cache.add_order(primary.clone(), None, None, false).unwrap();
3075 }
3076
3077 let spawn_qty = Quantity::from("0.3");
3078 algo.reduce_primary_order(&mut primary, spawn_qty);
3079
3080 assert_eq!(primary.quantity(), Quantity::from("0.7"));
3081 }
3082
3083 #[rstest]
3084 fn test_algorithm_reduce_primary_order_publishes_updated_event() {
3085 let mut algo = create_test_algorithm();
3086 register_algorithm(&mut algo);
3087
3088 let strategy_id = StrategyId::from("STRAT-ALGO-REDUCE-PUBLISH");
3089 let order = OrderAny::Market(MarketOrder::new(
3090 TraderId::from("TRADER-001"),
3091 strategy_id,
3092 InstrumentId::from("BTC/USDT.BINANCE"),
3093 ClientOrderId::from("O-ALGO-REDUCE"),
3094 OrderSide::Buy,
3095 Quantity::from("1.0"),
3096 TimeInForce::Gtc,
3097 UUID4::new(),
3098 0.into(),
3099 false,
3100 false,
3101 None,
3102 None,
3103 None,
3104 None,
3105 None,
3106 None,
3107 None,
3108 None,
3109 ));
3110 let mut primary = TestOrderStubs::make_accepted_order(&order);
3111
3112 {
3113 let cache_rc = algo.core.cache_rc();
3114 let mut cache = cache_rc.borrow_mut();
3115 cache.add_order(primary.clone(), None, None, false).unwrap();
3116 }
3117
3118 let (handler, events) = subscribe_order_topic(strategy_id);
3119
3120 algo.reduce_primary_order(&mut primary, Quantity::from("0.3"));
3121
3122 msgbus::unsubscribe_order_events(format!("events.order.{strategy_id}").into(), &handler);
3123 let events = events.borrow();
3124
3125 assert_eq!(events.len(), 1);
3126 assert!(matches!(
3127 &events[0],
3128 OrderEventAny::Updated(event) if event.quantity == Quantity::from("0.7")
3129 ));
3130 }
3131
3132 #[rstest]
3133 fn test_algorithm_submit_order_publishes_initialized_for_new_order() {
3134 let mut algo = create_test_algorithm();
3135 register_algorithm(&mut algo);
3136
3137 let strategy_id = StrategyId::from("STRAT-ALGO-INIT-PUBLISH");
3138 let order = OrderAny::Market(MarketOrder::new(
3139 TraderId::from("TRADER-001"),
3140 strategy_id,
3141 InstrumentId::from("BTC/USDT.BINANCE"),
3142 ClientOrderId::from("O-ALGO-INIT"),
3143 OrderSide::Buy,
3144 Quantity::from("1.0"),
3145 TimeInForce::Gtc,
3146 UUID4::new(),
3147 0.into(),
3148 false,
3149 false,
3150 None,
3151 None,
3152 None,
3153 None,
3154 None,
3155 None,
3156 None,
3157 None,
3158 ));
3159 let (handler, events) = subscribe_order_topic(strategy_id);
3160
3161 algo.submit_order(order.clone(), None, None).unwrap();
3162
3163 msgbus::unsubscribe_order_events(format!("events.order.{strategy_id}").into(), &handler);
3164 let events = events.borrow();
3165
3166 assert_eq!(events.len(), 1);
3167 assert!(matches!(
3168 &events[0],
3169 OrderEventAny::Initialized(event) if event.client_order_id == order.client_order_id()
3170 ));
3171 }
3172
3173 #[rstest]
3174 fn test_algorithm_submit_order_does_not_republish_initialized_for_existing_order() {
3175 let mut algo = create_test_algorithm();
3176 register_algorithm(&mut algo);
3177
3178 let strategy_id = StrategyId::from("STRAT-ALGO-INIT-EXISTING");
3179 let order = OrderAny::Market(MarketOrder::new(
3180 TraderId::from("TRADER-001"),
3181 strategy_id,
3182 InstrumentId::from("BTC/USDT.BINANCE"),
3183 ClientOrderId::from("O-ALGO-INIT-EXISTING"),
3184 OrderSide::Buy,
3185 Quantity::from("1.0"),
3186 TimeInForce::Gtc,
3187 UUID4::new(),
3188 0.into(),
3189 false,
3190 false,
3191 None,
3192 None,
3193 None,
3194 None,
3195 None,
3196 None,
3197 None,
3198 None,
3199 ));
3200 {
3201 let cache_rc = algo.core.cache_rc();
3202 let mut cache = cache_rc.borrow_mut();
3203 cache.add_order(order.clone(), None, None, true).unwrap();
3204 }
3205 let (handler, events) = subscribe_order_topic(strategy_id);
3206
3207 algo.submit_order(order, None, None).unwrap();
3208
3209 msgbus::unsubscribe_order_events(format!("events.order.{strategy_id}").into(), &handler);
3210 assert!(events.borrow().is_empty());
3211 }
3212
3213 #[rstest]
3214 fn test_algorithm_submit_order_refuses_emulated_limit_spawn() {
3215 let mut algo = create_test_algorithm();
3216 register_algorithm(&mut algo);
3217
3218 let strategy_id = StrategyId::from("STRAT-ALGO-EMULATED-LIMIT");
3219 let order = OrderTestBuilder::new(OrderType::Market)
3220 .strategy_id(strategy_id)
3221 .instrument_id(InstrumentId::from("BTC/USDT.BINANCE"))
3222 .client_order_id(ClientOrderId::from("O-ALGO-EMULATED-LIMIT"))
3223 .quantity(Quantity::from("1.0"))
3224 .build();
3225 let mut primary = TestOrderStubs::make_accepted_order(&order);
3226 {
3227 let cache_rc = algo.core.cache_rc();
3228 let mut cache = cache_rc.borrow_mut();
3229 cache.add_order(primary.clone(), None, None, false).unwrap();
3230 }
3231 let (event_handler, events) = subscribe_order_topic(strategy_id);
3232 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
3233 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
3234 msgbus::register_trading_command_endpoint(
3235 MessagingSwitchboard::risk_engine_queue_execute(),
3236 risk_handler,
3237 );
3238 let (emulator_handler, emulator_messages): (
3239 _,
3240 TypedIntoMessageSavingHandler<TradingCommand>,
3241 ) = get_typed_into_message_saving_handler(Some(Ustr::from("OrderEmulator.execute")));
3242 msgbus::register_trading_command_endpoint(
3243 MessagingSwitchboard::order_emulator_execute(),
3244 emulator_handler,
3245 );
3246
3247 let spawned = algo.spawn_limit(
3248 &mut primary,
3249 Quantity::from("0.4"),
3250 Price::from("50000.0"),
3251 TimeInForce::Gtc,
3252 None,
3253 false,
3254 false,
3255 None,
3256 Some(TriggerType::BidAsk),
3257 None,
3258 true,
3259 );
3260 let spawned = OrderAny::Limit(spawned);
3261 let client_order_id = spawned.client_order_id();
3262 let result = algo.submit_order(spawned, None, None);
3263
3264 msgbus::unsubscribe_order_events(
3265 format!("events.order.{strategy_id}").into(),
3266 &event_handler,
3267 );
3268 let cache = algo.core.cache_ref();
3269 let error = result.unwrap_err();
3270 assert!(
3271 error
3272 .downcast_ref::<EmulatedOrderSubmissionError>()
3273 .is_some()
3274 );
3275 let error = error.to_string();
3276 assert!(error.contains("live emulation trigger"), "{error}");
3277 assert!(error.contains(client_order_id.as_str()), "{error}");
3278 assert!(risk_messages.get_messages().is_empty());
3279 assert!(emulator_messages.get_messages().is_empty());
3280 assert!(!cache.order_exists(&client_order_id));
3281 assert!(!events.borrow().iter().any(|event| matches!(
3282 event,
3283 OrderEventAny::Initialized(initialized)
3284 if initialized.client_order_id == client_order_id
3285 )));
3286 assert_eq!(
3290 cache.order(&primary.client_order_id()).unwrap().quantity(),
3291 Quantity::from("0.6"),
3292 );
3293 drop(cache);
3294 assert!(
3295 algo.core
3296 .take_pending_spawn_reduction(client_order_id)
3297 .is_none()
3298 );
3299 }
3300
3301 #[rstest]
3302 fn test_algorithm_submit_order_routes_unemulated_spawn_to_risk() {
3303 let mut algo = create_test_algorithm();
3304 register_algorithm(&mut algo);
3305
3306 let mut primary = OrderTestBuilder::new(OrderType::Market)
3307 .instrument_id(InstrumentId::from("BTC/USDT.BINANCE"))
3308 .client_order_id(ClientOrderId::from("O-ALGO-UNEMULATED"))
3309 .quantity(Quantity::from("1.0"))
3310 .build();
3311 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
3312 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
3313 msgbus::register_trading_command_endpoint(
3314 MessagingSwitchboard::risk_engine_queue_execute(),
3315 risk_handler,
3316 );
3317
3318 let spawned = algo.spawn_limit(
3319 &mut primary,
3320 Quantity::from("0.4"),
3321 Price::from("50000.0"),
3322 TimeInForce::Gtc,
3323 None,
3324 false,
3325 false,
3326 None,
3327 None,
3328 None,
3329 false,
3330 );
3331 let client_order_id = spawned.client_order_id;
3332 algo.submit_order(OrderAny::Limit(spawned), None, None)
3333 .unwrap();
3334
3335 let risk_messages = risk_messages.get_messages();
3336 assert_eq!(risk_messages.len(), 1);
3337 assert!(matches!(
3338 risk_messages.first(),
3339 Some(TradingCommand::SubmitOrder(command))
3340 if command.client_order_id == client_order_id
3341 ));
3342 }
3343
3344 #[rstest]
3345 fn test_algorithm_spawn_market_with_reduce_primary() {
3346 let mut algo = create_test_algorithm();
3347 register_algorithm(&mut algo);
3348
3349 let order = OrderAny::Market(MarketOrder::new(
3350 TraderId::from("TRADER-001"),
3351 StrategyId::from("STRAT-001"),
3352 InstrumentId::from("BTC/USDT.BINANCE"),
3353 ClientOrderId::from("O-001"),
3354 OrderSide::Buy,
3355 Quantity::from("1.0"),
3356 TimeInForce::Gtc,
3357 UUID4::new(),
3358 0.into(),
3359 false,
3360 false,
3361 None,
3362 None,
3363 None,
3364 None,
3365 None,
3366 None,
3367 None,
3368 None,
3369 ));
3370
3371 let mut primary = TestOrderStubs::make_accepted_order(&order);
3373
3374 {
3375 let cache_rc = algo.core.cache_rc();
3376 let mut cache = cache_rc.borrow_mut();
3377 cache.add_order(primary.clone(), None, None, false).unwrap();
3378 }
3379
3380 let spawned = algo.spawn_market(
3381 &mut primary,
3382 Quantity::from("0.4"),
3383 TimeInForce::Ioc,
3384 false,
3385 None,
3386 true, );
3388
3389 assert_eq!(spawned.quantity, Quantity::from("0.4"));
3390 assert_eq!(primary.quantity(), Quantity::from("0.6"));
3391 }
3392 #[rstest]
3393 fn test_algorithm_forwards_captured_params_to_spawned_order() {
3394 let mut algo = create_test_algorithm();
3395 register_algorithm(&mut algo);
3396
3397 let strategy_id = StrategyId::from("STRAT-FWD-001");
3398 let mut primary = OrderAny::Market(MarketOrder::new(
3399 TraderId::from("TRADER-001"),
3400 strategy_id,
3401 InstrumentId::from("BTC/USDT.BINANCE"),
3402 ClientOrderId::from("O-FWD-001"),
3403 OrderSide::Buy,
3404 Quantity::from("1.0"),
3405 TimeInForce::Gtc,
3406 UUID4::new(),
3407 0.into(),
3408 false,
3409 false,
3410 None,
3411 None,
3412 None,
3413 None,
3414 None,
3415 None,
3416 None,
3417 None,
3418 ));
3419 {
3420 let cache_rc = algo.core.cache_rc();
3421 let mut cache = cache_rc.borrow_mut();
3422 cache.add_order(primary.clone(), None, None, true).unwrap();
3423 }
3424
3425 let mut params = nautilus_core::Params::new();
3426 params.insert("is_leverage".to_string(), serde_json::Value::Bool(true));
3427 let command = SubmitOrder::new(
3428 TraderId::from("TRADER-001"),
3429 None,
3430 strategy_id,
3431 primary.instrument_id(),
3432 primary.client_order_id(),
3433 primary.init_event().clone(),
3434 primary.exec_algorithm_id(),
3435 None,
3436 Some(params),
3437 UUID4::new(),
3438 0.into(),
3439 None,
3440 );
3441 algo.execute(TradingCommand::SubmitOrder(command)).unwrap();
3442
3443 let received = Rc::new(RefCell::new(None::<SubmitOrder>));
3444 let handler = msgbus::TypedIntoHandler::from({
3445 let captured = received.clone();
3446 move |cmd: TradingCommand| {
3447 if let TradingCommand::SubmitOrder(cmd) = cmd {
3448 *captured.borrow_mut() = Some(cmd);
3449 }
3450 }
3451 });
3452 msgbus::register_trading_command_endpoint(
3453 MessagingSwitchboard::risk_engine_queue_execute(),
3454 handler,
3455 );
3456
3457 let spawned = algo.spawn_market(
3458 &mut primary,
3459 Quantity::from("0.4"),
3460 TimeInForce::Ioc,
3461 false,
3462 None,
3463 false, );
3465 algo.submit_order(OrderAny::Market(spawned), None, None)
3466 .unwrap();
3467
3468 let captured = received.borrow();
3469 let cmd = captured.as_ref().expect("expected a forwarded SubmitOrder");
3470 assert_eq!(cmd.client_order_id, ClientOrderId::from("O-FWD-001-E1"));
3471 assert_eq!(
3472 cmd.params.as_ref().and_then(|p| p.get_bool("is_leverage")),
3473 Some(true),
3474 );
3475 }
3476
3477 #[rstest]
3478 fn test_algorithm_routes_modify_and_cancel_commands_through_engine_queues() {
3479 let mut modify_algo = create_test_algorithm();
3480 let mut cancel_algo = create_test_algorithm();
3481 register_algorithm(&mut modify_algo);
3482 register_algorithm(&mut cancel_algo);
3483
3484 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
3485 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
3486 msgbus::register_trading_command_endpoint(
3487 MessagingSwitchboard::risk_engine_queue_execute(),
3488 risk_handler,
3489 );
3490 let (exec_handler, exec_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
3491 get_typed_into_message_saving_handler(Some(Ustr::from("ExecEngine.queue_execute")));
3492 msgbus::register_trading_command_endpoint(
3493 MessagingSwitchboard::exec_engine_queue_execute(),
3494 exec_handler,
3495 );
3496
3497 let mut modify_order = TestOrderStubs::make_accepted_order(
3498 &OrderTestBuilder::new(OrderType::Limit)
3499 .strategy_id(StrategyId::from("STRAT-ALGO-ROUTING"))
3500 .instrument_id(InstrumentId::from("BTC/USDT.BINANCE"))
3501 .client_order_id(ClientOrderId::from("O-ALGO-MODIFY"))
3502 .quantity(Quantity::from("1.0"))
3503 .price(Price::from("50000.0"))
3504 .build(),
3505 );
3506 let mut cancel_order = TestOrderStubs::make_accepted_order(
3507 &OrderTestBuilder::new(OrderType::Market)
3508 .strategy_id(StrategyId::from("STRAT-ALGO-ROUTING"))
3509 .instrument_id(InstrumentId::from("BTC/USDT.BINANCE"))
3510 .client_order_id(ClientOrderId::from("O-ALGO-CANCEL"))
3511 .quantity(Quantity::from("1.0"))
3512 .build(),
3513 );
3514 {
3515 let cache_rc = modify_algo.core.cache_rc();
3516 let mut cache = cache_rc.borrow_mut();
3517 cache
3518 .add_order(modify_order.clone(), None, None, false)
3519 .unwrap();
3520 }
3521 {
3522 let cache_rc = cancel_algo.core.cache_rc();
3523 let mut cache = cache_rc.borrow_mut();
3524 cache
3525 .add_order(cancel_order.clone(), None, None, false)
3526 .unwrap();
3527 }
3528
3529 modify_algo
3530 .modify_order(
3531 &mut modify_order,
3532 None,
3533 Some(Price::from("51000.0")),
3534 None,
3535 None,
3536 )
3537 .unwrap();
3538 cancel_algo.cancel_order(&mut cancel_order, None).unwrap();
3539
3540 let risk_messages = risk_messages.get_messages();
3541 let exec_messages = exec_messages.get_messages();
3542 assert_eq!(risk_messages.len(), 1);
3543 assert!(matches!(
3544 risk_messages.first(),
3545 Some(TradingCommand::ModifyOrder(command))
3546 if command.client_order_id == modify_order.client_order_id()
3547 ));
3548 assert_eq!(exec_messages.len(), 1);
3549 assert!(matches!(
3550 exec_messages.first(),
3551 Some(TradingCommand::CancelOrder(command))
3552 if command.client_order_id == cancel_order.client_order_id()
3553 ));
3554 }
3555
3556 #[rstest]
3557 fn test_algorithm_submit_order_list_captures_params_per_order() {
3558 use nautilus_common::messages::execution::SubmitOrderList;
3559 use nautilus_model::identifiers::OrderListId;
3560
3561 let mut algo = create_test_algorithm();
3562 register_algorithm(&mut algo);
3563
3564 let strategy_id = StrategyId::from("STRAT-LIST-001");
3565 let order1 = OrderAny::Market(MarketOrder::new(
3566 TraderId::from("TRADER-001"),
3567 strategy_id,
3568 InstrumentId::from("BTC/USDT.BINANCE"),
3569 ClientOrderId::from("O-LIST-001"),
3570 OrderSide::Buy,
3571 Quantity::from("1.0"),
3572 TimeInForce::Gtc,
3573 UUID4::new(),
3574 0.into(),
3575 false,
3576 false,
3577 None,
3578 None,
3579 None,
3580 None,
3581 None,
3582 None,
3583 None,
3584 None,
3585 ));
3586 let order2 = OrderAny::Market(MarketOrder::new(
3587 TraderId::from("TRADER-001"),
3588 strategy_id,
3589 InstrumentId::from("BTC/USDT.BINANCE"),
3590 ClientOrderId::from("O-LIST-002"),
3591 OrderSide::Buy,
3592 Quantity::from("1.0"),
3593 TimeInForce::Gtc,
3594 UUID4::new(),
3595 0.into(),
3596 false,
3597 false,
3598 None,
3599 None,
3600 None,
3601 None,
3602 None,
3603 None,
3604 None,
3605 None,
3606 ));
3607 {
3608 let cache_rc = algo.core.cache_rc();
3609 let mut cache = cache_rc.borrow_mut();
3610 cache.add_order(order1.clone(), None, None, true).unwrap();
3611 cache.add_order(order2.clone(), None, None, true).unwrap();
3612 }
3613
3614 let order_list = OrderList::new(
3615 OrderListId::from("OL-001"),
3616 order1.instrument_id(),
3617 strategy_id,
3618 vec![order1.client_order_id(), order2.client_order_id()],
3619 0.into(),
3620 );
3621
3622 let mut params = nautilus_core::Params::new();
3623 params.insert("is_leverage".to_string(), serde_json::Value::Bool(true));
3624 let command = SubmitOrderList::new(
3625 TraderId::from("TRADER-001"),
3626 None,
3627 strategy_id,
3628 order_list,
3629 vec![order1.init_event().clone(), order2.init_event().clone()],
3630 order1.exec_algorithm_id(),
3631 None,
3632 Some(params),
3633 UUID4::new(),
3634 0.into(),
3635 None,
3636 );
3637 algo.execute(TradingCommand::SubmitOrderList(command))
3638 .unwrap();
3639
3640 assert_eq!(
3641 algo.order_client_ids,
3642 [
3643 ClientOrderId::from("O-LIST-001"),
3644 ClientOrderId::from("O-LIST-002"),
3645 ],
3646 );
3647
3648 for id in ["O-LIST-001", "O-LIST-002"] {
3649 assert_eq!(
3650 algo.core
3651 .submit_params(&ClientOrderId::from(id))
3652 .and_then(|p| p.get_bool("is_leverage")),
3653 Some(true),
3654 "expected forwarded params for {id}",
3655 );
3656 }
3657 }
3658
3659 #[rstest]
3660 fn test_algorithm_generate_order_canceled() {
3661 let mut algo = create_test_algorithm();
3662 register_algorithm(&mut algo);
3663
3664 let order = OrderAny::Market(MarketOrder::new(
3665 TraderId::from("TRADER-001"),
3666 StrategyId::from("STRAT-001"),
3667 InstrumentId::from("BTC/USDT.BINANCE"),
3668 ClientOrderId::from("O-001"),
3669 OrderSide::Buy,
3670 Quantity::from("1.0"),
3671 TimeInForce::Gtc,
3672 UUID4::new(),
3673 0.into(),
3674 false,
3675 false,
3676 None,
3677 None,
3678 None,
3679 None,
3680 None,
3681 None,
3682 None,
3683 None,
3684 ));
3685
3686 let event = algo.generate_order_canceled(&order);
3687
3688 assert_eq!(event.trader_id, TraderId::from("TRADER-001"));
3689 assert_eq!(event.strategy_id, StrategyId::from("STRAT-001"));
3690 assert_eq!(event.instrument_id, InstrumentId::from("BTC/USDT.BINANCE"));
3691 assert_eq!(event.client_order_id, ClientOrderId::from("O-001"));
3692 }
3693
3694 #[rstest]
3695 fn test_algorithm_handle_cancel_order_publishes_instrument_canceled_topic() {
3696 let mut algo = create_test_algorithm();
3697 register_algorithm(&mut algo);
3698
3699 let strategy_id = StrategyId::from("STRAT-ALGO-CANCEL-PUBLISH");
3700 let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
3701 let order = OrderAny::Market(MarketOrder::new(
3702 TraderId::from("TRADER-001"),
3703 strategy_id,
3704 instrument_id,
3705 ClientOrderId::from("O-ALGO-CANCEL"),
3706 OrderSide::Buy,
3707 Quantity::from("1.0"),
3708 TimeInForce::Gtc,
3709 UUID4::new(),
3710 0.into(),
3711 false,
3712 false,
3713 None,
3714 None,
3715 None,
3716 None,
3717 None,
3718 None,
3719 None,
3720 None,
3721 ));
3722 let order = TestOrderStubs::make_accepted_order(&order);
3723
3724 {
3725 let cache_rc = algo.core.cache_rc();
3726 let mut cache = cache_rc.borrow_mut();
3727 cache.add_order(order.clone(), None, None, false).unwrap();
3728 }
3729
3730 let received = Rc::new(RefCell::new(Vec::<OrderEventAny>::new()));
3731 let handler = TypedHandler::from({
3732 let received = received.clone();
3733 move |event: &OrderEventAny| {
3734 received.borrow_mut().push(event.clone());
3735 }
3736 });
3737 let topic = msgbus::switchboard::get_order_canceled_topic(instrument_id);
3738 msgbus::subscribe_order_events(topic.into(), handler.clone(), None);
3739
3740 let command = CancelOrder::new(
3741 order.trader_id(),
3742 None,
3743 strategy_id,
3744 instrument_id,
3745 order.client_order_id(),
3746 order.venue_order_id(),
3747 UUID4::new(),
3748 0.into(),
3749 None,
3750 None,
3751 );
3752 algo.handle_cancel_order(command).unwrap();
3753
3754 msgbus::unsubscribe_order_events(topic.into(), &handler);
3755 let received = received.borrow();
3756 assert_eq!(received.len(), 1);
3757 assert!(matches!(received[0], OrderEventAny::Canceled(_)));
3758 assert_eq!(received[0].client_order_id(), order.client_order_id());
3759 assert_eq!(received[0].instrument_id(), instrument_id);
3760 }
3761
3762 #[rstest]
3763 fn test_algorithm_execute_dispatches_modify_order_to_handler() {
3764 let unique_id = format!("TEST-{}", UUID4::new());
3765 let config = ExecutionAlgorithmConfig {
3766 exec_algorithm_id: Some(ExecAlgorithmId::new(&unique_id)),
3767 ..Default::default()
3768 };
3769 let mut algo = ModifyDispatchAlgorithm::new(config);
3770 algo.core
3771 .register(
3772 TraderId::from("TRADER-001"),
3773 Rc::new(RefCell::new(TestClock::new())),
3774 Rc::new(RefCell::new(Cache::default())),
3775 )
3776 .unwrap();
3777 algo.transition_state(ComponentTrigger::Initialize).unwrap();
3778 algo.transition_state(ComponentTrigger::Start).unwrap();
3779 algo.transition_state(ComponentTrigger::StartCompleted)
3780 .unwrap();
3781
3782 let client_order_id = ClientOrderId::from("O-ALGO-DISPATCH");
3783 let command = ModifyOrder::new(
3784 TraderId::from("TRADER-001"),
3785 None,
3786 StrategyId::from("STRAT-ALGO-DISPATCH"),
3787 InstrumentId::from("BTC/USDT.BINANCE"),
3788 client_order_id,
3789 None,
3790 Some(Quantity::from("0.5")),
3791 None,
3792 None,
3793 UUID4::new(),
3794 0.into(),
3795 None,
3796 None,
3797 );
3798
3799 algo.execute(TradingCommand::ModifyOrder(command)).unwrap();
3800
3801 assert_eq!(algo.modify_client_order_ids, vec![client_order_id]);
3802 }
3803
3804 #[rstest]
3805 fn test_algorithm_handle_modify_order_refuses_active_local_order_without_events() {
3806 let mut algo = create_test_algorithm();
3807 register_algorithm(&mut algo);
3808
3809 let strategy_id = StrategyId::from("STRAT-ALGO-MODIFY");
3810 let order = OrderTestBuilder::new(OrderType::Market)
3811 .trader_id(TraderId::from("TRADER-001"))
3812 .strategy_id(strategy_id)
3813 .instrument_id(InstrumentId::from("BTC/USDT.BINANCE"))
3814 .client_order_id(ClientOrderId::from("O-ALGO-MODIFY"))
3815 .quantity(Quantity::from("1.0"))
3816 .exec_algorithm_id(algo.id())
3817 .exec_spawn_id(ClientOrderId::from("O-ALGO-MODIFY"))
3818 .build();
3819 {
3820 let cache_rc = algo.core.cache_rc();
3821 cache_rc
3822 .borrow_mut()
3823 .add_order(order.clone(), None, None, false)
3824 .unwrap();
3825 }
3826 let (handler, events) = subscribe_order_topic(strategy_id);
3827 let command = ModifyOrder::new(
3828 order.trader_id(),
3829 None,
3830 strategy_id,
3831 order.instrument_id(),
3832 order.client_order_id(),
3833 None,
3834 Some(Quantity::from("0.5")),
3835 None,
3836 None,
3837 UUID4::new(),
3838 0.into(),
3839 None,
3840 None,
3841 );
3842
3843 algo.execute(TradingCommand::ModifyOrder(command)).unwrap();
3844
3845 msgbus::unsubscribe_order_events(format!("events.order.{strategy_id}").into(), &handler);
3846 let cached_order = algo.cache().order(&order.client_order_id()).unwrap();
3847 assert_eq!(cached_order.status(), OrderStatus::Initialized);
3848 assert_eq!(cached_order.quantity(), Quantity::from("1.0"));
3849 assert!(events.borrow().is_empty());
3850 }
3851
3852 #[rstest]
3853 fn test_algorithm_modify_order_in_place_updates_quantity() {
3854 let mut algo = create_test_algorithm();
3855 register_algorithm(&mut algo);
3856
3857 let strategy_id = StrategyId::from("STRAT-ALGO-MODIFY-IN-PLACE");
3858 let mut order = OrderAny::Limit(LimitOrder::new(
3859 TraderId::from("TRADER-001"),
3860 strategy_id,
3861 InstrumentId::from("BTC/USDT.BINANCE"),
3862 ClientOrderId::from("O-001"),
3863 OrderSide::Buy,
3864 Quantity::from("1.0"),
3865 Price::from("50000.0"),
3866 TimeInForce::Gtc,
3867 None, false, false, false, None, None, None, None, None, None, None, None, None, None, None, UUID4::new(),
3883 0.into(),
3884 ));
3885
3886 {
3887 let cache_rc = algo.core.cache_rc();
3888 let mut cache = cache_rc.borrow_mut();
3889 cache.add_order(order.clone(), None, None, false).unwrap();
3890 }
3891
3892 let new_qty = Quantity::from("0.5");
3893 let (handler, events) = subscribe_order_topic(strategy_id);
3894
3895 algo.modify_order_in_place(&mut order, Some(new_qty), None, None)
3896 .unwrap();
3897
3898 msgbus::unsubscribe_order_events(format!("events.order.{strategy_id}").into(), &handler);
3899 let events = events.borrow();
3900
3901 assert_eq!(order.quantity(), new_qty);
3902 assert_eq!(events.len(), 1);
3903 assert!(matches!(
3904 &events[0],
3905 OrderEventAny::Updated(event) if event.quantity == new_qty
3906 ));
3907 }
3908
3909 #[rstest]
3910 fn test_algorithm_modify_order_in_place_rejects_no_changes() {
3911 let mut algo = create_test_algorithm();
3912 register_algorithm(&mut algo);
3913
3914 let mut order = OrderAny::Limit(LimitOrder::new(
3915 TraderId::from("TRADER-001"),
3916 StrategyId::from("STRAT-001"),
3917 InstrumentId::from("BTC/USDT.BINANCE"),
3918 ClientOrderId::from("O-001"),
3919 OrderSide::Buy,
3920 Quantity::from("1.0"),
3921 Price::from("50000.0"),
3922 TimeInForce::Gtc,
3923 None,
3924 false,
3925 false,
3926 false,
3927 None,
3928 None,
3929 None,
3930 None,
3931 None,
3932 None,
3933 None,
3934 None,
3935 None,
3936 None,
3937 None,
3938 UUID4::new(),
3939 0.into(),
3940 ));
3941
3942 let result =
3944 algo.modify_order_in_place(&mut order, Some(Quantity::from("1.0")), None, None);
3945
3946 assert!(result.is_err());
3947 assert!(
3948 result
3949 .unwrap_err()
3950 .to_string()
3951 .contains("no parameters differ")
3952 );
3953 }
3954
3955 #[rstest]
3956 fn test_spawned_order_denied_restores_primary_quantity() {
3957 let mut algo = create_test_algorithm();
3958 register_algorithm(&mut algo);
3959
3960 let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
3961 let exec_algorithm_id = algo.id();
3962 let client_order_id = ClientOrderId::from("O-001");
3963
3964 let mut primary = OrderAny::Market(MarketOrder::new(
3965 TraderId::from("TRADER-001"),
3966 StrategyId::from("STRAT-001"),
3967 instrument_id,
3968 client_order_id,
3969 OrderSide::Buy,
3970 Quantity::from("1.0"),
3971 TimeInForce::Gtc,
3972 UUID4::new(),
3973 0.into(),
3974 false,
3975 false,
3976 None,
3977 None,
3978 None,
3979 None,
3980 Some(exec_algorithm_id),
3981 None,
3982 Some(client_order_id),
3983 None,
3984 ));
3985
3986 {
3987 let cache_rc = algo.core.cache_rc();
3988 let mut cache = cache_rc.borrow_mut();
3989 cache.add_order(primary.clone(), None, None, false).unwrap();
3990 }
3991
3992 let spawned = algo.spawn_market(
3993 &mut primary,
3994 Quantity::from("0.5"),
3995 TimeInForce::Fok,
3996 false,
3997 None,
3998 true,
3999 );
4000
4001 assert_eq!(primary.quantity(), Quantity::from("0.5"));
4002
4003 let spawned_order = OrderAny::Market(spawned);
4004 {
4005 let cache_rc = algo.core.cache_rc();
4006 let mut cache = cache_rc.borrow_mut();
4007 cache
4008 .add_order(spawned_order.clone(), None, None, false)
4009 .unwrap();
4010 }
4011
4012 let denied = OrderDeniedSpec::builder()
4013 .trader_id(spawned_order.trader_id())
4014 .strategy_id(spawned_order.strategy_id())
4015 .instrument_id(spawned_order.instrument_id())
4016 .client_order_id(spawned_order.client_order_id())
4017 .reason("TEST_DENIAL".into())
4018 .build();
4019
4020 {
4021 let cache_rc = algo.core.cache_rc();
4022 let mut cache = cache_rc.borrow_mut();
4023 cache.update_order(&OrderEventAny::Denied(denied)).unwrap();
4024 }
4025
4026 algo.handle_order_event(OrderEventAny::Denied(denied));
4027
4028 let restored_primary = algo.cache().order(&client_order_id).unwrap();
4029 assert_eq!(restored_primary.quantity(), Quantity::from("1.0"));
4030 }
4031
4032 #[rstest]
4033 fn test_spawned_order_rejected_restores_primary_quantity() {
4034 let mut algo = create_test_algorithm();
4035 register_algorithm(&mut algo);
4036
4037 let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
4038 let exec_algorithm_id = algo.id();
4039 let client_order_id = ClientOrderId::from("O-001");
4040
4041 let mut primary = OrderAny::Market(MarketOrder::new(
4042 TraderId::from("TRADER-001"),
4043 StrategyId::from("STRAT-001"),
4044 instrument_id,
4045 client_order_id,
4046 OrderSide::Buy,
4047 Quantity::from("1.0"),
4048 TimeInForce::Gtc,
4049 UUID4::new(),
4050 0.into(),
4051 false,
4052 false,
4053 None,
4054 None,
4055 None,
4056 None,
4057 Some(exec_algorithm_id),
4058 None,
4059 Some(client_order_id),
4060 None,
4061 ));
4062
4063 {
4064 let cache_rc = algo.core.cache_rc();
4065 let mut cache = cache_rc.borrow_mut();
4066 cache.add_order(primary.clone(), None, None, false).unwrap();
4067 }
4068
4069 let spawned = algo.spawn_market(
4070 &mut primary,
4071 Quantity::from("0.5"),
4072 TimeInForce::Fok,
4073 false,
4074 None,
4075 true,
4076 );
4077
4078 assert_eq!(primary.quantity(), Quantity::from("0.5"));
4079
4080 let spawned_order = OrderAny::Market(spawned);
4081 {
4082 let cache_rc = algo.core.cache_rc();
4083 let mut cache = cache_rc.borrow_mut();
4084 cache
4085 .add_order(spawned_order.clone(), None, None, false)
4086 .unwrap();
4087 }
4088
4089 let rejected = OrderRejectedSpec::builder()
4090 .trader_id(spawned_order.trader_id())
4091 .strategy_id(spawned_order.strategy_id())
4092 .instrument_id(spawned_order.instrument_id())
4093 .client_order_id(spawned_order.client_order_id())
4094 .account_id(AccountId::from("BINANCE-001"))
4095 .reason("TEST_REJECTION".into())
4096 .build();
4097
4098 {
4099 let cache_rc = algo.core.cache_rc();
4100 let mut cache = cache_rc.borrow_mut();
4101 cache
4102 .update_order(&OrderEventAny::Rejected(rejected))
4103 .unwrap();
4104 }
4105
4106 algo.handle_order_event(OrderEventAny::Rejected(rejected));
4107
4108 let restored_primary = algo.cache().order(&client_order_id).unwrap();
4109 assert_eq!(restored_primary.quantity(), Quantity::from("1.0"));
4110 }
4111
4112 #[rstest]
4113 fn test_spawned_order_with_reduce_primary_false_does_not_restore() {
4114 let mut algo = create_test_algorithm();
4115 register_algorithm(&mut algo);
4116
4117 let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
4118 let exec_algorithm_id = algo.id();
4119 let client_order_id = ClientOrderId::from("O-001");
4120
4121 let mut primary = OrderAny::Market(MarketOrder::new(
4122 TraderId::from("TRADER-001"),
4123 StrategyId::from("STRAT-001"),
4124 instrument_id,
4125 client_order_id,
4126 OrderSide::Buy,
4127 Quantity::from("1.0"),
4128 TimeInForce::Gtc,
4129 UUID4::new(),
4130 0.into(),
4131 false,
4132 false,
4133 None,
4134 None,
4135 None,
4136 None,
4137 Some(exec_algorithm_id),
4138 None,
4139 Some(client_order_id),
4140 None,
4141 ));
4142
4143 {
4144 let cache_rc = algo.core.cache_rc();
4145 let mut cache = cache_rc.borrow_mut();
4146 cache.add_order(primary.clone(), None, None, false).unwrap();
4147 }
4148
4149 let spawned = algo.spawn_market(
4150 &mut primary,
4151 Quantity::from("0.5"),
4152 TimeInForce::Fok,
4153 false,
4154 None,
4155 false,
4156 );
4157
4158 assert_eq!(primary.quantity(), Quantity::from("1.0"));
4159
4160 let spawned_order = OrderAny::Market(spawned);
4161 {
4162 let cache_rc = algo.core.cache_rc();
4163 let mut cache = cache_rc.borrow_mut();
4164 cache
4165 .add_order(spawned_order.clone(), None, None, false)
4166 .unwrap();
4167 }
4168
4169 let denied = OrderDeniedSpec::builder()
4170 .trader_id(spawned_order.trader_id())
4171 .strategy_id(spawned_order.strategy_id())
4172 .instrument_id(spawned_order.instrument_id())
4173 .client_order_id(spawned_order.client_order_id())
4174 .reason("TEST_DENIAL".into())
4175 .build();
4176
4177 {
4178 let cache_rc = algo.core.cache_rc();
4179 let mut cache = cache_rc.borrow_mut();
4180 cache.update_order(&OrderEventAny::Denied(denied)).unwrap();
4181 }
4182
4183 algo.handle_order_event(OrderEventAny::Denied(denied));
4184
4185 let final_primary = algo.cache().order(&client_order_id).unwrap();
4186 assert_eq!(final_primary.quantity(), Quantity::from("1.0"));
4187 }
4188
4189 #[rstest]
4190 fn test_multiple_spawns_with_one_denied_restores_correctly() {
4191 let mut algo = create_test_algorithm();
4192 register_algorithm(&mut algo);
4193
4194 let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
4195 let exec_algorithm_id = algo.id();
4196 let client_order_id = ClientOrderId::from("O-001");
4197
4198 let mut primary = OrderAny::Market(MarketOrder::new(
4199 TraderId::from("TRADER-001"),
4200 StrategyId::from("STRAT-001"),
4201 instrument_id,
4202 client_order_id,
4203 OrderSide::Buy,
4204 Quantity::from("1.0"),
4205 TimeInForce::Gtc,
4206 UUID4::new(),
4207 0.into(),
4208 false,
4209 false,
4210 None,
4211 None,
4212 None,
4213 None,
4214 Some(exec_algorithm_id),
4215 None,
4216 Some(client_order_id),
4217 None,
4218 ));
4219
4220 {
4221 let cache_rc = algo.core.cache_rc();
4222 let mut cache = cache_rc.borrow_mut();
4223 cache.add_order(primary.clone(), None, None, false).unwrap();
4224 }
4225
4226 let spawned1 = algo.spawn_market(
4227 &mut primary,
4228 Quantity::from("0.3"),
4229 TimeInForce::Fok,
4230 false,
4231 None,
4232 true,
4233 );
4234 let spawned2 = algo.spawn_market(
4235 &mut primary,
4236 Quantity::from("0.4"),
4237 TimeInForce::Fok,
4238 false,
4239 None,
4240 true,
4241 );
4242 assert_eq!(primary.quantity(), Quantity::from("0.3"));
4243
4244 let spawned_order1 = OrderAny::Market(spawned1);
4245 let spawned_order2 = OrderAny::Market(spawned2);
4246 {
4247 let cache_rc = algo.core.cache_rc();
4248 let mut cache = cache_rc.borrow_mut();
4249 cache.add_order(spawned_order1, None, None, false).unwrap();
4250 cache
4251 .add_order(spawned_order2.clone(), None, None, false)
4252 .unwrap();
4253 }
4254
4255 let denied = OrderDeniedSpec::builder()
4256 .trader_id(spawned_order2.trader_id())
4257 .strategy_id(spawned_order2.strategy_id())
4258 .instrument_id(spawned_order2.instrument_id())
4259 .client_order_id(spawned_order2.client_order_id())
4260 .reason("TEST_DENIAL".into())
4261 .build();
4262
4263 {
4264 let cache_rc = algo.core.cache_rc();
4265 let mut cache = cache_rc.borrow_mut();
4266 cache.update_order(&OrderEventAny::Denied(denied)).unwrap();
4267 }
4268
4269 let (handler, events) = subscribe_order_topic(spawned_order2.strategy_id());
4270
4271 algo.handle_order_event(OrderEventAny::Denied(denied));
4272
4273 msgbus::unsubscribe_order_events(
4274 format!("events.order.{}", spawned_order2.strategy_id()).into(),
4275 &handler,
4276 );
4277 let events = events.borrow();
4278
4279 let restored_primary = algo.cache().order(&client_order_id).unwrap();
4280 assert_eq!(restored_primary.quantity(), Quantity::from("0.7"));
4281 assert_eq!(events.len(), 1);
4282 assert!(matches!(
4283 &events[0],
4284 OrderEventAny::Updated(event) if event.quantity == Quantity::from("0.7")
4285 ));
4286 }
4287
4288 #[rstest]
4289 fn test_spawned_order_accepted_then_canceled_restores_reduction() {
4290 let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
4291
4292 let primary_after_accept = algo.cache().order(&client_order_id).unwrap();
4293 assert_eq!(primary_after_accept.quantity(), Quantity::from("0.5"));
4294
4295 cancel_spawned_order(&mut algo, &mut spawned_order);
4297
4298 let final_primary = algo.cache().order(&client_order_id).unwrap();
4299 assert_eq!(final_primary.quantity(), Quantity::from("1.0"));
4300 }
4301
4302 #[rstest]
4303 fn test_spawned_order_canceled_after_primary_submission_does_not_restore_reduction() {
4304 let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
4305 let mut primary = algo.cache().order(&client_order_id).unwrap();
4306 submit_order_in_cache(&mut algo, &mut primary);
4307
4308 cancel_spawned_order(&mut algo, &mut spawned_order);
4309
4310 let mut submitted_primary = algo.cache().order(&client_order_id).unwrap();
4311 assert_eq!(submitted_primary.quantity(), Quantity::from("0.5"));
4312 assert!(
4313 algo.core
4314 .take_pending_spawn_reduction(spawned_order.client_order_id())
4315 .is_none()
4316 );
4317 fill_spawned_order(&mut algo, &mut submitted_primary, Quantity::from("0.5"));
4318 assert!(submitted_primary.is_closed());
4319 assert_eq!(submitted_primary.status(), OrderStatus::Filled);
4320 }
4321
4322 #[rstest]
4323 fn test_spawned_order_canceled_during_primary_submission_handoff_does_not_restore() {
4324 let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
4325 let primary = algo.cache().order(&client_order_id).unwrap();
4326 algo.core
4327 .add_spawn_fill_debt(client_order_id, Quantity::from("0.1"));
4328 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4329 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4330 msgbus::register_trading_command_endpoint(
4331 MessagingSwitchboard::risk_engine_queue_execute(),
4332 risk_handler,
4333 );
4334 let strategy_id = primary.strategy_id();
4335 let (event_handler, events) = subscribe_order_topic(strategy_id);
4336
4337 algo.submit_order(primary, None, None).unwrap();
4338 assert_eq!(
4339 algo.cache().order(&client_order_id).unwrap().status(),
4340 OrderStatus::Initialized,
4341 );
4342 cancel_spawned_order(&mut algo, &mut spawned_order);
4343
4344 msgbus::unsubscribe_order_events(
4345 format!("events.order.{strategy_id}").into(),
4346 &event_handler,
4347 );
4348 let risk_messages = risk_messages.get_messages();
4349 let [TradingCommand::SubmitOrder(command)] = risk_messages.as_slice() else {
4350 panic!("Expected exactly one SubmitOrder command");
4351 };
4352 assert_eq!(command.client_order_id, client_order_id);
4353 assert_eq!(command.order_init.quantity, Quantity::from("1.0"));
4357 assert_eq!(
4358 algo.cache().order(&client_order_id).unwrap().quantity(),
4359 Quantity::from("0.5"),
4360 );
4361 assert!(!events.borrow().iter().any(|event| matches!(
4362 event,
4363 OrderEventAny::Updated(updated) if updated.client_order_id == client_order_id
4364 )));
4365 assert!(
4366 algo.core
4367 .take_pending_spawn_reduction(spawned_order.client_order_id())
4368 .is_none()
4369 );
4370 assert!(algo.core.spawn_fill_debt(client_order_id).is_none());
4371 }
4372
4373 #[rstest]
4374 fn test_late_spawn_fill_rededucts_restored_primary_before_final_submission() {
4375 let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
4376 cancel_spawned_order(&mut algo, &mut spawned_order);
4377 assert_eq!(
4378 algo.cache().order(&client_order_id).unwrap().quantity(),
4379 Quantity::from("1.0"),
4380 );
4381
4382 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.5"));
4383
4384 let mut primary = algo.cache().order(&client_order_id).unwrap();
4385 assert_eq!(primary.quantity(), Quantity::from("0.5"));
4386 submit_order_in_cache(&mut algo, &mut primary);
4387 assert_eq!(
4388 spawned_order.filled_qty() + primary.quantity(),
4389 Quantity::from("1.0"),
4390 );
4391 }
4392
4393 #[rstest]
4394 fn test_late_spawn_fill_during_primary_submission_handoff_does_not_rededuct() {
4395 let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
4396 cancel_spawned_order(&mut algo, &mut spawned_order);
4397 let primary = algo.cache().order(&client_order_id).unwrap();
4398 algo.core
4399 .add_spawn_fill_debt(client_order_id, Quantity::from("0.1"));
4400 let (risk_handler, risk_messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4401 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4402 msgbus::register_trading_command_endpoint(
4403 MessagingSwitchboard::risk_engine_queue_execute(),
4404 risk_handler,
4405 );
4406 let strategy_id = primary.strategy_id();
4407 let (event_handler, events) = subscribe_order_topic(strategy_id);
4408
4409 algo.submit_order(primary, None, None).unwrap();
4410 assert_eq!(
4411 algo.cache().order(&client_order_id).unwrap().status(),
4412 OrderStatus::Initialized,
4413 );
4414 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.5"));
4415
4416 msgbus::unsubscribe_order_events(
4417 format!("events.order.{strategy_id}").into(),
4418 &event_handler,
4419 );
4420 let risk_messages = risk_messages.get_messages();
4421 let [TradingCommand::SubmitOrder(command)] = risk_messages.as_slice() else {
4422 panic!("Expected exactly one SubmitOrder command");
4423 };
4424 assert_eq!(command.client_order_id, client_order_id);
4425 assert_eq!(command.order_init.quantity, Quantity::from("1.0"));
4428 assert_eq!(
4429 algo.cache().order(&client_order_id).unwrap().quantity(),
4430 Quantity::from("1.0"),
4431 );
4432 assert!(!events.borrow().iter().any(|event| matches!(
4433 event,
4434 OrderEventAny::Updated(updated) if updated.client_order_id == client_order_id
4435 )));
4436 assert!(
4437 algo.core
4438 .take_pending_spawn_reduction(spawned_order.client_order_id())
4439 .is_none()
4440 );
4441 assert!(algo.core.spawn_fill_debt(client_order_id).is_none());
4442 }
4443
4444 #[rstest]
4445 fn test_converted_quote_spawn_canceled_unfilled_restores_full_quote_quantity() {
4446 let (mut algo, client_order_id, mut spawned_order) =
4447 setup_accepted_quote_spawn(Quantity::from("100"), Quantity::from("50"));
4448 convert_spawn_to_base(&mut algo, &mut spawned_order, Quantity::from("5"));
4449
4450 cancel_spawned_order(&mut algo, &mut spawned_order);
4451
4452 assert_eq!(
4453 algo.cache().order(&client_order_id).unwrap().quantity(),
4454 Quantity::from("100"),
4455 );
4456 assert!(
4457 algo.cache()
4458 .order(&client_order_id)
4459 .unwrap()
4460 .is_quote_quantity()
4461 );
4462 assert_eq!(
4463 algo.core
4464 .spawn_reduction(spawned_order.client_order_id())
4465 .unwrap()
4466 .restored_qty
4467 .unwrap(),
4468 Quantity::from("50"),
4469 );
4470 }
4471
4472 #[rstest]
4473 fn test_converted_quote_spawn_partial_fill_restores_proportional_quote_quantity() {
4474 let (mut algo, client_order_id, mut spawned_order) =
4475 setup_accepted_quote_spawn(Quantity::from("100"), Quantity::from("50"));
4476 convert_spawn_to_base(&mut algo, &mut spawned_order, Quantity::from("5"));
4477 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("2"));
4478
4479 cancel_spawned_order(&mut algo, &mut spawned_order);
4480
4481 assert_eq!(
4482 algo.cache().order(&client_order_id).unwrap().quantity(),
4483 Quantity::from("80"),
4484 );
4485 assert!(
4486 algo.cache()
4487 .order(&client_order_id)
4488 .unwrap()
4489 .is_quote_quantity()
4490 );
4491 assert_eq!(
4492 algo.core
4493 .spawn_reduction(spawned_order.client_order_id())
4494 .unwrap()
4495 .restored_qty
4496 .unwrap(),
4497 Quantity::from("30"),
4498 );
4499 }
4500
4501 #[rstest]
4502 fn test_converted_quote_spawn_late_fill_charges_proportional_quote_quantity() {
4503 let (mut algo, client_order_id, mut spawned_order) =
4504 setup_accepted_quote_spawn(Quantity::from("100"), Quantity::from("50"));
4505 convert_spawn_to_base(&mut algo, &mut spawned_order, Quantity::from("5"));
4506 cancel_spawned_order(&mut algo, &mut spawned_order);
4507
4508 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("1"));
4509
4510 assert_eq!(
4511 algo.cache().order(&client_order_id).unwrap().quantity(),
4512 Quantity::from("90"),
4513 );
4514 assert!(
4515 algo.cache()
4516 .order(&client_order_id)
4517 .unwrap()
4518 .is_quote_quantity()
4519 );
4520 assert_eq!(
4521 algo.core
4522 .spawn_reduction(spawned_order.client_order_id())
4523 .unwrap()
4524 .restored_qty
4525 .unwrap(),
4526 Quantity::from("40"),
4527 );
4528 }
4529
4530 #[rstest]
4531 fn test_converted_quote_spawn_restoration_rounds_down_to_primary_precision() {
4532 let (mut algo, client_order_id, mut spawned_order) =
4533 setup_accepted_quote_spawn(Quantity::from("20.00"), Quantity::from("10.00"));
4534 convert_spawn_to_base(&mut algo, &mut spawned_order, Quantity::from("3.000"));
4535 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("1.000"));
4536
4537 cancel_spawned_order(&mut algo, &mut spawned_order);
4538
4539 assert_eq!(
4540 algo.cache().order(&client_order_id).unwrap().quantity(),
4541 Quantity::from("16.66"),
4542 );
4543 assert!(
4544 algo.cache()
4545 .order(&client_order_id)
4546 .unwrap()
4547 .is_quote_quantity()
4548 );
4549 assert_eq!(
4550 algo.core
4551 .spawn_reduction(spawned_order.client_order_id())
4552 .unwrap()
4553 .restored_qty
4554 .unwrap(),
4555 Quantity::from("6.66"),
4556 );
4557 }
4558
4559 #[rstest]
4560 fn test_converted_quote_spawn_repeated_fractional_late_fills_conserve_budget() {
4561 let (mut algo, client_order_id, mut spawned_order) =
4562 setup_accepted_quote_spawn(Quantity::from("20.00"), Quantity::from("10.00"));
4563 convert_spawn_to_base(&mut algo, &mut spawned_order, Quantity::from("3.000"));
4564 cancel_spawned_order(&mut algo, &mut spawned_order);
4565
4566 for (primary_qty, restored_qty) in [
4567 (Quantity::from("16.66"), Quantity::from("6.66")),
4568 (Quantity::from("13.33"), Quantity::from("3.33")),
4569 (Quantity::from("10.00"), Quantity::from("0.00")),
4570 ] {
4571 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("1.000"));
4572 assert_eq!(
4573 algo.cache().order(&client_order_id).unwrap().quantity(),
4574 primary_qty,
4575 );
4576 assert_eq!(
4577 algo.core
4578 .spawn_reduction(spawned_order.client_order_id())
4579 .unwrap()
4580 .restored_qty
4581 .unwrap(),
4582 restored_qty,
4583 );
4584 }
4585 }
4586
4587 #[rstest]
4588 #[case::single_fill(Quantity::from("3.0"), 1)]
4589 #[case::split_fills(Quantity::from("0.1"), 30)]
4590 fn test_converted_quote_spawn_fill_partition_preserves_total(
4591 #[case] fill_qty: Quantity,
4592 #[case] fills: usize,
4593 ) {
4594 let (mut algo, primary_id, mut child) =
4595 setup_accepted_quote_spawn(Quantity::from("20"), Quantity::from("10"));
4596 convert_spawn_to_base(&mut algo, &mut child, Quantity::from("3.0"));
4597 cancel_spawned_order(&mut algo, &mut child);
4598
4599 for _ in 0..fills {
4600 fill_spawned_order(&mut algo, &mut child, fill_qty);
4601 }
4602
4603 assert_eq!(child.filled_qty(), Quantity::from("3.0"));
4604 assert_eq!(
4605 algo.cache().order(&primary_id).unwrap().quantity(),
4606 Quantity::from("10")
4607 );
4608 assert_eq!(
4609 algo.core
4610 .spawn_reduction(child.client_order_id())
4611 .unwrap()
4612 .restored_qty,
4613 Some(Quantity::from("0"))
4614 );
4615 assert!(algo.core.spawn_fill_debt(primary_id).is_none());
4616 }
4617
4618 #[rstest]
4619 fn test_increased_spawn_late_fill_debits_original_restoration() {
4620 let (mut algo, primary_id, mut child) = setup_pending_spawn();
4621 algo.modify_order_in_place(&mut child, Some(Quantity::from("0.8")), None, None)
4622 .unwrap();
4623 accept_spawned_order(&mut algo, &mut child);
4624 cancel_spawned_order(&mut algo, &mut child);
4625
4626 fill_spawned_order(&mut algo, &mut child, Quantity::from("0.1"));
4627
4628 assert_eq!(child.quantity(), Quantity::from("0.8"));
4629 assert_eq!(child.filled_qty(), Quantity::from("0.1"));
4630 assert_eq!(
4631 algo.cache().order(&primary_id).unwrap().quantity(),
4632 Quantity::from("0.9")
4633 );
4634 assert_eq!(
4635 algo.core
4636 .spawn_reduction(child.client_order_id())
4637 .unwrap()
4638 .restored_qty,
4639 Some(Quantity::from("0.4"))
4640 );
4641 assert!(algo.core.spawn_fill_debt(primary_id).is_none());
4642
4643 void_last_spawn_fill(&mut algo, &mut child, Quantity::from("0.1"));
4644
4645 assert_eq!(
4646 algo.cache().order(&primary_id).unwrap().quantity(),
4647 Quantity::from("1.0")
4648 );
4649 }
4650
4651 #[rstest]
4652 #[case::partial_fill(Quantity::from("0.1"))]
4653 #[case::full_fill(Quantity::from("0.5"))]
4654 fn test_voided_late_spawn_fill_restores_primary_quantity(#[case] fill_qty: Quantity) {
4655 let (mut algo, primary_id, mut child) = setup_accepted_spawn();
4656 cancel_spawned_order(&mut algo, &mut child);
4657 fill_spawned_order(&mut algo, &mut child, fill_qty);
4658
4659 void_last_spawn_fill(&mut algo, &mut child, fill_qty);
4660
4661 assert_eq!(child.filled_qty(), Quantity::from("0.0"));
4662 assert_eq!(
4663 algo.cache().order(&primary_id).unwrap().quantity(),
4664 Quantity::from("1.0")
4665 );
4666 assert!(algo.core.spawn_fill_debt(primary_id).is_none());
4667 assert_eq!(
4668 algo.core
4669 .spawn_reduction(child.client_order_id())
4670 .unwrap()
4671 .restored_qty,
4672 Some(Quantity::from("0.5"))
4673 );
4674 }
4675
4676 #[rstest]
4677 #[case::debt_only(
4678 Quantity::from("0.1"),
4679 Quantity::from("0.0"),
4680 Some(Quantity::from("0.1"))
4681 )]
4682 #[case::debt_and_quantity(Quantity::from("0.3"), Quantity::from("0.1"), None)]
4683 fn test_voided_late_spawn_fill_discharges_debt_before_restoring_quantity(
4684 #[case] voided_qty: Quantity,
4685 #[case] primary_qty: Quantity,
4686 #[case] debt_qty: Option<Quantity>,
4687 ) {
4688 let (mut algo, primary_id, mut child) = setup_accepted_spawn();
4689 cancel_spawned_order(&mut algo, &mut child);
4690 let _second = spawn_reduced_child(&mut algo, primary_id, Quantity::from("0.8"));
4691 fill_spawned_order(&mut algo, &mut child, Quantity::from("0.4"));
4692 assert_eq!(
4693 algo.core.spawn_fill_debt(primary_id),
4694 Some(Quantity::from("0.2"))
4695 );
4696
4697 void_last_spawn_fill(&mut algo, &mut child, voided_qty);
4698
4699 assert_eq!(
4700 algo.cache().order(&primary_id).unwrap().quantity(),
4701 primary_qty
4702 );
4703 assert_eq!(algo.core.spawn_fill_debt(primary_id), debt_qty);
4704 assert_eq!(child.filled_qty(), Quantity::from("0.4") - voided_qty);
4705 assert_eq!(
4706 algo.core
4707 .spawn_reduction(child.client_order_id())
4708 .unwrap()
4709 .restored_qty,
4710 Some(Quantity::from("0.1") + voided_qty)
4711 );
4712 }
4713
4714 #[rstest]
4715 fn test_spawn_fill_void_before_restoration_preserves_reserved_quantity() {
4716 let (mut algo, primary_id, mut child) = setup_accepted_spawn();
4717 fill_spawned_order(&mut algo, &mut child, Quantity::from("0.1"));
4718
4719 void_last_spawn_fill(&mut algo, &mut child, Quantity::from("0.1"));
4720
4721 assert_eq!(
4722 algo.cache().order(&primary_id).unwrap().quantity(),
4723 Quantity::from("0.5")
4724 );
4725 assert_eq!(
4726 algo.core
4727 .spawn_reduction(child.client_order_id())
4728 .unwrap()
4729 .restored_qty,
4730 None
4731 );
4732 cancel_spawned_order(&mut algo, &mut child);
4733 assert_eq!(
4734 algo.cache().order(&primary_id).unwrap().quantity(),
4735 Quantity::from("1.0")
4736 );
4737 }
4738
4739 #[rstest]
4740 fn test_spawn_fill_void_after_primary_handoff_preserves_submitted_quantity() {
4741 let (mut algo, primary_id, mut child) = setup_accepted_spawn();
4742 cancel_spawned_order(&mut algo, &mut child);
4743 fill_spawned_order(&mut algo, &mut child, Quantity::from("0.1"));
4744 let primary = algo.cache().order(&primary_id).unwrap();
4745 let (handler, _messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4746 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4747 msgbus::register_trading_command_endpoint(
4748 MessagingSwitchboard::risk_engine_queue_execute(),
4749 handler,
4750 );
4751 algo.submit_order(primary, None, None).unwrap();
4752
4753 void_last_spawn_fill(&mut algo, &mut child, Quantity::from("0.1"));
4754
4755 assert_eq!(
4756 algo.cache().order(&primary_id).unwrap().quantity(),
4757 Quantity::from("0.9")
4758 );
4759 assert_eq!(
4760 algo.cache().order(&primary_id).unwrap().status(),
4761 OrderStatus::Initialized
4762 );
4763 assert!(algo.core.primary_was_handed_off(primary_id));
4764 assert!(algo.core.spawn_reduction(child.client_order_id()).is_none());
4765 assert!(algo.core.spawn_fill_debt(primary_id).is_none());
4766 }
4767
4768 #[rstest]
4769 fn test_converted_quote_spawn_fill_void_restores_cumulative_budget_once() {
4770 let (mut algo, primary_id, mut child) =
4771 setup_accepted_quote_spawn(Quantity::from("20.00"), Quantity::from("10.00"));
4772 convert_spawn_to_base(&mut algo, &mut child, Quantity::from("3.000"));
4773 cancel_spawned_order(&mut algo, &mut child);
4774 fill_spawned_order(&mut algo, &mut child, Quantity::from("1.000"));
4775 fill_spawned_order(&mut algo, &mut child, Quantity::from("1.000"));
4776
4777 void_last_spawn_fill(&mut algo, &mut child, Quantity::from("0.500"));
4778 assert_eq!(
4779 algo.cache().order(&primary_id).unwrap().quantity(),
4780 Quantity::from("15.00")
4781 );
4782 void_last_spawn_fill(&mut algo, &mut child, Quantity::from("1.000"));
4783 let duplicate = child.events().into_iter().last().unwrap().clone();
4784 algo.handle_order_event(duplicate);
4785
4786 assert_eq!(child.filled_qty(), Quantity::from("1.000"));
4787 assert_eq!(
4788 algo.cache().order(&primary_id).unwrap().quantity(),
4789 Quantity::from("16.66")
4790 );
4791 assert_eq!(
4792 algo.core
4793 .spawn_reduction(child.client_order_id())
4794 .unwrap()
4795 .restored_qty,
4796 Some(Quantity::from("6.66"))
4797 );
4798 assert!(algo.core.spawn_fill_debt(primary_id).is_none());
4799 }
4800
4801 fn void_last_spawn_fill(algo: &mut TestAlgorithm, order: &mut OrderAny, quantity: Quantity) {
4802 let fill = order
4803 .events()
4804 .into_iter()
4805 .rev()
4806 .find_map(|event| match event {
4807 OrderEventAny::Filled(fill) => Some(fill.clone()),
4808 _ => None,
4809 })
4810 .unwrap();
4811 let voided = OrderFillVoidedSpec::builder()
4812 .trader_id(fill.trader_id)
4813 .strategy_id(fill.strategy_id)
4814 .instrument_id(fill.instrument_id)
4815 .client_order_id(fill.client_order_id)
4816 .venue_order_id(fill.venue_order_id)
4817 .account_id(fill.account_id)
4818 .trade_id(fill.trade_id)
4819 .voided_qty(quantity)
4820 .order_side(fill.order_side)
4821 .order_type(fill.order_type)
4822 .last_px(fill.last_px)
4823 .currency(fill.currency)
4824 .liquidity_side(fill.liquidity_side)
4825 .build();
4826 *order = algo
4827 .core
4828 .cache_rc()
4829 .borrow_mut()
4830 .update_order(&OrderEventAny::FillVoided(voided.clone()))
4831 .unwrap();
4832 algo.handle_order_event(OrderEventAny::FillVoided(voided));
4833 }
4834
4835 #[rstest]
4836 fn test_primary_handoff_clears_spawn_accounting_without_child_events() {
4837 let (mut algo, primary_id, mut child) = setup_accepted_spawn();
4838 cancel_spawned_order(&mut algo, &mut child);
4839 let primary = algo.cache().order(&primary_id).unwrap();
4840 let (handler, _messages): (_, TypedIntoMessageSavingHandler<TradingCommand>) =
4841 get_typed_into_message_saving_handler(Some(Ustr::from("RiskEngine.queue_execute")));
4842 msgbus::register_trading_command_endpoint(
4843 MessagingSwitchboard::risk_engine_queue_execute(),
4844 handler,
4845 );
4846
4847 algo.submit_order(primary, None, None).unwrap();
4848
4849 assert!(algo.core.spawn_reduction(child.client_order_id()).is_none());
4850 assert!(algo.core.spawn_fill_debt(primary_id).is_none());
4851 assert!(algo.core.primary_was_handed_off(primary_id));
4852
4853 let mut primary = algo.cache().order(&primary_id).unwrap();
4854 submit_order_in_cache(&mut algo, &mut primary);
4855 assert!(!algo.core.primary_was_handed_off(primary_id));
4856 }
4857
4858 #[rstest]
4859 fn test_primary_cancellation_clears_spawn_accounting() {
4860 let (mut algo, primary_id, mut child) = setup_accepted_spawn();
4861 cancel_spawned_order(&mut algo, &mut child);
4862 let second = spawn_reduced_child(&mut algo, primary_id, Quantity::from("0.8"));
4863 fill_spawned_order(&mut algo, &mut child, Quantity::from("0.4"));
4864 let mut primary = algo.cache().order(&primary_id).unwrap();
4865
4866 cancel_spawned_order(&mut algo, &mut primary);
4867
4868 assert_eq!(primary.status(), OrderStatus::Canceled);
4869 assert!(algo.core.spawn_reduction(child.client_order_id()).is_none());
4870 assert!(
4871 algo.core
4872 .spawn_reduction(second.client_order_id())
4873 .is_none()
4874 );
4875 assert!(algo.core.spawn_fill_debt(primary_id).is_none());
4876 assert!(!algo.core.primary_was_handed_off(primary_id));
4877 }
4878
4879 #[rstest]
4880 fn test_converted_quote_spawn_partial_cancel_then_late_fill_charges_restored_budget() {
4881 let (mut algo, client_order_id, mut spawned_order) =
4882 setup_accepted_quote_spawn(Quantity::from("100"), Quantity::from("50"));
4883 convert_spawn_to_base(&mut algo, &mut spawned_order, Quantity::from("5"));
4884 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("2"));
4885 cancel_spawned_order(&mut algo, &mut spawned_order);
4886
4887 assert_eq!(
4888 algo.cache().order(&client_order_id).unwrap().quantity(),
4889 Quantity::from("80"),
4890 );
4891 assert_eq!(
4892 algo.core
4893 .spawn_reduction(spawned_order.client_order_id())
4894 .unwrap()
4895 .restored_qty
4896 .unwrap(),
4897 Quantity::from("30"),
4898 );
4899
4900 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("1"));
4901
4902 assert_eq!(
4903 algo.cache().order(&client_order_id).unwrap().quantity(),
4904 Quantity::from("70"),
4905 );
4906 assert_eq!(
4907 algo.core
4908 .spawn_reduction(spawned_order.client_order_id())
4909 .unwrap()
4910 .restored_qty
4911 .unwrap(),
4912 Quantity::from("20"),
4913 );
4914 }
4915
4916 #[rstest]
4917 fn test_unmarked_submitted_primary_cancellation_discards_reduction_and_debt() {
4918 let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
4919 let mut primary = algo.cache().order(&client_order_id).unwrap();
4920 algo.core
4921 .add_spawn_fill_debt(client_order_id, Quantity::from("0.1"));
4922
4923 submit_order_in_cache(&mut algo, &mut primary);
4924 assert!(algo.core.spawn_fill_debt(client_order_id).is_none());
4925 cancel_spawned_order(&mut algo, &mut spawned_order);
4926
4927 assert_eq!(
4928 algo.cache().order(&client_order_id).unwrap().quantity(),
4929 Quantity::from("0.5"),
4930 );
4931 assert!(
4932 algo.core
4933 .spawn_reduction(spawned_order.client_order_id())
4934 .is_none()
4935 );
4936 assert!(algo.core.spawn_fill_debt(client_order_id).is_none());
4937 }
4938
4939 #[rstest]
4940 #[case::denied(true)]
4941 #[case::rejected(false)]
4942 fn test_spawn_refusal_does_not_restore_submitted_primary(#[case] denied: bool) {
4943 let (mut algo, client_order_id, spawned_order) = setup_pending_spawn();
4944 let spawned_id = spawned_order.client_order_id();
4945 let mut primary = algo.cache().order(&client_order_id).unwrap();
4946 submit_order_in_cache(&mut algo, &mut primary);
4947
4948 if denied {
4949 let event = OrderDeniedSpec::builder()
4950 .trader_id(spawned_order.trader_id())
4951 .strategy_id(spawned_order.strategy_id())
4952 .instrument_id(spawned_order.instrument_id())
4953 .client_order_id(spawned_id)
4954 .reason("TEST_DENIAL".into())
4955 .build();
4956 algo.handle_order_event(OrderEventAny::Denied(event));
4957 } else {
4958 let event = OrderRejectedSpec::builder()
4959 .trader_id(spawned_order.trader_id())
4960 .strategy_id(spawned_order.strategy_id())
4961 .instrument_id(spawned_order.instrument_id())
4962 .client_order_id(spawned_id)
4963 .account_id(AccountId::from("BINANCE-001"))
4964 .reason("TEST_REJECTION".into())
4965 .build();
4966 algo.handle_order_event(OrderEventAny::Rejected(event));
4967 }
4968
4969 assert_eq!(
4970 algo.cache().order(&client_order_id).unwrap().quantity(),
4971 Quantity::from("0.5"),
4972 );
4973 assert!(algo.core.take_pending_spawn_reduction(spawned_id).is_none());
4974 }
4975
4976 #[rstest]
4977 fn test_multiple_late_partial_fills_net_only_the_restored_quantity() {
4978 let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
4979 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.2"));
4980 cancel_spawned_order(&mut algo, &mut spawned_order);
4981
4982 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.1"));
4983 assert_eq!(
4984 algo.cache().order(&client_order_id).unwrap().quantity(),
4985 Quantity::from("0.7"),
4986 );
4987
4988 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.2"));
4989 assert_eq!(
4990 algo.cache().order(&client_order_id).unwrap().quantity(),
4991 Quantity::from("0.5"),
4992 );
4993 assert_eq!(
4994 algo.core
4995 .spawn_reduction(spawned_order.client_order_id())
4996 .unwrap()
4997 .restored_qty,
4998 Some(Quantity::from("0.0")),
4999 );
5000 }
5001
5002 #[rstest]
5003 fn test_late_spawn_fill_after_restored_quantity_reused_caps_at_primary_quantity() {
5004 let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
5005 cancel_spawned_order(&mut algo, &mut spawned_order);
5006 assert_eq!(
5007 algo.cache().order(&client_order_id).unwrap().quantity(),
5008 Quantity::from("1.0"),
5009 );
5010
5011 let mut primary = algo.cache().order(&client_order_id).unwrap();
5013 let second_order = OrderAny::Market(algo.spawn_market(
5014 &mut primary,
5015 Quantity::from("0.8"),
5016 TimeInForce::Fok,
5017 false,
5018 None,
5019 true,
5020 ));
5021 algo.core
5022 .cache_rc()
5023 .borrow_mut()
5024 .add_order(second_order.clone(), None, None, false)
5025 .unwrap();
5026 assert_eq!(
5027 algo.cache().order(&client_order_id).unwrap().quantity(),
5028 Quantity::from("0.2"),
5029 );
5030
5031 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.5"));
5034 assert_eq!(
5035 algo.cache().order(&client_order_id).unwrap().quantity(),
5036 Quantity::from("0.0"),
5037 );
5038 assert_eq!(
5039 algo.core
5040 .spawn_reduction(spawned_order.client_order_id())
5041 .unwrap()
5042 .restored_qty,
5043 Some(Quantity::from("0.0")),
5044 );
5045 assert_eq!(
5046 algo.core.spawn_fill_debt(client_order_id),
5047 Some(Quantity::from("0.3")),
5048 );
5049
5050 let rejected = OrderRejectedSpec::builder()
5053 .trader_id(second_order.trader_id())
5054 .strategy_id(second_order.strategy_id())
5055 .instrument_id(second_order.instrument_id())
5056 .client_order_id(second_order.client_order_id())
5057 .account_id(AccountId::from("BINANCE-001"))
5058 .reason("TEST_REJECTION".into())
5059 .build();
5060 algo.handle_order_event(OrderEventAny::Rejected(rejected));
5061
5062 let final_primary = algo.cache().order(&client_order_id).unwrap();
5063 assert_eq!(final_primary.quantity(), Quantity::from("0.5"));
5064 assert!(algo.core.spawn_fill_debt(client_order_id).is_none());
5065 assert_eq!(
5066 spawned_order.filled_qty() + final_primary.quantity(),
5067 Quantity::from("1.0"),
5068 );
5069 }
5070
5071 #[rstest]
5072 fn test_late_fill_on_child_that_fully_discharged_debt_recreates_debt() {
5073 let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
5074 cancel_spawned_order(&mut algo, &mut spawned_order);
5075
5076 let mut child_b = spawn_reduced_child(&mut algo, client_order_id, Quantity::from("0.3"));
5077 let mut child_c = spawn_reduced_child(&mut algo, client_order_id, Quantity::from("0.5"));
5078 accept_spawned_order(&mut algo, &mut child_b);
5079 accept_spawned_order(&mut algo, &mut child_c);
5080 assert_eq!(
5081 algo.cache().order(&client_order_id).unwrap().quantity(),
5082 Quantity::from("0.2"),
5083 );
5084
5085 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.5"));
5086 assert_eq!(
5087 algo.cache().order(&client_order_id).unwrap().quantity(),
5088 Quantity::from("0.0"),
5089 );
5090 assert_eq!(
5091 algo.core.spawn_fill_debt(client_order_id),
5092 Some(Quantity::from("0.3")),
5093 );
5094
5095 cancel_spawned_order(&mut algo, &mut child_b);
5098 assert_eq!(
5099 algo.cache().order(&client_order_id).unwrap().quantity(),
5100 Quantity::from("0.0"),
5101 );
5102 assert!(algo.core.spawn_fill_debt(client_order_id).is_none());
5103 assert_eq!(
5104 algo.core
5105 .spawn_reduction(child_b.client_order_id())
5106 .unwrap()
5107 .restored_qty
5108 .unwrap(),
5109 Quantity::from("0.3"),
5110 );
5111
5112 fill_spawned_order(&mut algo, &mut child_b, Quantity::from("0.2"));
5114 assert_eq!(
5115 algo.cache().order(&client_order_id).unwrap().quantity(),
5116 Quantity::from("0.0"),
5117 );
5118 assert_eq!(
5119 algo.core.spawn_fill_debt(client_order_id),
5120 Some(Quantity::from("0.2")),
5121 );
5122
5123 cancel_spawned_order(&mut algo, &mut child_c);
5124 let final_primary = algo.cache().order(&client_order_id).unwrap();
5125 assert_eq!(final_primary.quantity(), Quantity::from("0.3"));
5126 assert!(algo.core.spawn_fill_debt(client_order_id).is_none());
5127 assert_eq!(
5128 spawned_order.filled_qty() + child_b.filled_qty() + final_primary.quantity(),
5129 Quantity::from("1.0"),
5130 );
5131 }
5132
5133 #[rstest]
5134 fn test_late_fill_on_child_after_partial_debt_discharge_nets_from_primary() {
5135 let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
5136 cancel_spawned_order(&mut algo, &mut spawned_order);
5137
5138 let mut child_b = spawn_reduced_child(&mut algo, client_order_id, Quantity::from("0.8"));
5139 accept_spawned_order(&mut algo, &mut child_b);
5140 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.5"));
5141 assert_eq!(
5142 algo.core.spawn_fill_debt(client_order_id),
5143 Some(Quantity::from("0.3")),
5144 );
5145
5146 cancel_spawned_order(&mut algo, &mut child_b);
5149 assert_eq!(
5150 algo.cache().order(&client_order_id).unwrap().quantity(),
5151 Quantity::from("0.5"),
5152 );
5153 assert!(algo.core.spawn_fill_debt(client_order_id).is_none());
5154 assert_eq!(
5155 algo.core
5156 .spawn_reduction(child_b.client_order_id())
5157 .unwrap()
5158 .restored_qty
5159 .unwrap(),
5160 Quantity::from("0.8"),
5161 );
5162
5163 fill_spawned_order(&mut algo, &mut child_b, Quantity::from("0.3"));
5164 let final_primary = algo.cache().order(&client_order_id).unwrap();
5165 assert_eq!(final_primary.quantity(), Quantity::from("0.2"));
5166 assert_eq!(
5167 spawned_order.filled_qty() + child_b.filled_qty() + final_primary.quantity(),
5168 Quantity::from("1.0"),
5169 );
5170 }
5171
5172 #[rstest]
5173 fn test_emulated_spawn_refusal_restores_initialized_primary() {
5174 let (mut algo, client_order_id, _spawned_order) = setup_pending_spawn();
5175 let mut primary = algo.cache().order(&client_order_id).unwrap();
5176 let spawned = algo.spawn_limit(
5177 &mut primary,
5178 Quantity::from("0.3"),
5179 Price::from("50000.0"),
5180 TimeInForce::Gtc,
5181 None,
5182 false,
5183 false,
5184 None,
5185 Some(TriggerType::BidAsk),
5186 None,
5187 true,
5188 );
5189 assert_eq!(
5190 algo.cache().order(&client_order_id).unwrap().quantity(),
5191 Quantity::from("0.2"),
5192 );
5193
5194 let result = algo.submit_order(OrderAny::Limit(spawned), None, None);
5195
5196 assert!(result.is_err());
5197 assert_eq!(
5198 algo.cache().order(&client_order_id).unwrap().quantity(),
5199 Quantity::from("0.5"),
5200 );
5201 }
5202
5203 #[rstest]
5204 fn test_spawned_order_accepted_then_expired_restores_reduction() {
5205 let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
5206
5207 expire_spawned_order(&mut algo, &mut spawned_order);
5208
5209 let final_primary = algo.cache().order(&client_order_id).unwrap();
5210 assert_eq!(final_primary.quantity(), Quantity::from("1.0"));
5211 }
5212
5213 #[rstest]
5214 fn test_partially_filled_spawned_order_canceled_restores_leaves_quantity() {
5215 let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
5216 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.2"));
5217
5218 cancel_spawned_order(&mut algo, &mut spawned_order);
5219
5220 let final_primary = algo.cache().order(&client_order_id).unwrap();
5221 assert_eq!(final_primary.quantity(), Quantity::from("0.8"));
5222 }
5223
5224 #[rstest]
5225 fn test_partially_filled_spawned_order_expired_restores_leaves_quantity() {
5226 let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
5227 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.2"));
5228
5229 expire_spawned_order(&mut algo, &mut spawned_order);
5230
5231 let final_primary = algo.cache().order(&client_order_id).unwrap();
5232 assert_eq!(final_primary.quantity(), Quantity::from("0.8"));
5233 }
5234
5235 #[rstest]
5236 fn test_fully_filled_spawned_order_consumes_reduction_without_restoration() {
5237 let (mut algo, client_order_id, mut spawned_order) = setup_accepted_spawn();
5238 let spawned_id = spawned_order.client_order_id();
5239
5240 fill_spawned_order(&mut algo, &mut spawned_order, Quantity::from("0.5"));
5241
5242 let final_primary = algo.cache().order(&client_order_id).unwrap();
5243 assert_eq!(final_primary.quantity(), Quantity::from("0.5"));
5244 assert!(algo.core.take_pending_spawn_reduction(spawned_id).is_none());
5245 }
5246
5247 #[rstest]
5248 #[case::denied(true)]
5249 #[case::rejected(false)]
5250 fn test_spawned_order_refusal_then_terminal_event_restores_only_once(#[case] denied: bool) {
5251 let (mut algo, client_order_id, spawned_order) = setup_pending_spawn();
5256
5257 if denied {
5258 let event = OrderDeniedSpec::builder()
5259 .trader_id(spawned_order.trader_id())
5260 .strategy_id(spawned_order.strategy_id())
5261 .instrument_id(spawned_order.instrument_id())
5262 .client_order_id(spawned_order.client_order_id())
5263 .reason("TEST_DENIAL".into())
5264 .build();
5265 algo.core
5266 .cache_rc()
5267 .borrow_mut()
5268 .update_order(&OrderEventAny::Denied(event))
5269 .unwrap();
5270 algo.handle_order_event(OrderEventAny::Denied(event));
5271 } else {
5272 let event = OrderRejectedSpec::builder()
5273 .trader_id(spawned_order.trader_id())
5274 .strategy_id(spawned_order.strategy_id())
5275 .instrument_id(spawned_order.instrument_id())
5276 .client_order_id(spawned_order.client_order_id())
5277 .account_id(AccountId::from("BINANCE-001"))
5278 .reason("TEST_REJECTION".into())
5279 .build();
5280 algo.core
5281 .cache_rc()
5282 .borrow_mut()
5283 .update_order(&OrderEventAny::Rejected(event))
5284 .unwrap();
5285 algo.handle_order_event(OrderEventAny::Rejected(event));
5286 }
5287
5288 assert_eq!(
5289 algo.cache().order(&client_order_id).unwrap().quantity(),
5290 Quantity::from("1.0"),
5291 );
5292
5293 let canceled = OrderCanceledSpec::builder()
5294 .trader_id(spawned_order.trader_id())
5295 .strategy_id(spawned_order.strategy_id())
5296 .instrument_id(spawned_order.instrument_id())
5297 .client_order_id(spawned_order.client_order_id())
5298 .build();
5299 algo.handle_order_event(OrderEventAny::Canceled(canceled));
5300
5301 let final_primary = algo.cache().order(&client_order_id).unwrap();
5302 assert_eq!(final_primary.quantity(), Quantity::from("1.0"));
5303 }
5304
5305 #[rstest]
5306 #[should_panic(expected = "exceeds primary leaves_qty")]
5307 fn test_spawn_quantity_exceeds_leaves_qty_panics() {
5308 let mut algo = create_test_algorithm();
5309 register_algorithm(&mut algo);
5310
5311 let instrument_id = InstrumentId::from("BTC/USDT.BINANCE");
5312 let exec_algorithm_id = algo.id();
5313 let client_order_id = ClientOrderId::from("O-001");
5314
5315 let mut primary = OrderAny::Market(MarketOrder::new(
5316 TraderId::from("TRADER-001"),
5317 StrategyId::from("STRAT-001"),
5318 instrument_id,
5319 client_order_id,
5320 OrderSide::Buy,
5321 Quantity::from("1.0"),
5322 TimeInForce::Gtc,
5323 UUID4::new(),
5324 0.into(),
5325 false,
5326 false,
5327 None,
5328 None,
5329 None,
5330 None,
5331 Some(exec_algorithm_id),
5332 None,
5333 Some(client_order_id),
5334 None,
5335 ));
5336
5337 {
5338 let cache_rc = algo.core.cache_rc();
5339 let mut cache = cache_rc.borrow_mut();
5340 cache.add_order(primary.clone(), None, None, false).unwrap();
5341 }
5342
5343 let _ = algo.spawn_market(
5344 &mut primary,
5345 Quantity::from("0.8"),
5346 TimeInForce::Fok,
5347 false,
5348 None,
5349 true,
5350 );
5351
5352 assert_eq!(primary.quantity(), Quantity::from("0.2"));
5353 assert_eq!(primary.leaves_qty(), Quantity::from("0.2"));
5354
5355 let _ = algo.spawn_market(
5357 &mut primary,
5358 Quantity::from("0.5"),
5359 TimeInForce::Fok,
5360 false,
5361 None,
5362 true,
5363 );
5364 }
5365}