1#![warn(rustc::all)]
22#![warn(clippy::pedantic)]
23#![allow(
24 clippy::manual_let_else,
25 clippy::option_if_let_else,
26 clippy::or_fun_call,
27 clippy::redundant_else,
28 clippy::similar_names,
29 clippy::single_match_else,
30 reason = "workspace policy; generated by scripts/update-clippy-allowances.py"
31)]
32#![deny(unsafe_code)]
33#![deny(unsafe_op_in_unsafe_fn)]
34#![deny(nonstandard_style)]
35#![deny(missing_debug_implementations)]
36#![deny(rustdoc::broken_intra_doc_links)]
37#![allow(
38 clippy::missing_panics_doc,
39 reason = "test support reports invalid setup and failed assertions by panicking"
40)]
41
42use std::{cell::RefCell, fmt::Debug, rc::Rc, time::Duration};
43
44use nautilus_common::{
45 cache::Cache,
46 clients::ExecutionClient,
47 clock::{Clock, VirtualClock},
48 live::{
49 dst,
50 runner::{replace_data_event_sender, replace_exec_event_sender},
51 },
52 messages::{
53 ExecutionEvent,
54 execution::{
55 TradingCommand, cancel::CancelOrder, modify::ModifyOrder, submit::SubmitOrder,
56 },
57 },
58 msgbus::{self, MessageBus, MessagingSwitchboard},
59};
60use nautilus_core::{UUID4, UnixNanos};
61use nautilus_execution::engine::{ExecutionEngine, config::ExecutionEngineConfig};
62use nautilus_model::{
63 events::{OrderEventAny, OrderPendingCancel, OrderPendingUpdate},
64 identifiers::{AccountId, ClientId, InstrumentId, StrategyId, TraderId},
65 instruments::{Instrument, InstrumentAny},
66 orders::{Order, OrderAny},
67 reports::ExecutionMassStatus,
68 types::{Price, Quantity},
69};
70use nautilus_portfolio::Portfolio;
71use nautilus_risk::engine::{RiskEngine, config::RiskEngineConfig};
72use nautilus_testkit::testers::{ExecTester, ExecTesterConfig};
73use nautilus_trading::strategy::StrategyNative;
74
75use crate::runner::AsyncRunner;
76
77pub struct ExecutionHarness {
79 clock: Rc<RefCell<dyn Clock>>,
80 cache: Rc<RefCell<Cache>>,
81 risk_engine: Rc<RefCell<RiskEngine>>,
82 exec_engine: Rc<RefCell<ExecutionEngine>>,
83 exec_rx: tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
84 routed: Vec<RoutedKind>,
85 trader_id: TraderId,
86 client_id: ClientId,
87 account_id: AccountId,
88 instrument_id: InstrumentId,
89}
90
91impl Debug for ExecutionHarness {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 f.debug_struct(stringify!(ExecutionHarness))
94 .field("trader_id", &self.trader_id)
95 .field("client_id", &self.client_id)
96 .field("account_id", &self.account_id)
97 .field("instrument_id", &self.instrument_id)
98 .field("routed", &self.routed)
99 .finish_non_exhaustive()
100 }
101}
102
103impl ExecutionHarness {
104 #[must_use]
108 pub fn new(
109 trader_id: TraderId,
110 client_id: ClientId,
111 account_id: AccountId,
112 instrument: InstrumentAny,
113 ) -> Self {
114 let _bus = MessageBus::new(trader_id, UUID4::new(), None, None).register_message_bus();
115 let clock: Rc<RefCell<dyn Clock>> = Rc::new(RefCell::new(VirtualClock::new()));
116 let cache = Rc::new(RefCell::new(Cache::default()));
117 let instrument_id = instrument.id();
118 cache
119 .borrow_mut()
120 .add_instrument(instrument)
121 .expect("instrument should be added to the harness cache");
122
123 let portfolio = Portfolio::new(clock.clone(), cache.clone(), None);
124
125 let risk_engine = Rc::new(RefCell::new(RiskEngine::new(
126 RiskEngineConfig::default(),
127 portfolio,
128 clock.clone(),
129 cache.clone(),
130 )));
131 RiskEngine::register_msgbus_handlers(&risk_engine);
132
133 let exec_config = ExecutionEngineConfig::builder()
134 .manage_own_order_books(true)
135 .build()
136 .expect("execution engine config should be valid");
137
138 let exec_engine = Rc::new(RefCell::new(ExecutionEngine::new(
139 clock.clone(),
140 cache.clone(),
141 Some(exec_config),
142 )));
143 ExecutionEngine::register_msgbus_handlers(&exec_engine);
144
145 let (exec_tx, exec_rx) = tokio::sync::mpsc::unbounded_channel();
146 replace_exec_event_sender(exec_tx);
147 let (data_tx, _data_rx) = tokio::sync::mpsc::unbounded_channel();
148 replace_data_event_sender(data_tx);
149
150 Self {
151 clock,
152 cache,
153 risk_engine,
154 exec_engine,
155 exec_rx,
156 routed: Vec::new(),
157 trader_id,
158 client_id,
159 account_id,
160 instrument_id,
161 }
162 }
163
164 #[must_use]
166 pub const fn clock(&self) -> &Rc<RefCell<dyn Clock>> {
167 &self.clock
168 }
169
170 #[must_use]
172 pub const fn cache(&self) -> &Rc<RefCell<Cache>> {
173 &self.cache
174 }
175
176 #[must_use]
178 pub const fn risk_engine(&self) -> &Rc<RefCell<RiskEngine>> {
179 &self.risk_engine
180 }
181
182 #[must_use]
184 pub const fn exec_engine(&self) -> &Rc<RefCell<ExecutionEngine>> {
185 &self.exec_engine
186 }
187
188 #[must_use]
190 pub const fn trader_id(&self) -> TraderId {
191 self.trader_id
192 }
193
194 #[must_use]
196 pub const fn client_id(&self) -> ClientId {
197 self.client_id
198 }
199
200 #[must_use]
202 pub const fn account_id(&self) -> AccountId {
203 self.account_id
204 }
205
206 #[must_use]
208 pub const fn instrument_id(&self) -> InstrumentId {
209 self.instrument_id
210 }
211
212 #[must_use]
214 pub fn routed(&self) -> &[RoutedKind] {
215 &self.routed
216 }
217
218 pub fn assert_engine_ready(&self) {
220 let engine = self.exec_engine.borrow();
221 assert!(engine.get_client(&self.client_id).is_some());
222 assert!(engine.check_integrity());
223 assert!(engine.check_connected());
224 }
225
226 #[must_use]
228 pub fn risk_command_count(&self) -> u64 {
229 self.risk_engine.borrow().command_count()
230 }
231
232 pub fn register_client(&self, client: Box<dyn ExecutionClient>) -> anyhow::Result<()> {
238 let client_id = client.client_id();
239 let venue = client.venue();
240 let mut engine = self.exec_engine.borrow_mut();
241 engine.register_client(client)?;
242 if let Err(e) = engine.register_venue_routing(client_id, venue) {
243 engine.deregister_client(client_id)?;
244 return Err(e);
245 }
246
247 Ok(())
248 }
249
250 pub fn submit_via_risk(&self, order: &OrderAny) {
252 let cmd = SubmitOrder::from_order(
253 order,
254 self.trader_id,
255 Some(self.client_id),
256 None,
257 UUID4::new(),
258 UnixNanos::default(),
259 );
260 self.cache
261 .borrow_mut()
262 .add_order(order.clone(), None, Some(self.client_id), false)
263 .expect("order should be added to the harness cache");
264 msgbus::send_trading_command(
265 MessagingSwitchboard::risk_engine_execute(),
266 TradingCommand::SubmitOrder(cmd),
267 );
268 }
269
270 pub fn modify_via_risk(
272 &self,
273 order: &OrderAny,
274 price: Option<Price>,
275 quantity: Option<Quantity>,
276 ) {
277 self.mark_pending_update(order);
278 let venue_order_id = self
279 .cache
280 .borrow()
281 .order(&order.client_order_id())
282 .and_then(|cached| cached.venue_order_id());
283
284 let cmd = ModifyOrder::new(
285 self.trader_id,
286 Some(self.client_id),
287 order.strategy_id(),
288 order.instrument_id(),
289 order.client_order_id(),
290 venue_order_id,
291 quantity,
292 price,
293 None,
294 UUID4::new(),
295 UnixNanos::default(),
296 None,
297 None,
298 );
299 msgbus::send_trading_command(
300 MessagingSwitchboard::risk_engine_execute(),
301 TradingCommand::ModifyOrder(cmd),
302 );
303 }
304
305 pub fn cancel_via_execution(&self, order: &OrderAny) {
307 self.mark_pending_cancel(order);
308 let venue_order_id = self
309 .cache
310 .borrow()
311 .order(&order.client_order_id())
312 .and_then(|cached| cached.venue_order_id());
313
314 let cmd = CancelOrder::new(
315 self.trader_id,
316 Some(self.client_id),
317 order.strategy_id(),
318 order.instrument_id(),
319 order.client_order_id(),
320 venue_order_id,
321 UUID4::new(),
322 UnixNanos::default(),
323 None,
324 None,
325 );
326 msgbus::send_trading_command(
327 MessagingSwitchboard::exec_engine_queue_execute(),
328 TradingCommand::CancelOrder(cmd),
329 );
330 }
331
332 pub async fn pump_until(
334 &mut self,
335 timeout: Duration,
336 predicate: impl Fn(&Cache) -> bool,
337 ) -> bool {
338 self.pump_until_condition(timeout, |harness| predicate(&harness.cache.borrow()))
339 .await
340 }
341
342 pub async fn pump_until_routed(&mut self, timeout: Duration, kind: RoutedKind) -> bool {
344 self.pump_until_condition(timeout, |harness| harness.routed.contains(&kind))
345 .await
346 }
347
348 pub async fn pump_for(&mut self, duration: Duration) {
350 let deadline = dst::time::Instant::now() + duration;
351
352 while dst::time::Instant::now() < deadline {
353 let remaining = deadline.saturating_duration_since(dst::time::Instant::now());
354 match dst::time::timeout(remaining.min(EVENT_POLL_INTERVAL), self.exec_rx.recv()).await
355 {
356 Ok(Some(event)) => self.route_event(event),
357 Ok(None) => return,
358 Err(_) => dst::task::yield_now().await,
359 }
360 }
361 }
362
363 #[must_use]
365 pub fn register_exec_tester(&self, strategy_id: StrategyId, order_qty: Quantity) -> ExecTester {
366 let mut config =
367 ExecTesterConfig::new(strategy_id, self.instrument_id, self.client_id, order_qty);
368 config.subscribe_quotes = false;
369 config.subscribe_trades = false;
370 config.enable_limit_sells = false;
371 config.tob_offset_ticks = 1;
372 config.cancel_orders_on_stop = false;
373 config.close_positions_on_stop = false;
374
375 let mut tester = ExecTester::new(config);
376
377 let portfolio = Rc::new(RefCell::new(Portfolio::new(
378 self.clock.clone(),
379 self.cache.clone(),
380 None,
381 )));
382 StrategyNative::strategy_core_mut(&mut tester)
383 .register(
384 self.trader_id,
385 self.clock.clone(),
386 self.cache.clone(),
387 portfolio,
388 )
389 .expect("ExecTester should register against the harness");
390 tester
391 }
392
393 pub fn mark_pending_cancel(&self, order: &OrderAny) {
395 let cached = self.cached_order(order);
396 let ts_now = self.clock.borrow().timestamp_ns();
397 let event = OrderEventAny::PendingCancel(OrderPendingCancel::new(
398 cached.trader_id(),
399 cached.strategy_id(),
400 cached.instrument_id(),
401 cached.client_order_id(),
402 cached.account_id(),
403 UUID4::new(),
404 ts_now,
405 ts_now,
406 false,
407 cached.venue_order_id(),
408 ));
409 self.apply_pending_event(&event);
410 }
411
412 #[allow(
414 clippy::await_holding_refcell_ref,
415 reason = "single-threaded test harness only runs mock venue tasks during the await"
416 )]
417 pub async fn reconcile_from_venue(&self) -> ExecutionMassStatus {
418 let mass_status = self
419 .exec_engine
420 .borrow_mut()
421 .generate_mass_status(&self.client_id, None)
422 .await
423 .expect("mass-status request should succeed")
424 .expect("mass-status request should return a report");
425 self.exec_engine
426 .borrow_mut()
427 .reconcile_execution_mass_status(&mass_status);
428 mass_status
429 }
430
431 fn mark_pending_update(&self, order: &OrderAny) {
432 let cached = self.cached_order(order);
433 let ts_now = self.clock.borrow().timestamp_ns();
434 let event = OrderEventAny::PendingUpdate(OrderPendingUpdate::new(
435 cached.trader_id(),
436 cached.strategy_id(),
437 cached.instrument_id(),
438 cached.client_order_id(),
439 cached.account_id(),
440 UUID4::new(),
441 ts_now,
442 ts_now,
443 false,
444 cached.venue_order_id(),
445 ));
446 self.apply_pending_event(&event);
447 }
448
449 fn cached_order(&self, order: &OrderAny) -> OrderAny {
450 self.cache
451 .borrow()
452 .order(&order.client_order_id())
453 .map(|cached| cached.clone())
454 .expect("order must be cached before a pending transition")
455 }
456
457 fn apply_pending_event(&self, event: &OrderEventAny) {
458 self.cache
459 .borrow_mut()
460 .update_order(event)
461 .expect("pending event should update the cached order");
462 }
463
464 async fn pump_until_condition(
465 &mut self,
466 timeout: Duration,
467 predicate: impl Fn(&Self) -> bool,
468 ) -> bool {
469 let start = dst::time::Instant::now();
470
471 loop {
472 if predicate(self) {
473 return true;
474 }
475
476 if start.elapsed() >= timeout {
477 return false;
478 }
479
480 match dst::time::timeout(EVENT_POLL_INTERVAL, self.exec_rx.recv()).await {
481 Ok(Some(event)) => self.route_event(event),
482 Ok(None) => return predicate(self),
483 Err(_) => dst::task::yield_now().await,
484 }
485 }
486 }
487
488 fn route_event(&mut self, event: ExecutionEvent) {
489 self.routed.push(RoutedKind::of(&event));
490 AsyncRunner::handle_exec_event(event);
491 }
492}
493
494const EVENT_POLL_INTERVAL: Duration = Duration::from_millis(50);
495
496#[derive(Debug, Clone, Copy, PartialEq, Eq)]
498pub enum RoutedKind {
499 Order,
501 Report,
503 Account,
505}
506
507impl RoutedKind {
508 fn of(event: &ExecutionEvent) -> Self {
509 match event {
510 ExecutionEvent::Report(_) => Self::Report,
511 ExecutionEvent::Account(_) => Self::Account,
512 _ => Self::Order,
513 }
514 }
515}
516
517pub mod invariants {
519 use ahash::AHashSet;
520 use nautilus_common::cache::Cache;
521 use nautilus_model::{
522 enums::OrderStatus,
523 identifiers::{ClientOrderId, InstrumentId},
524 orderbook::own::should_handle_own_book_order,
525 orders::Order,
526 };
527 use rust_decimal::Decimal;
528
529 use super::RoutedKind;
530
531 pub fn assert_tracked_used_events(routed: &[RoutedKind]) {
533 assert!(
534 routed.contains(&RoutedKind::Order),
535 "tracked lifecycle routed no typed order event: {routed:?}",
536 );
537 let reports = routed
538 .iter()
539 .filter(|kind| **kind == RoutedKind::Report)
540 .count();
541 assert_eq!(
542 reports, 0,
543 "tracked happy path routed {reports} report(s), expected 0: {routed:?}",
544 );
545 }
546
547 pub fn assert_order_status(cache: &Cache, id: &ClientOrderId, expected: OrderStatus) {
549 let status = cache.order(id).map(|order| order.status());
550 assert_eq!(
551 status,
552 Some(expected),
553 "order {id} status was {status:?}, expected {expected:?}",
554 );
555 }
556
557 pub fn assert_own_book_consistent(cache: &Cache, instrument_id: &InstrumentId) {
565 let mut tracked_ids = AHashSet::new();
566
567 if let Some(book) = cache.own_order_book(instrument_id) {
568 let entries = book
569 .bids()
570 .chain(book.asks())
571 .flat_map(|level| level.orders.values());
572
573 for entry in entries {
574 let id = entry.client_order_id;
575
576 let Some(order) = cache.order(&id).filter(|order| !order.is_closed()) else {
577 panic!("own order book retains closed or missing order {id}");
578 };
579
580 assert_eq!(
582 format!("{entry:?}"),
583 format!("{:?}", order.to_own_book_order()),
584 "own order book entry for {id} diverges from the cached order",
585 );
586 tracked_ids.insert(id);
587 }
588 }
589
590 for order in cache.orders(None, Some(instrument_id), None, None, None) {
591 let id = order.client_order_id();
592 let eligible = !order.is_closed() && should_handle_own_book_order(&order);
593 assert!(
594 !eligible || tracked_ids.contains(&id),
595 "own order book is missing open order {id}",
596 );
597 }
598 }
599
600 pub fn assert_filled_qty(cache: &Cache, id: &ClientOrderId, expected: Decimal) {
602 let filled = cache.order(id).map(|order| order.filled_qty().as_decimal());
603 assert_eq!(
604 filled,
605 Some(expected),
606 "order {id} filled_qty was {filled:?}, expected {expected}",
607 );
608 }
609
610 pub fn assert_in_own_book(
612 cache: &Cache,
613 instrument_id: &InstrumentId,
614 id: &ClientOrderId,
615 expected: bool,
616 ) {
617 let present = cache
618 .own_order_book(instrument_id)
619 .is_some_and(|book| book.is_order_in_book(id));
620 assert_eq!(
621 present, expected,
622 "order {id} own-book membership was {present}, expected {expected}",
623 );
624 }
625}
626
627#[cfg(test)]
628mod tests {
629 use std::panic::{AssertUnwindSafe, catch_unwind};
630
631 use nautilus_common::cache::Cache;
632 use nautilus_execution::engine::stubs::StubExecutionClient;
633 use nautilus_model::{
634 enums::{OmsType, OrderSide, OrderStatus, OrderType},
635 identifiers::{AccountId, ClientId, ClientOrderId, TraderId, VenueOrderId},
636 instruments::{Instrument, stubs::audusd_sim},
637 orderbook::own::OwnOrderBook,
638 orders::{Order, OrderAny, OrderTestBuilder, stubs::TestOrderEventStubs},
639 types::{Price, Quantity},
640 };
641 use rstest::rstest;
642
643 use super::{ExecutionHarness, invariants};
644
645 #[derive(Clone, Copy, Debug)]
646 enum OwnBookFault {
647 Missing,
648 StaleSize,
649 Closed,
650 Unknown,
651 }
652
653 #[rstest]
654 #[case::occupied_route(false, "Venue SIM already routed to A, cannot re-route to B")]
655 #[case::duplicate_client(true, "Client already registered with ID A")]
656 fn test_registration_failure_preserves_client_and_route(
657 #[case] duplicate_id: bool,
658 #[case] expected: &str,
659 ) {
660 let instrument = audusd_sim();
661 let client_id = ClientId::from("A");
662 let account_id = AccountId::from("A-001");
663
664 let harness = ExecutionHarness::new(
665 TraderId::from("TRADER-001"),
666 client_id,
667 account_id,
668 instrument.clone().into(),
669 );
670 harness
671 .register_client(Box::new(StubExecutionClient::new(
672 client_id,
673 account_id,
674 instrument.id().venue,
675 OmsType::Netting,
676 None,
677 )))
678 .unwrap();
679
680 let replacement_id = if duplicate_id {
681 client_id
682 } else {
683 ClientId::from("B")
684 };
685
686 let error = harness
687 .register_client(Box::new(StubExecutionClient::new(
688 replacement_id,
689 AccountId::from("B-002"),
690 instrument.id().venue,
691 OmsType::Hedging,
692 None,
693 )))
694 .unwrap_err();
695 let order = OrderTestBuilder::new(OrderType::Market)
696 .instrument_id(instrument.id())
697 .quantity(Quantity::from(1))
698 .build();
699 let engine = harness.exec_engine().borrow();
700 let routed = engine.get_clients_for_orders(&[order]);
701
702 assert_eq!(error.to_string(), expected);
703 assert_eq!(engine.client_ids(), vec![client_id]);
704 assert_eq!(routed.len(), 1);
705 assert_eq!(routed[0].client_id(), client_id);
706 assert_eq!(routed[0].account_id(), account_id);
707 assert_eq!(routed[0].oms_type(), OmsType::Netting);
708 }
709
710 #[rstest]
711 #[case::consistent(None, None)]
712 #[case::missing(
713 Some(OwnBookFault::Missing),
714 Some("own order book is missing open order O-001")
715 )]
716 #[case::stale_size(
717 Some(OwnBookFault::StaleSize),
718 Some("own order book entry for O-001 diverges from the cached order")
719 )]
720 #[case::closed(
721 Some(OwnBookFault::Closed),
722 Some("own order book retains closed or missing order O-001")
723 )]
724 #[case::unknown(
725 Some(OwnBookFault::Unknown),
726 Some("own order book retains closed or missing order O-UNKNOWN")
727 )]
728 fn test_assert_own_book_consistent_detects_divergence(
729 #[case] fault: Option<OwnBookFault>,
730 #[case] expected: Option<&str>,
731 ) {
732 let (mut cache, order) = cache_with_accepted_order(false);
733 let instrument_id = order.instrument_id();
734 let entry = order.to_own_book_order();
735
736 match fault {
737 None => {}
738 Some(OwnBookFault::Missing) => {
739 let own_book = cache.own_order_book_mut(&instrument_id).unwrap();
740 own_book.delete(entry).unwrap();
741 }
742 Some(OwnBookFault::StaleSize) => {
743 let mut stale = entry;
744 stale.size = Quantity::from(50_000);
745 let own_book = cache.own_order_book_mut(&instrument_id).unwrap();
746 own_book.update(stale).unwrap();
747 }
748 Some(OwnBookFault::Closed) => {
749 let account_id = order.account_id().unwrap();
750 let canceled = TestOrderEventStubs::canceled(&order, account_id, None);
751 cache.update_order(&canceled).unwrap();
752 let own_book = cache.own_order_book_mut(&instrument_id).unwrap();
753 own_book.add(entry);
754 }
755 Some(OwnBookFault::Unknown) => {
756 let mut unknown = entry;
757 unknown.client_order_id = ClientOrderId::from("O-UNKNOWN");
758 let own_book = cache.own_order_book_mut(&instrument_id).unwrap();
759 own_book.add(unknown);
760 }
761 }
762
763 let result = catch_unwind(AssertUnwindSafe(|| {
764 invariants::assert_own_book_consistent(&cache, &instrument_id);
765 }));
766
767 let message = result.err().map(|payload| {
768 payload
769 .downcast_ref::<String>()
770 .cloned()
771 .unwrap_or_default()
772 });
773
774 match expected {
775 None => assert!(message.is_none(), "unexpected panic: {message:?}"),
776 Some(expected) => assert!(
777 message.as_deref().is_some_and(|m| m.contains(expected)),
778 "panic message {message:?} lacks {expected:?}",
779 ),
780 }
781 }
782
783 #[rstest]
784 fn test_assert_own_book_consistent_ignores_ineligible_open_order() {
785 let (cache, order) = cache_with_accepted_order(true);
786 let instrument_id = order.instrument_id();
787 let own_book = cache.own_order_book(&instrument_id).unwrap();
788 let in_own_book = own_book.is_order_in_book(&order.client_order_id());
789
790 invariants::assert_own_book_consistent(&cache, &instrument_id);
791
792 assert_eq!(order.status(), OrderStatus::Accepted);
793 assert!(!in_own_book);
794 }
795
796 fn cache_with_accepted_order(quote_quantity: bool) -> (Cache, OrderAny) {
797 let instrument = audusd_sim();
798 let instrument_id = instrument.id();
799 let account_id = AccountId::from("SIM-001");
800 let mut cache = Cache::default();
801 cache.add_instrument(instrument.into()).unwrap();
802 cache
803 .add_own_order_book(OwnOrderBook::new(instrument_id))
804 .unwrap();
805
806 let order = OrderTestBuilder::new(OrderType::Limit)
807 .instrument_id(instrument_id)
808 .client_order_id(ClientOrderId::from("O-001"))
809 .side(OrderSide::Buy)
810 .quantity(Quantity::from(100_000))
811 .price(Price::from("1.00000"))
812 .quote_quantity(quote_quantity)
813 .build();
814 cache.add_order(order.clone(), None, None, false).unwrap();
815 let order = cache
816 .update_order(&TestOrderEventStubs::submitted(&order, account_id))
817 .unwrap();
818 let order = cache
819 .update_order(&TestOrderEventStubs::accepted(
820 &order,
821 account_id,
822 VenueOrderId::from("V-001"),
823 ))
824 .unwrap();
825
826 (cache, order)
827 }
828}