1use std::{
26 borrow::Cow,
27 cell::RefCell,
28 rc::Rc,
29 sync::{
30 Arc,
31 atomic::{AtomicBool, Ordering},
32 },
33 time::Duration,
34};
35
36use ahash::{AHashMap, AHashSet};
37use alloy_primitives::U256;
38use anyhow::Context;
39use async_trait::async_trait;
40use indexmap::IndexMap;
41use nautilus_common::{
42 cache::ORDER_NOT_FOUND,
43 clients::ExecutionClient,
44 enums::LogLevel,
45 live::{
46 dst::time::{self, Instant},
47 runner::{get_exec_event_sender, try_get_data_event_sender, try_get_exec_event_sender},
48 },
49 messages::{
50 DataEvent, ExecutionReport,
51 execution::{
52 BatchCancelOrders, CancelAllOrders, CancelOrder, GenerateFillReports,
53 GenerateOrderStatusReport, GenerateOrderStatusReports, GeneratePositionStatusReports,
54 ModifyOrder, QueryAccount, QueryOrder, SubmitOrder, SubmitOrderList,
55 },
56 },
57 runner::{TimeEventMessage, TimeEventSender, try_get_time_event_sender},
58 timer::{TimeEvent, TimeEventCallback},
59};
60use nautilus_core::{
61 AtomicMap, DurationNanos, Params, UUID4, UnixNanos,
62 string::secret::SecretString,
63 time::{AtomicTime, get_atomic_clock_realtime},
64};
65use nautilus_live::{
66 ExecutionClientCore, ExecutionEventEmitter, SocketControl,
67 execution::{failure::CommandFailure, reports::retain_order_status_reports},
68 task::{TaskGroup, TaskGroupGuard},
69};
70use nautilus_model::{
71 accounts::AccountAny,
72 data::QuoteTick,
73 enums::{OmsType, OrderSide, OrderStatus, OrderType, PositionSide},
74 events::{
75 OrderAccepted, OrderCanceled, OrderDeniedReason, OrderEventAny, OrderExpired, OrderFilled,
76 OrderRejected, OrderUpdated,
77 },
78 identifiers::{
79 AccountId, ClientId, ClientOrderId, InstrumentId, StrategyId, Venue, VenueOrderId,
80 },
81 instruments::{Instrument, InstrumentAny},
82 orders::{Order, OrderAny},
83 reports::{ExecutionMassStatus, FillReport, OrderStatusReport, PositionStatusReport},
84 types::{AccountBalance, Currency, MarginBalance, Price, Quantity},
85};
86use parking_lot::Mutex;
87use rust_decimal::Decimal;
88use tokio_util::sync::CancellationToken;
89use ustr::Ustr;
90
91use crate::{
92 common::{
93 consts::{
94 DECIMAL_SCALE, DERIVE_ACCOUNT_REGISTRATION_TIMEOUT_SECS, DERIVE_VENUE,
95 MIN_SIGNATURE_TTL, TRIGGER_ORDER_SIGNATURE_TTL, WS_REQUEST_TIMEOUT,
96 },
97 credential::DeriveCredential,
98 enums::{DeriveInstrumentType, DeriveOrderSide, DeriveOrderStatus},
99 parse::{
100 STRATEGY_REASON_MAX_CHARS, derive_order_type_to_nautilus_for_order,
101 derive_rejection_due_post_only, derive_status_to_nautilus, format_instrument_id,
102 format_venue_symbol, parse_derive_instrument_any, strategy_rejection_reason,
103 },
104 retry::{http_retry_config, is_write_outcome_ambiguous_ws},
105 },
106 config::DeriveExecutionClientConfig,
107 http::{
108 DeriveCredentials, DeriveHttpClient, DeriveHttpError,
109 models::{
110 DeriveInstrument, DeriveOrder, DeriveOrderResult, DeriveReplaceOutcome,
111 DeriveSubaccount, DeriveTrade,
112 },
113 parse::{
114 CommissionError, parse_derive_order_to_report_with_precision,
115 parse_derive_position_to_report_with_precision, parse_derive_subaccount_to_balances,
116 parse_derive_trade_to_fill_report_with_precision,
117 },
118 query::{
119 DeriveCancelByInstrumentParams, DeriveCancelByLabelParams, DeriveCancelParams,
120 DeriveCancelTriggerOrderParams, DeriveGetOpenOrdersParams, DeriveGetOrderHistoryParams,
121 DeriveGetOrderParams, DeriveGetPositionsParams, DeriveGetSubaccountParams,
122 DeriveGetTradeHistoryParams, DeriveGetTriggerOrdersParams, PaginationCursor,
123 order_replace_to_derive_payload, order_to_derive_payload,
124 trigger_order_to_derive_payload, validate_order_support,
125 validate_trigger_order_support,
126 },
127 },
128 signing::{
129 context::{SigningContext, resolve_signing_context},
130 encoding::decimal_to_scaled_u256,
131 nonce::{NONCE_FUTURE_NS, NonceError, NonceManager},
132 },
133 websocket::{
134 DeriveOrdersSubscriptionData, DeriveTradesSubscriptionData, DeriveWebSocketClient,
135 DeriveWsChannel, DeriveWsCredentials, DeriveWsError, DeriveWsExecutionHandle,
136 DeriveWsMessage, OrderIdentity, WsDispatchState,
137 dispatch::{OrderBinding, ReplaceCompletion},
138 parse::parse_ticker_quote_from_rest,
139 },
140};
141
142const DERIVE_PRIVATE_PAGE_SIZE: u32 = 500;
143
144#[derive(Debug)]
151pub struct DeriveExecutionClient {
152 core: ExecutionClientCore,
153 clock: &'static AtomicTime,
154 config: DeriveExecutionClientConfig,
155 credential: DeriveCredential,
156 emitter: ExecutionEventEmitter,
157 http_client: DeriveHttpClient,
158 ws_client: DeriveWebSocketClient,
159 ws_exec: DeriveWsExecutionHandle,
160 instruments: Arc<AtomicMap<InstrumentId, DeriveInstrument>>,
161 nonce_manager: Arc<NonceManager>,
162 signing: SigningContext,
163 is_connected: Arc<AtomicBool>,
164 cancellation_token: CancellationToken,
165 session_tasks: TaskGroup,
166 pending_tasks: TaskGroup,
167 shutdown_errors: Vec<String>,
168 dispatch_state: Arc<WsDispatchState>,
169 reconciliation_request: Mutex<Option<ReconciliationSnapshotRequest>>,
170}
171
172impl DeriveExecutionClient {
173 pub fn new(
189 core: ExecutionClientCore,
190 config: DeriveExecutionClientConfig,
191 ) -> anyhow::Result<Self> {
192 config.validate()?;
193
194 let credential = DeriveCredential::resolve(
195 config.wallet_address.clone(),
196 config.session_key.clone().map(SecretString::into_inner),
197 config.subaccount_id,
198 config.environment,
199 )?;
200
201 let http_credentials = DeriveCredentials::new(
202 credential.wallet_address().to_string(),
203 credential.session_key(),
204 )
205 .context("failed to build Derive HTTP credentials")?;
206 let retry_config = http_retry_config(
207 config.max_retries,
208 config.retry_delay_initial_ms,
209 config.retry_delay_max_ms,
210 );
211 let proxy_url = config
212 .proxy_url
213 .as_ref()
214 .map(|value| value.expose_secret().to_owned());
215 let http_client = DeriveHttpClient::with_credentials(
216 config.rest_url(),
217 http_credentials,
218 Some(config.http_timeout_secs),
219 proxy_url.clone(),
220 Some(retry_config),
221 )
222 .context("failed to create Derive HTTP client")?;
223
224 let ws_credentials = DeriveWsCredentials::new(
225 credential.wallet_address().to_string(),
226 credential.session_key(),
227 )
228 .context("failed to build Derive WebSocket credentials")?;
229 let mut ws_client = DeriveWebSocketClient::with_credentials(
230 Some(config.ws_url()),
231 config.environment,
232 config.transport_backend,
233 proxy_url,
234 ws_credentials,
235 config.max_matching_requests_per_second,
236 config.max_per_instrument_matching_requests_per_second,
237 )
238 .with_socket_control(SocketControl::new(
239 core.client_id,
240 Some(*DERIVE_VENUE),
241 "derive-user-streams",
242 ));
243
244 if let Some(secs) = config.ws_timeout_secs {
245 ws_client.set_request_timeout(Duration::from_secs(secs));
246 }
247
248 let ws_exec = ws_client.execution_handle();
251
252 let signing = resolve_signing_context(&credential, &config)?;
253
254 let clock = get_atomic_clock_realtime();
255
256 let mut emitter = ExecutionEventEmitter::new(
257 clock,
258 core.trader_id,
259 core.account_id,
260 core.account_type,
261 core.base_currency,
262 );
263
264 if let Some(sender) = try_get_exec_event_sender() {
265 emitter.set_sender(sender);
266 }
267
268 let session_tasks = TaskGroup::new();
269 let pending_tasks = TaskGroup::new();
270
271 let dispatch_state = Arc::new(WsDispatchState::new(credential.subaccount_id()));
272
273 Ok(Self {
274 core,
275 clock,
276 config,
277 credential,
278 emitter,
279 http_client,
280 ws_client,
281 ws_exec,
282 instruments: Arc::new(AtomicMap::new()),
283 nonce_manager: Arc::new(NonceManager::new()),
284 signing,
285 is_connected: Arc::new(AtomicBool::new(false)),
286 cancellation_token: CancellationToken::new(),
287 session_tasks,
288 pending_tasks,
289 shutdown_errors: Vec::new(),
290 dispatch_state,
291 reconciliation_request: Mutex::new(None),
292 })
293 }
294
295 #[must_use]
297 pub const fn subaccount_id(&self) -> u64 {
298 self.credential.subaccount_id()
299 }
300
301 #[must_use]
303 pub fn config(&self) -> &DeriveExecutionClientConfig {
304 &self.config
305 }
306
307 #[must_use]
309 pub fn http_client(&self) -> &DeriveHttpClient {
310 &self.http_client
311 }
312
313 pub fn cache_instrument(&self, instrument: DeriveInstrument) -> anyhow::Result<()> {
321 let instrument_id = format_instrument_id(instrument.instrument_name)?;
322 anyhow::ensure!(
323 instrument.tick_size > Decimal::ZERO,
324 "Derive tick_size must be positive"
325 );
326 anyhow::ensure!(
327 instrument.amount_step > Decimal::ZERO,
328 "Derive amount_step must be positive"
329 );
330 let price_increment =
331 Price::from_decimal(instrument.tick_size).context("invalid Derive tick_size")?;
332 let size_increment =
333 Quantity::from_decimal(instrument.amount_step).context("invalid Derive amount_step")?;
334 self.dispatch_state.register_instrument_precision(
335 instrument_id,
336 price_increment.precision,
337 size_increment.precision,
338 );
339 self.instruments.insert(instrument_id, instrument);
340 Ok(())
341 }
342
343 fn spawn_task<F>(&self, description: impl Into<Cow<'static, str>>, fut: F)
345 where
346 F: std::future::Future<Output = anyhow::Result<()>> + Send + 'static,
347 {
348 let description = description.into();
349 let task_description = description.clone();
350
351 let future = async move {
352 if fut.await.is_err() {
353 log::warn!("{task_description} failed");
354 }
355 };
356
357 if let Err(e) = self.pending_tasks.spawn(future) {
358 log::warn!("Skipping Derive {description} after shutdown began: {e}");
359 }
360 }
361
362 fn abort_pending_tasks(&self) {
363 self.pending_tasks.begin_shutdown();
364 }
365
366 fn abort_session_tasks(&self) {
367 let request = self.reconciliation_request.lock().take();
368 if let Some(request) = request {
369 request.close();
370 }
371
372 self.session_tasks.begin_shutdown();
373 self.ws_client.begin_shutdown();
374 }
375
376 async fn await_pending_tasks(&self) -> anyhow::Result<()> {
377 self.pending_tasks.begin_shutdown();
378 self.pending_tasks
379 .finish_shutdown(Duration::from_secs(1), Duration::from_secs(2))
380 .await
381 .map_err(|e| anyhow::anyhow!("Failed to terminate Derive execution tasks: {e}"))?;
382 Ok(())
383 }
384
385 async fn await_session_tasks(&self) -> anyhow::Result<()> {
386 self.session_tasks.begin_shutdown();
387 self.session_tasks
388 .finish_shutdown(Duration::from_secs(1), Duration::from_secs(2))
389 .await
390 .map_err(|e| {
391 anyhow::anyhow!("Failed to terminate Derive execution session tasks: {e}")
392 })?;
393
394 Ok(())
395 }
396
397 async fn ensure_instruments_initialized(&self) -> anyhow::Result<()> {
398 if self.core.instruments_initialized() {
399 return Ok(());
400 }
401
402 let instruments: Vec<_> = self
403 .core
404 .cache()
405 .instruments(&DERIVE_VENUE, None)
406 .into_iter()
407 .cloned()
408 .collect();
409
410 for instrument in instruments {
411 self.cache_native_instrument(&instrument)?;
412 }
413
414 self.core.set_instruments_initialized();
415 Ok(())
416 }
417
418 async fn initialize_portfolio(&self) -> anyhow::Result<()> {
419 let snapshot = self
420 .http_client
421 .get_subaccount(&DeriveGetSubaccountParams::new(self.subaccount_id()))
422 .await?;
423 self.reconciliation_context()
424 .emit_account_snapshot(&snapshot)?;
425 let triggers = self
426 .http_client
427 .get_trigger_orders(&DeriveGetTriggerOrdersParams::new(self.subaccount_id()))
428 .await?;
429 let mut names: AHashSet<Ustr> = snapshot
430 .open_orders
431 .iter()
432 .chain(triggers.orders.iter())
433 .map(|order| order.instrument_name)
434 .chain(
435 snapshot
436 .positions
437 .iter()
438 .map(|position| position.instrument_name),
439 )
440 .collect();
441 {
442 let cache = self.core.cache();
443 for order in cache.orders_refs(
444 Some(&DERIVE_VENUE),
445 None,
446 None,
447 Some(&self.core.account_id),
448 None,
449 ) {
450 if (order.is_open() || order.is_inflight())
451 && cache.client_id(&order.client_order_id()) == Some(&self.core.client_id)
452 {
453 names.insert(format_venue_symbol(&order.instrument_id())?);
454 }
455 }
456 }
457
458 let mut names: Vec<_> = names.into_iter().collect();
459 names.sort_unstable();
460 let mut required = Vec::with_capacity(names.len());
461 for name in names {
462 let id = format_instrument_id(name)?;
463 required.push(id);
464 if self.core.cache().instrument(&id).is_some() {
465 continue;
466 }
467
468 let raw = cached_or_fetch_instrument(
469 &self.http_client,
470 &self.instruments,
471 &id,
472 name.as_str(),
473 )
474 .await?;
475 let instrument = parse_derive_instrument_any(&raw, self.clock.get_time_ns())?
476 .context("unsupported required Derive portfolio instrument")?;
477 self.cache_instrument(raw)?;
478 let sender = try_get_data_event_sender()
479 .context("Derive instrument event sender is unavailable")?;
480 sender
481 .send(DataEvent::Instrument(instrument))
482 .map_err(|e| {
483 anyhow::anyhow!("Failed to publish required Derive instrument {id}: {e}")
484 })?;
485 }
486
487 self.await_instruments_registered(&required).await?;
488 self.await_account_registered(DERIVE_ACCOUNT_REGISTRATION_TIMEOUT_SECS)
489 .await?;
490 Ok(())
491 }
492
493 async fn await_instruments_registered(&self, required: &[InstrumentId]) -> anyhow::Result<()> {
494 let started = Instant::now();
495 let timeout = Duration::from_secs_f64(DERIVE_ACCOUNT_REGISTRATION_TIMEOUT_SECS);
496
497 loop {
498 if required
499 .iter()
500 .all(|id| self.core.cache().instrument(id).is_some())
501 {
502 return Ok(());
503 }
504
505 anyhow::ensure!(
506 started.elapsed() < timeout,
507 "Timeout waiting for required Derive instruments to be registered"
508 );
509 time::sleep(Duration::from_millis(10)).await;
510 }
511 }
512
513 fn cache_native_instrument(&self, instrument: &InstrumentAny) -> anyhow::Result<()> {
514 let info = instrument
515 .info()
516 .context("Derive instrument has no venue metadata")?;
517 let definition: DeriveInstrument = serde_json::from_value(serde_json::to_value(info)?)
518 .context("invalid Derive instrument metadata")?;
519 anyhow::ensure!(
520 format_instrument_id(definition.instrument_name)? == instrument.id(),
521 "Derive metadata identity does not match {}",
522 instrument.id(),
523 );
524 self.cache_instrument(definition)?;
525 Ok(())
526 }
527
528 fn reconciliation_context(&self) -> DeriveReconciliationContext {
529 let (orders, terminal_revision) = cached_order_bindings(&self.core, &self.dispatch_state);
530
531 DeriveReconciliationContext {
532 http_client: self.http_client.clone(),
533 emitter: self.emitter.clone(),
534 client_id: self.core.client_id,
535 account_id: self.core.account_id,
536 subaccount_id: self.credential.subaccount_id(),
537 clock: self.clock,
538 dispatch_state: Arc::clone(&self.dispatch_state),
539 instruments: Arc::clone(&self.instruments),
540 orders,
541 terminal_revision,
542 }
543 }
544
545 fn validate_order(&self, order: &OrderAny) -> Result<(), OrderDeniedReason> {
546 if is_derive_trigger_order_type(order.order_type()) {
547 validate_trigger_order_support(order)?;
548 } else {
549 validate_order_support(order)?;
550 }
551
552 let cache = self.core.cache();
553
554 if order.is_reduce_only()
555 && matches!(
556 cache.instrument(&order.instrument_id()),
557 Some(InstrumentAny::CurrencyPair(_))
558 )
559 {
560 return Err(OrderDeniedReason::UnsupportedReduceOnly);
561 }
562
563 if order.order_type() == OrderType::Market && cache.quote(&order.instrument_id()).is_none()
564 {
565 return Err(OrderDeniedReason::MarketPriceUnavailable {
566 order_type: order.order_type(),
567 instrument_id: order.instrument_id(),
568 });
569 }
570
571 Ok(())
572 }
573
574 fn restore_active_orders(&self) {
575 let _delivery = self.dispatch_state.delivery_guard();
576 let cache = self.core.cache();
577 for order in cache.orders_refs(
578 Some(&self.core.venue),
579 None,
580 None,
581 Some(&self.core.account_id),
582 None,
583 ) {
584 if (order.is_open() || order.is_inflight())
585 && cache.client_id(&order.client_order_id()) == Some(&self.core.client_id)
586 {
587 self.dispatch_state.restore_order(&order);
588 }
589 }
590 }
591
592 async fn await_account_registered(&self, timeout_secs: f64) -> anyhow::Result<()> {
600 let account_id = self.core.account_id;
601
602 if self.core.cache().account(&account_id).is_some() {
603 log::info!("Account {account_id} registered");
604 return Ok(());
605 }
606
607 let start = Instant::now();
608 let timeout = Duration::from_secs_f64(timeout_secs);
609 let interval = Duration::from_millis(10);
610 loop {
611 time::sleep(interval).await;
612
613 if self.core.cache().account(&account_id).is_some() {
614 log::info!("Account {account_id} registered");
615 return Ok(());
616 }
617
618 if start.elapsed() >= timeout {
619 anyhow::bail!(
620 "Timeout waiting for account {account_id} to be registered after {timeout_secs}s"
621 );
622 }
623 }
624 }
625
626 async fn teardown_partial_connect(&mut self) -> anyhow::Result<()> {
631 self.cancellation_token.cancel();
632 self.abort_session_tasks();
633 self.abort_pending_tasks();
634
635 if let Err(e) = self.ws_client.disconnect().await {
636 self.shutdown_errors
637 .push(format!("Derive WebSocket shutdown failed: {e}"));
638 }
639
640 let (session_result, pending_result) =
641 tokio::join!(self.await_session_tasks(), self.await_pending_tasks());
642 self.core.set_disconnected();
643 self.is_connected.store(false, Ordering::Release);
644
645 if let Err(e) = session_result {
646 self.shutdown_errors.push(e.to_string());
647 }
648
649 if let Err(e) = pending_result {
650 self.shutdown_errors.push(e.to_string());
651 }
652
653 if !self.shutdown_errors.is_empty() {
654 anyhow::bail!(std::mem::take(&mut self.shutdown_errors).join("; "));
655 }
656
657 Ok(())
658 }
659
660 fn start_ws_dispatch(
661 &self,
662 rx: tokio::sync::mpsc::UnboundedReceiver<DeriveWsMessage>,
663 ) -> anyhow::Result<()> {
664 let emitter = self.emitter.clone();
665 let account_id = self.core.account_id;
666 let clock = self.clock;
667 let cancellation = self.cancellation_token.clone();
668 let dispatch_state = self.dispatch_state.clone();
669 let reconciliation = self.reconciliation_context();
670 let timeout = self
671 .config
672 .ws_timeout_secs
673 .map_or(WS_REQUEST_TIMEOUT, Duration::from_secs);
674
675 let request = ReconciliationSnapshotRequest::new(
676 self.core.clone(),
677 reconciliation.clone(),
678 cancellation.clone(),
679 timeout,
680 );
681 *self.reconciliation_request.lock() = request.clone();
682 let is_connected = Arc::clone(&self.is_connected);
683 let session_spawner = self
684 .session_tasks
685 .spawner()
686 .map_err(|e| anyhow::anyhow!("Derive session task admission is closed: {e}"))?;
687
688 self.session_tasks.spawn(async move {
689 let mut rx = rx;
690
691 loop {
692 tokio::select! {
693 biased;
694 () = cancellation.cancelled() => break,
695 maybe = rx.recv() => {
696 match maybe {
697 Some(DeriveWsMessage::Reconnected) => {
698 let context = reconciliation.clone();
699 let request = request.clone();
700 let task_cancellation = cancellation.clone();
701
702 if let Err(e) = session_spawner.spawn(async move {
703 tokio::select! {
704 biased;
705 () = task_cancellation.cancelled() => {}
706 result = async {
707 let (context, ts_init) = match request {
708 Some(request) => request.snapshot().await?,
709 None => (context, clock.get_time_ns()),
710 };
711 context.recover_after_reconnect(ts_init).await
712 } => {
713 if result.is_err() {
714 log::warn!("Derive post-reconnect recovery failed");
715 }
716 }
717 }
718 }) {
719 log::warn!("Skipping Derive reconnect recovery after shutdown began: {e}");
720 }
721 }
722 Some(DeriveWsMessage::SessionRecoveryFailed(_)) => {
723 is_connected.store(false, Ordering::Release);
724 log::error!("Derive execution WebSocket recovery failed");
725 }
726 Some(DeriveWsMessage::Subscription(payload))
727 if payload.channel == DeriveWsChannel::balances(dispatch_state.subaccount_id()).to_string() =>
728 {
729 let context = reconciliation.clone();
730 let task_cancellation = cancellation.clone();
731
732 if let Err(e) = session_spawner.spawn(async move {
733 tokio::select! {
734 biased;
735 () = task_cancellation.cancelled() => {}
736 result = context.refresh_account_state() => {
737 if result.is_err() {
738 log::warn!("Derive balance update refresh failed");
739 }
740 }
741 }
742 }) {
743 log::warn!("Skipping Derive account refresh after shutdown began: {e}");
744 }
745 }
746 Some(message) => handle_ws_message(
747 message,
748 &emitter,
749 account_id,
750 clock,
751 &dispatch_state,
752 ),
753 None => break,
754 }
755 }
756 }
757 }
758
759 is_connected.store(false, Ordering::Release);
760
761 if !cancellation.is_cancelled() {
762 log::warn!("Derive execution WebSocket stream ended unexpectedly");
763 }
764 })?;
765
766 Ok(())
767 }
768
769 fn select_cancel_orders(
770 &self,
771 cmd: &CancelAllOrders,
772 ) -> Option<Vec<(ClientOrderId, VenueOrderId, bool)>> {
773 let side_filter = cmd.order_side;
774 let cache = self.core.cache();
775 let orders = cache.orders_open_refs(
776 Some(&self.core.venue),
777 Some(&cmd.instrument_id),
778 None,
779 Some(&self.core.account_id),
780 side_filter,
781 );
782 let mut cancels = Vec::with_capacity(orders.len());
783
784 for order in orders {
785 let client_order_id = order.client_order_id();
786 if cache.client_id(&client_order_id) != Some(&self.core.client_id) {
787 continue;
788 }
789
790 let is_trigger = is_derive_trigger_order_type(order.order_type());
791 if side_filter.is_none() && !is_trigger {
792 continue;
793 }
794
795 let Some(venue_order_id) = self
796 .dispatch_state
797 .bound_venue_order_id(&client_order_id)
798 .or_else(|| order.venue_order_id())
799 else {
800 log::warn!(
801 "Cannot cancel all orders for {}: order {client_order_id} has no venue_order_id",
802 cmd.instrument_id,
803 );
804 return None;
805 };
806
807 cancels.push((client_order_id, venue_order_id, is_trigger));
808 }
809
810 Some(cancels)
811 }
812}
813
814#[async_trait(?Send)]
815impl ExecutionClient for DeriveExecutionClient {
816 fn is_connected(&self) -> bool {
817 self.is_connected.load(Ordering::Acquire)
818 }
819
820 fn client_id(&self) -> ClientId {
821 self.core.client_id
822 }
823
824 fn account_id(&self) -> AccountId {
825 self.core.account_id
826 }
827
828 fn venue(&self) -> Venue {
829 *DERIVE_VENUE
830 }
831
832 fn oms_type(&self) -> OmsType {
833 self.core.oms_type
834 }
835
836 fn get_account(&self) -> Option<AccountAny> {
837 self.core.cache().account_owned(&self.core.account_id)
838 }
839
840 fn venue_order_ids(
841 &self,
842 mass_status: &ExecutionMassStatus,
843 ) -> Option<IndexMap<ClientOrderId, Vec<VenueOrderId>>> {
844 let reports = mass_status.order_reports();
845 let mut groups: IndexMap<ClientOrderId, Vec<&OrderStatusReport>> = IndexMap::new();
846
847 for report in reports.values() {
848 if let Some(id) = report.client_order_id {
849 groups.entry(id).or_default().push(report);
850 }
851 }
852
853 let cache = self.core.cache();
854 let mut order_venue_ids = IndexMap::new();
855
856 for (id, mut group) in groups {
857 if cache.order_exists(&id) {
858 continue;
859 }
860
861 let mut open = group.iter().filter(|report| report.order_status.is_open());
862
863 let Some(current) = open.next().copied() else {
864 continue;
865 };
866
867 if group.len() < 2
868 || open.next().is_some()
869 || group.iter().any(|report| {
870 cache.client_order_id(&report.venue_order_id).is_some()
871 || report.account_id != current.account_id
872 || report.instrument_id != current.instrument_id
873 || report.order_side != current.order_side
874 || report.order_type != current.order_type
875 || report.venue_order_id != current.venue_order_id
876 && !matches!(
877 report.order_status,
878 OrderStatus::Canceled | OrderStatus::Expired | OrderStatus::Filled
879 )
880 })
881 {
882 continue;
883 }
884
885 group.sort_by_key(|report| report.ts_accepted);
886 let venue_order_ids = group
887 .into_iter()
888 .filter(|report| report.venue_order_id != current.venue_order_id)
889 .map(|report| report.venue_order_id)
890 .chain(std::iter::once(current.venue_order_id))
891 .collect();
892 order_venue_ids.insert(id, venue_order_ids);
893 }
894
895 (!order_venue_ids.is_empty()).then_some(order_venue_ids)
896 }
897
898 fn register_external_order(
899 &self,
900 client_order_id: ClientOrderId,
901 venue_order_id: VenueOrderId,
902 instrument_id: InstrumentId,
903 strategy_id: StrategyId,
904 _ts_init: UnixNanos,
905 ) {
906 let _delivery = self.dispatch_state.delivery_guard();
907 let cache = self.core.cache();
908 if let Some(order) = cache.order_ref(&client_order_id)
909 && order.is_open()
910 && order.account_id() == Some(self.core.account_id)
911 && order.instrument_id() == instrument_id
912 && order.strategy_id() == strategy_id
913 && order.venue_order_id() == Some(venue_order_id)
914 && cache.client_id(&client_order_id) == Some(&self.core.client_id)
915 {
916 self.dispatch_state.restore_order(&order);
917 }
918 }
919
920 fn start(&mut self) -> anyhow::Result<()> {
921 self.emitter.set_sender(get_exec_event_sender());
922 self.restore_active_orders();
923 flush_pending_delivery(
924 &self.emitter,
925 self.core.account_id,
926 self.clock,
927 &self.dispatch_state,
928 );
929
930 if self.core.is_started() {
931 return Ok(());
932 }
933
934 self.core.set_started();
935
936 log::info!(
937 "Started: client_id={}, account_id={}, subaccount_id={}, environment={:?}, proxy_url={:?}",
938 self.core.client_id,
939 self.core.account_id,
940 self.credential.subaccount_id(),
941 self.config.environment,
942 self.config.proxy_url,
943 );
944 Ok(())
945 }
946
947 fn stop(&mut self) -> anyhow::Result<()> {
948 if self.core.is_stopped()
949 && self.cancellation_token.is_cancelled()
950 && !self.session_tasks.is_open()
951 && !self.pending_tasks.is_open()
952 {
953 return Ok(());
954 }
955
956 log::info!("Stopping Derive execution client");
957
958 self.cancellation_token.cancel();
959 self.abort_session_tasks();
960 self.abort_pending_tasks();
961
962 self.core.set_stopped();
963 self.core.set_disconnected();
964 self.is_connected.store(false, Ordering::Release);
965
966 log::info!("Derive execution client stopped");
967 Ok(())
968 }
969
970 fn reset(&mut self) -> anyhow::Result<()> {
971 self.stop()
972 }
973
974 fn dispose(&mut self) -> anyhow::Result<()> {
975 self.stop()
976 }
977
978 async fn connect(&mut self) -> anyhow::Result<()> {
979 if self.is_connected()
980 && !self.cancellation_token.is_cancelled()
981 && self.session_tasks.is_open()
982 && self.pending_tasks.is_open()
983 {
984 return Ok(());
985 }
986
987 log::info!("Connecting Derive execution client");
988
989 if self.cancellation_token.is_cancelled()
990 || !self.session_tasks.is_open()
991 || !self.pending_tasks.is_open()
992 {
993 self.teardown_partial_connect().await?;
994 self.session_tasks
995 .start_generation()
996 .map_err(|e| anyhow::anyhow!("Failed to start Derive session generation: {e}"))?;
997 self.pending_tasks
998 .start_generation()
999 .map_err(|e| anyhow::anyhow!("Failed to start Derive task generation: {e}"))?;
1000 self.cancellation_token = CancellationToken::new();
1001 }
1002
1003 let cancellation_token = self.cancellation_token.clone();
1004 let ws_shutdown = self.ws_client.shutdown_handle();
1005
1006 let setup_guard =
1007 TaskGroupGuard::new(&[&self.session_tasks, &self.pending_tasks], move || {
1008 cancellation_token.cancel();
1009 ws_shutdown.begin_shutdown();
1010 });
1011
1012 self.ensure_instruments_initialized()
1013 .await
1014 .context("failed to initialize Derive instruments")?;
1015 self.initialize_portfolio()
1016 .await
1017 .context("failed initial Derive account state refresh")?;
1018 self.restore_active_orders();
1019
1020 if self.dispatch_state.has_active_orders() {
1021 let context = self.reconciliation_context();
1022 let mut instruments: Vec<_> = context
1023 .orders
1024 .iter()
1025 .filter(|(id, _)| self.dispatch_state.identity(id).is_some())
1026 .map(|(_, binding)| binding.identity.instrument_id)
1027 .collect::<AHashSet<_>>()
1028 .into_iter()
1029 .collect();
1030 instruments.sort_unstable();
1031 for instrument_id in instruments {
1032 context.prime_order_bindings(instrument_id).await?;
1033 }
1034 }
1035
1036 self.ws_client
1037 .connect()
1038 .await
1039 .context("failed to connect Derive WebSocket")?;
1040
1041 let Some(rx) = self.ws_client.take_event_receiver() else {
1042 let e = anyhow::anyhow!("Derive execution WS event receiver not initialized");
1043 if let Err(teardown_error) = self.teardown_partial_connect().await {
1044 return Err(e.context(format!(
1045 "Derive execution startup teardown failed: {teardown_error}"
1046 )));
1047 }
1048
1049 return Err(e);
1050 };
1051
1052 let subaccount_id = self.credential.subaccount_id();
1053 let channels = vec![
1054 DeriveWsChannel::orders(subaccount_id),
1055 DeriveWsChannel::private_trades(subaccount_id),
1056 DeriveWsChannel::balances(subaccount_id),
1057 ];
1058
1059 if let Err(e) = self.ws_client.subscribe_channels(channels).await {
1060 log::warn!(
1061 "Derive private WS subscriptions failed: {}; tearing down",
1062 e.redacted_message()
1063 );
1064
1065 if let Err(teardown_error) = self.teardown_partial_connect().await {
1066 return Err(anyhow::Error::new(e).context(format!(
1067 "Derive execution startup teardown failed: {teardown_error}"
1068 )));
1069 }
1070
1071 return Err(anyhow::Error::new(e).context("failed Derive private WS subscriptions"));
1072 }
1073
1074 self.initialize_portfolio()
1075 .await
1076 .context("failed subscribed Derive portfolio refresh")?;
1077
1078 if let Err(e) = self.start_ws_dispatch(rx) {
1079 if let Err(teardown_error) = self.teardown_partial_connect().await {
1080 return Err(e.context(format!(
1081 "Derive execution startup teardown failed: {teardown_error}"
1082 )));
1083 }
1084
1085 return Err(e.context("failed to register Derive execution WebSocket dispatch task"));
1086 }
1087
1088 self.core.set_connected();
1089 self.is_connected.store(true, Ordering::Release);
1090 setup_guard.disarm();
1091 log::info!(
1092 "Connected Derive execution client ({:?})",
1093 self.config.environment
1094 );
1095 Ok(())
1096 }
1097
1098 async fn disconnect(&mut self) -> anyhow::Result<()> {
1099 log::info!("Disconnecting Derive execution client");
1100 self.teardown_partial_connect().await?;
1101 log::info!("Derive execution client disconnected");
1102 Ok(())
1103 }
1104
1105 fn generate_account_state(
1106 &self,
1107 balances: Vec<AccountBalance>,
1108 margins: Vec<MarginBalance>,
1109 reported: bool,
1110 ts_event: UnixNanos,
1111 info: Option<Params>,
1112 ) -> anyhow::Result<()> {
1113 self.emitter
1114 .emit_account_state(balances, margins, reported, ts_event, info);
1115 Ok(())
1116 }
1117
1118 fn on_instrument(&mut self, instrument: InstrumentAny) {
1119 if let Err(e) = self.cache_native_instrument(&instrument) {
1120 log::warn!("Cannot cache Derive instrument {}: {e:#}", instrument.id());
1121 }
1122 }
1123
1124 async fn generate_order_status_report(
1125 &self,
1126 cmd: &GenerateOrderStatusReport,
1127 ) -> anyhow::Result<Option<OrderStatusReport>> {
1128 self.reconciliation_context()
1129 .generate_order_status_report(cmd)
1130 .await
1131 }
1132
1133 async fn generate_order_status_reports(
1134 &self,
1135 cmd: &GenerateOrderStatusReports,
1136 ) -> anyhow::Result<Vec<OrderStatusReport>> {
1137 let context = self.reconciliation_context();
1138 let mut reports = context
1139 .generate_order_status_reports(cmd)
1140 .await?
1141 .into_complete("order")?;
1142
1143 if !cmd.open_only {
1144 let mut active_cmd = cmd.clone();
1145 active_cmd.open_only = true;
1146 let active = context
1147 .generate_order_status_reports(&active_cmd)
1148 .await?
1149 .into_complete("active order")?;
1150 reports.extend(active);
1151 }
1152
1153 context.ensure_order_context()?;
1154 let reports = deduplicate_order_status_reports(reports)?;
1155 log_report_receipt(reports.len(), "OrderStatusReport", cmd.log_receipt_level);
1156 Ok(reports)
1157 }
1158
1159 async fn generate_fill_reports(
1160 &self,
1161 cmd: GenerateFillReports,
1162 ) -> anyhow::Result<Vec<FillReport>> {
1163 let log_level = cmd.log_receipt_level;
1164 let reports = self
1165 .reconciliation_context()
1166 .generate_fill_reports(cmd, false)
1167 .await?
1168 .into_complete("fill")?;
1169 log_report_receipt(reports.len(), "FillReport", log_level);
1170 Ok(reports)
1171 }
1172
1173 async fn generate_position_status_reports(
1174 &self,
1175 cmd: &GeneratePositionStatusReports,
1176 ) -> anyhow::Result<Vec<PositionStatusReport>> {
1177 let snapshot = self
1178 .reconciliation_context()
1179 .generate_position_status_snapshot(cmd)
1180 .await?;
1181 log_report_receipt(
1182 snapshot.reports.len(),
1183 "PositionStatusReport",
1184 cmd.log_receipt_level,
1185 );
1186 Ok(snapshot.reports)
1187 }
1188
1189 async fn generate_mass_status(
1190 &self,
1191 lookback_mins: Option<u64>,
1192 ) -> anyhow::Result<Option<ExecutionMassStatus>> {
1193 let ts_init = self.clock.get_time_ns();
1194 Box::pin(
1195 self.reconciliation_context()
1196 .generate_mass_status(lookback_mins, ts_init, true),
1197 )
1198 .await
1199 .map(Some)
1200 }
1201
1202 fn submit_order(&self, cmd: SubmitOrder) -> anyhow::Result<()> {
1203 let order = self.core.cache().try_order_owned(&cmd.client_order_id)?;
1204
1205 if order.is_closed() {
1206 log::warn!("Cannot submit closed order {}", order.client_order_id());
1207 return Ok(());
1208 }
1209
1210 if let Err(reason) = self.validate_order(&order) {
1211 log::warn!("Cannot submit order {}: {reason}", order.client_order_id());
1212 self.emitter.emit_order_denied(&order, &reason.to_string());
1213 return Ok(());
1214 }
1215
1216 let is_trigger_order = is_derive_trigger_order_type(order.order_type());
1217 let market_quote = (order.order_type() == OrderType::Market).then_some(());
1218
1219 let venue_symbol = format_venue_symbol(&cmd.instrument_id)?.to_string();
1220 let http_client = self.http_client.clone();
1221 let ws_exec = self.ws_exec.clone();
1222 let signing = self.signing.clone();
1223 let nonce_manager = self.nonce_manager.clone();
1224 let wallet_str = self.credential.wallet_address().to_string();
1225 let emitter = self.emitter.clone();
1226 let clock = self.clock;
1227 let instruments = self.instruments.clone();
1228 let instrument_id = cmd.instrument_id;
1229 let order_for_task = order.clone();
1230 let account_id = self.core.account_id;
1231
1232 let identity = OrderIdentity {
1235 instrument_id: order.instrument_id(),
1236 strategy_id: order.strategy_id(),
1237 order_side: order.order_side(),
1238 order_type: order.order_type(),
1239 };
1240
1241 self.dispatch_state
1242 .register_identity(order.client_order_id(), identity);
1243 self.dispatch_state.record_order_shape(
1244 order.client_order_id(),
1245 order.quantity(),
1246 order.price(),
1247 );
1248
1249 self.emitter.emit_order_submitted(&order);
1250
1251 let slippage_bps = self.signing.market_order_slippage_bps;
1252 let dispatch_state = self.dispatch_state.clone();
1253
1254 self.spawn_task("submit_order", async move {
1255 let instrument = match cached_or_fetch_instrument(
1256 &http_client,
1257 &instruments,
1258 &instrument_id,
1259 &venue_symbol,
1260 )
1261 .await
1262 {
1263 Ok(i) => i,
1264 Err(e) => {
1265 log::warn!("Failed to resolve instrument {venue_symbol}: {e}");
1266 dispatch_state.forget(&order_for_task.client_order_id());
1267 let ts = clock.get_time_ns();
1268 emit_submit_failure(&emitter, &order_for_task, CommandFailure::not_sent(format!("instrument resolution failed: {e}")), ts, false);
1269 return Ok(());
1270 }
1271 };
1272
1273 if order_for_task.is_reduce_only()
1277 && instrument.instrument_type == DeriveInstrumentType::Erc20
1278 {
1279 let reason = format!(
1280 "reduce-only is not supported for spot instrument {}; Derive spot has no position to reduce",
1281 order_for_task.instrument_id(),
1282 );
1283 log::warn!("{reason}");
1284 dispatch_state.forget(&order_for_task.client_order_id());
1285 let ts = clock.get_time_ns();
1286 emit_submit_failure(&emitter, &order_for_task, CommandFailure::not_sent(&reason), ts, false);
1287 return Ok(());
1288 }
1289
1290 let explicit_price = if market_quote.is_some() {
1292 let quote = match refresh_market_order_quote(
1293 &http_client,
1294 &venue_symbol,
1295 &instrument,
1296 clock,
1297 )
1298 .await
1299 {
1300 Ok(quote) => quote,
1301 Err(e) => {
1302 let reason = format!(
1303 "market-order quote refresh failed for {}: {e}",
1304 order_for_task.client_order_id(),
1305 );
1306 log::warn!("{reason}");
1307 dispatch_state.forget(&order_for_task.client_order_id());
1308 let ts = clock.get_time_ns();
1309 emit_submit_failure(&emitter, &order_for_task, CommandFailure::not_sent(&reason), ts, false);
1310 return Ok(());
1311 }
1312 };
1313
1314 match market_order_limit_price(
1315 "e,
1316 order_for_task.order_side(),
1317 slippage_bps,
1318 instrument.tick_size,
1319 ) {
1320 Some(p) => Some(p),
1321 None => {
1322 let reason = format!(
1323 "market-order slippage bound is non-positive for {} ({} bps)",
1324 order_for_task.client_order_id(),
1325 slippage_bps,
1326 );
1327 log::warn!("{reason}");
1328 dispatch_state.forget(&order_for_task.client_order_id());
1329 let ts = clock.get_time_ns();
1330 emit_submit_failure(&emitter, &order_for_task, CommandFailure::not_sent(&reason), ts, false);
1331 return Ok(());
1332 }
1333 }
1334 } else if matches!(
1335 order_for_task.order_type(),
1336 OrderType::StopMarket | OrderType::MarketIfTouched
1337 ) {
1338 let trigger_price = match order_for_task.trigger_price() {
1339 Some(price) => price.as_decimal(),
1340 None => {
1341 let reason = format!(
1342 "trigger market order {} is missing trigger_price",
1343 order_for_task.client_order_id(),
1344 );
1345 log::warn!("{reason}");
1346 dispatch_state.forget(&order_for_task.client_order_id());
1347 let ts = clock.get_time_ns();
1348 emit_submit_failure(&emitter, &order_for_task, CommandFailure::not_sent(&reason), ts, false);
1349 return Ok(());
1350 }
1351 };
1352
1353 match trigger_market_limit_price(
1354 trigger_price,
1355 order_for_task.order_side(),
1356 slippage_bps,
1357 instrument.tick_size,
1358 ) {
1359 Some(p) => Some(p),
1360 None => {
1361 let reason = format!(
1362 "trigger market-order slippage bound is non-positive for {} ({} bps)",
1363 order_for_task.client_order_id(),
1364 slippage_bps,
1365 );
1366 log::warn!("{reason}");
1367 dispatch_state.forget(&order_for_task.client_order_id());
1368 let ts = clock.get_time_ns();
1369 emit_submit_failure(&emitter, &order_for_task, CommandFailure::not_sent(&reason), ts, false);
1370 return Ok(());
1371 }
1372 }
1373 } else {
1374 None
1375 };
1376
1377 let matching_reservation = match ws_exec
1378 .reserve_matching_request(
1379 "private/order",
1380 &instrument.instrument_name,
1381 )
1382 .await
1383 {
1384 Ok(reservation) => reservation,
1385 Err(e) => {
1386 let (reason, due_post_only) = ws_rejection_reason(&e);
1387 log::warn!(
1388 "Cannot reserve Derive order quota for {}: {reason}",
1389 order_for_task.client_order_id(),
1390 );
1391 dispatch_state.forget(&order_for_task.client_order_id());
1392 let ts = clock.get_time_ns();
1393 emit_submit_failure(&emitter, &order_for_task, CommandFailure::not_sent(&reason), ts, due_post_only);
1394 return Ok(());
1395 }
1396 };
1397
1398 let expiry =
1399 match normal_order_signature_expiry(
1400 clock,
1401 if is_trigger_order {
1402 TRIGGER_ORDER_SIGNATURE_TTL.as_secs()
1403 } else {
1404 signing.signature_expiry_secs
1405 },
1406 ) {
1407 Ok(expiry) => expiry,
1408 Err(e) => {
1409 log::warn!(
1410 "Order expiry validation failed for {}: {e}",
1411 order_for_task.client_order_id()
1412 );
1413 dispatch_state.forget(&order_for_task.client_order_id());
1414 let ts = clock.get_time_ns();
1415 emit_submit_failure(&emitter, &order_for_task, CommandFailure::not_sent(format!("order expiry validation failed: {e}")), ts, false);
1416 return Ok(());
1417 }
1418 };
1419
1420 let nonce = match resolve_submit_nonce(
1421 nonce_manager.next_nonce(&wallet_str, signing.subaccount_id),
1422 &emitter,
1423 &dispatch_state,
1424 &order_for_task,
1425 clock,
1426 ) {
1427 Some(nonce) => nonce,
1428 None => return Ok(()),
1429 };
1430
1431 let payload_result = if is_trigger_order {
1432 trigger_order_to_derive_payload(
1433 &order_for_task,
1434 &instrument,
1435 signing.subaccount_id,
1436 signing.wallet_address,
1437 &signing.signer,
1438 nonce,
1439 expiry,
1440 signing.trade_module_address,
1441 signing.domain_separator,
1442 signing.action_typehash,
1443 signing.max_fee_per_contract,
1444 explicit_price,
1445 "",
1446 "",
1447 )
1448 .map(|params| params.order)
1449 } else {
1450 order_to_derive_payload(
1451 &order_for_task,
1452 &instrument,
1453 signing.subaccount_id,
1454 signing.wallet_address,
1455 &signing.signer,
1456 nonce,
1457 expiry,
1458 signing.trade_module_address,
1459 signing.domain_separator,
1460 signing.action_typehash,
1461 signing.max_fee_per_contract,
1462 explicit_price,
1463 )
1464 };
1465
1466 let payload = match payload_result {
1467 Ok(p) => p,
1468 Err(e) => {
1469 log::warn!("Order encode failed for {}: {e}", order_for_task.client_order_id());
1470 dispatch_state.forget(&order_for_task.client_order_id());
1471 let ts = clock.get_time_ns();
1472 emit_submit_failure(&emitter, &order_for_task, CommandFailure::not_sent(format!("order encoding failed: {e}")), ts, false);
1473 return Ok(());
1474 }
1475 };
1476
1477 log::debug!(
1480 "Derive submit payload client_order_id={} instrument_name={} direction={} order_type={} time_in_force={} amount={} limit_price={}",
1481 order_for_task.client_order_id(),
1482 payload.instrument_name.as_str(),
1483 payload.direction,
1484 payload.order_type,
1485 payload.time_in_force,
1486 payload.amount,
1487 payload.limit_price,
1488 );
1489
1490 match ws_exec
1491 .submit_order_after_rate_limit(&payload, matching_reservation)
1492 .await
1493 .map_err(|e| ws_command_failure(&e))
1494 {
1495 Ok(result) => {
1496 dispatch_order_result(
1497 result,
1498 order_for_task.client_order_id(),
1499 &emitter,
1500 account_id,
1501 clock,
1502 &dispatch_state,
1503 );
1504 log::debug!(
1505 "Order submitted: client_order_id={}",
1506 order_for_task.client_order_id(),
1507 );
1508 }
1509 Err((CommandFailure::Ambiguous(reason), _)) => {
1511 log::warn!(
1512 "Derive submit for {} returned ambiguous WS outcome: {reason}; awaiting reconciliation",
1513 order_for_task.client_order_id(),
1514 );
1515 }
1516 Err((failure, due_post_only)) => {
1517 log::debug!(
1518 "Derive rejected order {}: {failure:?}",
1519 order_for_task.client_order_id(),
1520 );
1521
1522 if dispatch_state
1523 .take_identity(&order_for_task.client_order_id())
1524 .is_some()
1525 {
1526 let ts = clock.get_time_ns();
1527 emit_submit_failure(&emitter, &order_for_task, failure, ts, due_post_only);
1528 }
1529 }
1530 }
1531
1532 Ok(())
1533 });
1534
1535 Ok(())
1536 }
1537
1538 fn submit_order_list(&self, cmd: SubmitOrderList) -> anyhow::Result<()> {
1539 let orders = self.core.get_orders_for_list(&cmd.order_list)?;
1540 let denials: Vec<_> = orders
1541 .iter()
1542 .map(|order| self.validate_order(order).err())
1543 .collect();
1544
1545 if denials.iter().any(Option::is_some) {
1546 for (order, denial) in orders.iter().zip(denials) {
1547 let reason = denial.unwrap_or(OrderDeniedReason::OrderListDenied {
1548 order_list_id: cmd.order_list.id,
1549 });
1550
1551 self.emitter.emit_order_denied(order, &reason.to_string());
1552 }
1553
1554 return Ok(());
1555 }
1556
1557 for order in orders {
1558 let sub = SubmitOrder::from_order(
1559 &order,
1560 cmd.trader_id,
1561 cmd.client_id,
1562 cmd.position_id,
1563 UUID4::new(),
1564 cmd.ts_init,
1565 );
1566 self.submit_order(sub)?;
1567 }
1568
1569 Ok(())
1570 }
1571
1572 fn cancel_order(&self, cmd: CancelOrder) -> anyhow::Result<()> {
1573 if let Err(e) = ensure_order_binding_resolved(&self.dispatch_state, cmd.client_order_id) {
1574 emit_cancel_failure(
1575 &self.emitter,
1576 cmd.strategy_id,
1577 cmd.instrument_id,
1578 cmd.client_order_id,
1579 self.dispatch_state
1580 .bound_venue_order_id(&cmd.client_order_id)
1581 .or(cmd.venue_order_id),
1582 CommandFailure::not_sent(e.to_string()),
1583 self.clock.get_time_ns(),
1584 );
1585 return Ok(());
1586 }
1587
1588 let http_client = self.http_client.clone();
1589 let ws_exec = self.ws_exec.clone();
1590 let subaccount_id = self.credential.subaccount_id();
1591 let venue_symbol = format_venue_symbol(&cmd.instrument_id)?.to_string();
1592 let emitter = self.emitter.clone();
1593 let clock = self.clock;
1594 let account_id = self.core.account_id;
1595 let dispatch_state = self.dispatch_state.clone();
1596 let strategy_id = cmd.strategy_id;
1597 let instrument_id = cmd.instrument_id;
1598 let client_order_id = cmd.client_order_id;
1599 let venue_order_id = dispatch_state
1600 .bound_venue_order_id(&client_order_id)
1601 .or(cmd.venue_order_id);
1602 let is_trigger_order = self
1603 .core
1604 .cache()
1605 .order(&client_order_id)
1606 .is_some_and(|order| is_derive_trigger_order_type(order.order_type()));
1607
1608 self.spawn_task("cancel_order", async move {
1609 let venue_order_id = if venue_order_id.is_none() && is_trigger_order
1610 && !dispatch_state.trigger_active(&client_order_id)
1611 {
1612 match resolve_trigger_cancel_id(&http_client, &dispatch_state, subaccount_id, client_order_id, venue_symbol.as_str()).await {
1613 Ok(id) => Some(id),
1614 Err(reason) => {
1615 log::warn!("Cannot cancel trigger order {client_order_id}: {reason}");
1616 emit_cancel_failure(&emitter, strategy_id, instrument_id, client_order_id, None, CommandFailure::not_sent(&reason), clock.get_time_ns());
1617 return Ok(());
1618 }
1619 }
1620 } else { dispatch_state.bound_venue_order_id(&client_order_id).or(venue_order_id) };
1621
1622 let is_trigger_order = is_trigger_order && !dispatch_state.trigger_active(&client_order_id);
1623 let expected_cancel_id = venue_order_id;
1624
1625 let outcome = match venue_order_id {
1626 Some(venue_order_id) if is_trigger_order => {
1627 ws_exec
1628 .cancel_trigger_order_current(
1629 &DeriveCancelTriggerOrderParams::new(subaccount_id, venue_order_id.as_str()),
1630 &DeriveCancelParams::new(subaccount_id, venue_symbol.as_str(), venue_order_id.as_str()),
1631 || dispatch_state.trigger_active(&client_order_id),
1632 )
1633 .await
1634 }
1635 Some(venue_order_id) => ws_exec
1636 .cancel_order(&DeriveCancelParams::new(
1637 subaccount_id,
1638 venue_symbol.as_str(),
1639 venue_order_id.as_str(),
1640 ))
1641 .await
1642 .map(|()| None),
1643 None => ws_exec
1644 .cancel_by_label(&DeriveCancelByLabelParams::new(
1645 subaccount_id,
1646 client_order_id.as_str(),
1647 ))
1648 .await
1649 .map(|result| {
1650 if result.cancelled_orders == 0 {
1651 let reason = "no open order matched the client_order_id label";
1652 log::debug!(
1653 "Derive rejected cancel for {client_order_id}: {reason}"
1654 );
1655 let ts = clock.get_time_ns();
1656 emit_cancel_failure(&emitter, strategy_id, instrument_id, client_order_id, None, CommandFailure::venue_rejected(reason), ts);
1657 }
1658
1659 None
1660 }),
1661 };
1662
1663 match outcome.map_err(|e| ws_command_failure(&e)) {
1664 Ok(Some(canceled_order)) => {
1665 emit_trigger_cancel_result(&canceled_order, expected_cancel_id, &cmd, &emitter, &dispatch_state, account_id, clock)?;
1666 }
1667 Ok(None) => {}
1668 Err((CommandFailure::Ambiguous(reason), _)) => {
1670 log::warn!(
1671 "Derive cancel for {client_order_id} returned ambiguous WS outcome: {reason}; awaiting reconciliation",
1672 );
1673 }
1674 Err((failure, _)) => {
1675 log::debug!("Derive rejected cancel for {client_order_id}: {failure:?}");
1676 let ts = clock.get_time_ns();
1677 emit_cancel_failure(&emitter, strategy_id, instrument_id, client_order_id, venue_order_id, failure, ts);
1678 }
1679 }
1680
1681 Ok(())
1682 });
1683
1684 Ok(())
1685 }
1686
1687 fn cancel_all_orders(&self, cmd: CancelAllOrders) -> anyhow::Result<()> {
1688 ensure_bindings_resolved(&self.dispatch_state, Some(cmd.instrument_id))?;
1689 let venue_symbol = format_venue_symbol(&cmd.instrument_id)?.to_string();
1690 let side_filter = cmd.order_side;
1691
1692 let Some(cancels) = self.select_cancel_orders(&cmd) else {
1693 return Ok(());
1694 };
1695
1696 if side_filter.is_some() && cancels.is_empty() {
1697 return Ok(());
1698 }
1699
1700 let ws_exec = self.ws_exec.clone();
1701 let subaccount_id = self.credential.subaccount_id();
1702
1703 let dispatch_state = Arc::clone(&self.dispatch_state);
1704 self.spawn_task("cancel_all_orders", async move {
1705 for (client_order_id, venue_order_id, is_trigger) in cancels {
1706 let is_trigger = is_trigger && !dispatch_state.trigger_active(&client_order_id);
1707 if side_filter.is_none() && !is_trigger {
1708 continue;
1709 }
1710
1711 let outcome = if is_trigger {
1712 ws_exec
1713 .cancel_trigger_order_current(
1714 &DeriveCancelTriggerOrderParams::new(
1715 subaccount_id,
1716 venue_order_id.as_str(),
1717 ),
1718 &DeriveCancelParams::new(
1719 subaccount_id,
1720 venue_symbol.as_str(),
1721 venue_order_id.as_str(),
1722 ),
1723 || dispatch_state.trigger_active(&client_order_id),
1724 )
1725 .await
1726 .map(|_| ())
1727 } else {
1728 ws_exec
1729 .cancel_order(&DeriveCancelParams::new(
1730 subaccount_id,
1731 venue_symbol.as_str(),
1732 venue_order_id.as_str(),
1733 ))
1734 .await
1735 };
1736
1737 if let Err(e) = outcome {
1738 log::warn!(
1739 "Derive cancel_all_orders: cancel for {venue_order_id} failed: {:?}",
1740 ws_command_failure(&e).0,
1741 );
1742 }
1743 }
1744
1745 if side_filter.is_none() {
1746 match ws_exec
1747 .cancel_by_instrument(&DeriveCancelByInstrumentParams::new(
1748 subaccount_id,
1749 venue_symbol.as_str(),
1750 ))
1751 .await
1752 {
1753 Ok(result) if result.cancelled_orders == 0 => {
1754 log::debug!("No open orders to cancel for {venue_symbol}");
1755 }
1756 Ok(_) => {}
1757 Err(e) => {
1758 log::warn!(
1759 "Derive cancel_all_orders failed for {venue_symbol}: {:?}",
1760 ws_command_failure(&e).0
1761 );
1762 }
1763 }
1764 }
1765
1766 Ok(())
1767 });
1768
1769 Ok(())
1770 }
1771
1772 fn batch_cancel_orders(&self, cmd: BatchCancelOrders) -> anyhow::Result<()> {
1773 for cancel in &cmd.cancels {
1774 ensure_order_binding_resolved(&self.dispatch_state, cancel.client_order_id)?;
1775 }
1776
1777 for inner in cmd.cancels {
1778 self.cancel_order(inner)?;
1779 }
1780
1781 Ok(())
1782 }
1783
1784 fn modify_order(&self, cmd: ModifyOrder) -> anyhow::Result<()> {
1785 let ts_now = self.clock.get_time_ns();
1786
1787 if let Err(e) = ensure_order_binding_resolved(&self.dispatch_state, cmd.client_order_id) {
1788 emit_modify_failure(
1789 &self.emitter,
1790 cmd.strategy_id,
1791 cmd.instrument_id,
1792 cmd.client_order_id,
1793 self.dispatch_state
1794 .bound_venue_order_id(&cmd.client_order_id)
1795 .or(cmd.venue_order_id),
1796 CommandFailure::not_sent(e.to_string()),
1797 ts_now,
1798 );
1799 return Ok(());
1800 }
1801
1802 let Some(venue_order_id) = self
1803 .dispatch_state
1804 .bound_venue_order_id(&cmd.client_order_id)
1805 .or(cmd.venue_order_id)
1806 else {
1807 let reason = "venue_order_id is required for modify";
1808 log::warn!("Cannot modify order {}: {reason}", cmd.client_order_id);
1809 emit_modify_failure(
1810 &self.emitter,
1811 cmd.strategy_id,
1812 cmd.instrument_id,
1813 cmd.client_order_id,
1814 None,
1815 CommandFailure::not_sent(reason),
1816 ts_now,
1817 );
1818 return Ok(());
1819 };
1820
1821 let Ok(order) = self.core.cache().try_order_owned(&cmd.client_order_id) else {
1822 let reason = ORDER_NOT_FOUND;
1823 log::warn!("Cannot modify order {}: {reason}", cmd.client_order_id);
1824 emit_modify_failure(
1825 &self.emitter,
1826 cmd.strategy_id,
1827 cmd.instrument_id,
1828 cmd.client_order_id,
1829 Some(venue_order_id),
1830 CommandFailure::not_sent(reason),
1831 ts_now,
1832 );
1833 return Ok(());
1834 };
1835
1836 if is_derive_trigger_order_type(order.order_type()) {
1837 let reason = "Derive trigger orders cannot be modified; cancel and resubmit";
1838 log::warn!("Cannot modify order {}: {reason}", cmd.client_order_id);
1839 emit_modify_failure(
1840 &self.emitter,
1841 cmd.strategy_id,
1842 cmd.instrument_id,
1843 cmd.client_order_id,
1844 Some(venue_order_id),
1845 CommandFailure::not_sent(reason),
1846 ts_now,
1847 );
1848 return Ok(());
1849 }
1850
1851 if let Err(e) =
1852 normal_order_signature_expiry(self.clock, self.signing.signature_expiry_secs)
1853 {
1854 emit_modify_failure(
1855 &self.emitter,
1856 cmd.strategy_id,
1857 cmd.instrument_id,
1858 cmd.client_order_id,
1859 Some(venue_order_id),
1860 CommandFailure::not_sent(format!("replace expiry validation failed: {e}")),
1861 ts_now,
1862 );
1863 return Ok(());
1864 }
1865
1866 let (quantity, price) = self
1867 .dispatch_state
1868 .order_shape(&cmd.client_order_id)
1869 .unwrap_or_else(|| (order.quantity(), order.price()));
1870 let target_quantity = cmd.quantity.unwrap_or(quantity);
1871 let target_price = cmd.price.or(price);
1872
1873 let venue_symbol = format_venue_symbol(&cmd.instrument_id)?.to_string();
1874 let http_client = self.http_client.clone();
1875 let ws_exec = self.ws_exec.clone();
1876 let signing = self.signing.clone();
1877 let nonce_manager = self.nonce_manager.clone();
1878 let wallet_str = self.credential.wallet_address().to_string();
1879 let emitter = self.emitter.clone();
1880 let clock = self.clock;
1881 let instruments = self.instruments.clone();
1882 let dispatch_state = self.dispatch_state.clone();
1883 let order_for_task = order;
1884 let strategy_id = cmd.strategy_id;
1885 let instrument_id = cmd.instrument_id;
1886 let client_order_id = cmd.client_order_id;
1887 let stale_venue_order_id = venue_order_id;
1888 let account_id = self.core.account_id;
1889 let voi_str = venue_order_id.to_string();
1890
1891 {
1892 let _delivery = dispatch_state.delivery_guard();
1893 if !dispatch_state.mark_pending_modify(client_order_id, stale_venue_order_id) {
1894 emit_modify_failure(
1895 &emitter,
1896 strategy_id,
1897 instrument_id,
1898 client_order_id,
1899 Some(stale_venue_order_id),
1900 CommandFailure::not_sent("Another modify is pending"),
1901 clock.get_time_ns(),
1902 );
1903 return Ok(());
1904 }
1905
1906 dispatch_state.record_modify_target(client_order_id, target_quantity, target_price);
1907 }
1908
1909 self.spawn_task("modify_order", async move {
1910 let instrument = match cached_or_fetch_instrument(
1911 &http_client,
1912 &instruments,
1913 &instrument_id,
1914 &venue_symbol,
1915 )
1916 .await
1917 {
1918 Ok(i) => i,
1919 Err(e) => {
1920 let reason = format!("instrument resolution failed: {e}");
1921 log::warn!("Cannot modify order {client_order_id}: {reason}");
1922 let ts = clock.get_time_ns();
1923 emit_modify_failure(&emitter, strategy_id, instrument_id, client_order_id, Some(stale_venue_order_id), CommandFailure::not_sent(&reason), ts);
1924 dispatch_state.clear_pending_modify(&client_order_id);
1925 return Ok(());
1926 }
1927 };
1928
1929 let matching_reservation = match ws_exec
1930 .reserve_matching_request("private/replace", &instrument.instrument_name)
1931 .await
1932 {
1933 Ok(reservation) => reservation,
1934 Err(e) => {
1935 let (reason, _) = ws_rejection_reason(&e);
1936 log::warn!("Cannot reserve Derive replace quota for {client_order_id}: {reason}");
1937 let ts = clock.get_time_ns();
1938 emit_modify_failure(&emitter, strategy_id, instrument_id, client_order_id, Some(stale_venue_order_id), CommandFailure::not_sent(&reason), ts);
1939 dispatch_state.clear_pending_modify(&client_order_id);
1940 return Ok(());
1941 }
1942 };
1943
1944 let identity = OrderIdentity {
1945 instrument_id,
1946 strategy_id,
1947 order_side: order_for_task.order_side(),
1948 order_type: order_for_task.order_type(),
1949 };
1950
1951 let (remaining_amount, expected_filled_amount) = match replacement_amounts(&http_client, &dispatch_state, client_order_id, identity, stale_venue_order_id, target_quantity.as_decimal()).await {
1952 Ok(amounts) => amounts,
1953 Err(e) => {
1954 log::warn!("Cannot prepare Derive replace for {client_order_id}: {e:#}");
1955 emit_modify_failure(&emitter, strategy_id, instrument_id, client_order_id, Some(stale_venue_order_id), CommandFailure::not_sent(e.to_string()), clock.get_time_ns());
1956 dispatch_state.clear_pending_modify(&client_order_id);
1957 return Ok(());
1958 }
1959 };
1960
1961 let expiry = match normal_order_signature_expiry(clock, signing.signature_expiry_secs) {
1962 Ok(expiry) => expiry,
1963 Err(e) => {
1964 let reason = format!("replace expiry validation failed: {e}");
1965 log::warn!("Cannot modify order {client_order_id}: {reason}");
1966 let ts = clock.get_time_ns();
1967 emit_modify_failure(&emitter, strategy_id, instrument_id, client_order_id, Some(stale_venue_order_id), CommandFailure::not_sent(&reason), ts);
1968 dispatch_state.clear_pending_modify(&client_order_id);
1969 return Ok(());
1970 }
1971 };
1972
1973 let nonce = match resolve_modify_nonce(
1974 nonce_manager.next_nonce(&wallet_str, signing.subaccount_id),
1975 &emitter,
1976 strategy_id,
1977 instrument_id,
1978 client_order_id,
1979 stale_venue_order_id,
1980 clock,
1981 ) {
1982 Some(nonce) => nonce,
1983 None => {
1984 dispatch_state.clear_pending_modify(&client_order_id);
1985 return Ok(());
1986 }
1987 };
1988
1989 let mut payload = match order_replace_to_derive_payload(
1990 &order_for_task,
1991 &instrument,
1992 signing.subaccount_id,
1993 signing.wallet_address,
1994 &signing.signer,
1995 nonce,
1996 expiry,
1997 signing.trade_module_address,
1998 signing.domain_separator,
1999 signing.action_typehash,
2000 signing.max_fee_per_contract,
2001 Some(remaining_amount),
2002 target_price.map(|p| p.as_decimal()),
2003 &voi_str,
2004 ) {
2005 Ok(p) => p,
2006 Err(e) => {
2007 let reason = format!("replace encoding failed: {e}");
2008 log::warn!("Cannot modify order {client_order_id}: {reason}");
2009 let ts = clock.get_time_ns();
2010 emit_modify_failure(&emitter, strategy_id, instrument_id, client_order_id, Some(stale_venue_order_id), CommandFailure::not_sent(&reason), ts);
2011 dispatch_state.clear_pending_modify(&client_order_id);
2012 return Ok(());
2013 }
2014 };
2015
2016 payload.expected_filled_amount = Some(expected_filled_amount);
2017
2018 {
2019 let _delivery = dispatch_state.delivery_guard();
2020 dispatch_state.record_modify_nonce(client_order_id, nonce);
2021 }
2022
2023 let outcome = ws_exec
2024 .modify_order_after_rate_limit(&payload, matching_reservation)
2025 .await;
2026
2027 let _delivery = dispatch_state.delivery_guard();
2028
2029 match outcome.map_err(|e| ws_command_failure(&e)) {
2030 Ok(response) => {
2031 dispatch_state.record_replace_completion(client_order_id, ReplaceCompletion {
2032 old_venue_order_id: stale_venue_order_id,
2033 nonce,
2034 identity: OrderIdentity {
2035 instrument_id,
2036 strategy_id,
2037 order_side: order_for_task.order_side(),
2038 order_type: order_for_task.order_type(),
2039 },
2040 result: response.result,
2041 trades: response.trades,
2042 });
2043
2044 flush_pending_delivery(&emitter, account_id, clock, &dispatch_state);
2045 }
2046 Err((CommandFailure::Ambiguous(reason), _)) => {
2047 log::warn!(
2048 "Derive modify for {client_order_id} returned ambiguous WS outcome: {reason}; awaiting reconciliation",
2049 );
2050 }
2051 Err((failure, _)) => {
2052 if !dispatch_state.take_pending_modify(
2053 &client_order_id,
2054 stale_venue_order_id,
2055 None,
2056 ) {
2057 log::debug!(
2058 "Skipping private/replace rejection for {client_order_id}: an incoming terminal frame already resolved the modify",
2059 );
2060 return Ok(());
2061 }
2062
2063 dispatch_state.take_modify_target(&client_order_id);
2064 log::debug!("Derive rejected modify for {client_order_id}: {failure:?}");
2065 let ts = clock.get_time_ns();
2066 emit_modify_failure(&emitter, strategy_id, instrument_id, client_order_id, Some(stale_venue_order_id), failure, ts);
2067 }
2068 }
2069
2070 Ok(())
2071 });
2072
2073 Ok(())
2074 }
2075
2076 fn query_account(&self, _cmd: QueryAccount) -> anyhow::Result<()> {
2077 let http_client = self.http_client.clone();
2078 let subaccount_id = self.credential.subaccount_id();
2079 let emitter = self.emitter.clone();
2080 let clock = self.clock;
2081 self.spawn_task("query_account", async move {
2082 let subaccount = http_client
2083 .get_subaccount(&DeriveGetSubaccountParams::new(subaccount_id))
2084 .await?;
2085 let (balances, margins, info) = parse_derive_subaccount_to_balances(&subaccount)?;
2086 let ts_event = clock.get_time_ns();
2087 emitter.emit_account_state(balances, margins, true, ts_event, Some(info));
2088 Ok(())
2089 });
2090
2091 Ok(())
2092 }
2093
2094 fn query_order(&self, cmd: QueryOrder) -> anyhow::Result<()> {
2095 let context = self.reconciliation_context();
2096
2097 let report_cmd = GenerateOrderStatusReport::new(
2098 cmd.command_id,
2099 cmd.ts_init,
2100 Some(cmd.instrument_id),
2101 Some(cmd.client_order_id),
2102 cmd.venue_order_id,
2103 cmd.params,
2104 cmd.correlation_id,
2105 );
2106
2107 self.spawn_task(
2108 format!(
2109 "query_order client_order_id={}, venue_order_id={:?}",
2110 cmd.client_order_id, cmd.venue_order_id
2111 ),
2112 async move {
2113 let report = context
2114 .generate_order_status_report(&report_cmd)
2115 .await
2116 .with_context(|| {
2117 format!(
2118 "failed to query Derive order: client_order_id={}, venue_order_id={:?}",
2119 cmd.client_order_id, cmd.venue_order_id,
2120 )
2121 })?;
2122
2123 if let Some(report) = report {
2124 context.emitter.send_order_status_report(report);
2125 } else {
2126 log::debug!(
2127 "Derive order not found: client_order_id={}, venue_order_id={:?}",
2128 cmd.client_order_id,
2129 cmd.venue_order_id,
2130 );
2131 }
2132
2133 Ok(())
2134 },
2135 );
2136
2137 Ok(())
2138 }
2139}
2140
2141fn deduplicate_order_status_reports(
2142 reports: Vec<OrderStatusReport>,
2143) -> anyhow::Result<Vec<OrderStatusReport>> {
2144 let mut indices = AHashMap::new();
2145 let mut unique: Vec<OrderReportEvidence> = Vec::with_capacity(reports.len());
2146
2147 for report in reports {
2148 let Some(&index) = indices.get(&report.venue_order_id) else {
2149 indices.insert(report.venue_order_id, unique.len());
2150 unique.push(OrderReportEvidence {
2151 client_order_id: report.client_order_id,
2152 ts_terminal_first: report.order_status.is_closed().then_some(report.ts_last),
2153 ts_active_last: (!report.order_status.is_closed()).then_some(report.ts_last),
2154 report,
2155 });
2156
2157 continue;
2158 };
2159
2160 unique[index].observe(report)?;
2161 }
2162
2163 Ok(unique.into_iter().map(|evidence| evidence.report).collect())
2164}
2165
2166struct OrderReportEvidence {
2167 report: OrderStatusReport,
2168 client_order_id: Option<ClientOrderId>,
2169 ts_terminal_first: Option<UnixNanos>,
2170 ts_active_last: Option<UnixNanos>,
2171}
2172
2173impl OrderReportEvidence {
2174 fn observe(&mut self, report: OrderStatusReport) -> anyhow::Result<()> {
2175 let previous = &self.report;
2176 anyhow::ensure!(
2177 previous.account_id == report.account_id
2178 && previous.instrument_id == report.instrument_id
2179 && previous.order_side == report.order_side
2180 && previous.order_type == report.order_type
2181 && (self.client_order_id.is_none()
2182 || report.client_order_id.is_none()
2183 || self.client_order_id == report.client_order_id),
2184 "Conflicting Derive order identity for {}",
2185 report.venue_order_id,
2186 );
2187
2188 let consistent = if report.order_status.is_closed() {
2189 self.ts_active_last.is_none_or(|ts| ts <= report.ts_last)
2190 } else {
2191 self.ts_terminal_first.is_none_or(|ts| report.ts_last <= ts)
2192 };
2193
2194 anyhow::ensure!(
2195 consistent,
2196 "Conflicting Derive order status for {}",
2197 report.venue_order_id
2198 );
2199
2200 self.client_order_id = self.client_order_id.or(report.client_order_id);
2201 if report.order_status.is_closed() {
2202 self.ts_terminal_first = Some(
2203 self.ts_terminal_first
2204 .map_or(report.ts_last, |ts| ts.min(report.ts_last)),
2205 );
2206 } else {
2207 self.ts_active_last = Some(
2208 self.ts_active_last
2209 .map_or(report.ts_last, |ts| ts.max(report.ts_last)),
2210 );
2211 }
2212
2213 if report.ts_last > previous.ts_last
2214 || (report.ts_last == previous.ts_last
2215 && (report.order_status.is_closed()
2216 || (!previous.order_status.is_closed()
2217 && previous.client_order_id.is_none()
2218 && report.client_order_id.is_some())))
2219 {
2220 self.report = report;
2221 }
2222
2223 Ok(())
2224 }
2225}
2226
2227fn log_report_receipt(count: usize, report_type: &str, level: LogLevel) {
2228 match level {
2229 LogLevel::Off => {}
2230 LogLevel::Trace => log::trace!("Received {count} {report_type} reports"),
2231 LogLevel::Debug => log::debug!("Received {count} {report_type} reports"),
2232 LogLevel::Info => log::info!("Received {count} {report_type} reports"),
2233 LogLevel::Warning => log::warn!("Received {count} {report_type} reports"),
2234 LogLevel::Error => log::error!("Received {count} {report_type} reports"),
2235 }
2236}
2237
2238impl Drop for DeriveExecutionClient {
2239 fn drop(&mut self) {
2240 self.cancellation_token.cancel();
2241 self.abort_session_tasks();
2242 }
2243}
2244
2245#[derive(Debug, Clone)]
2246struct ReconciliationSnapshotRequest {
2247 sender: Arc<dyn TimeEventSender>,
2248 next: Arc<Mutex<Option<PendingReconciliationSnapshot>>>,
2249 cancellation: CancellationToken,
2250 timeout: Duration,
2251}
2252
2253#[derive(Debug)]
2254struct PendingReconciliationSnapshot {
2255 message: TimeEventMessage,
2256 result: tokio::sync::oneshot::Receiver<(DeriveReconciliationContext, UnixNanos)>,
2257}
2258
2259impl ReconciliationSnapshotRequest {
2260 fn new(
2261 core: ExecutionClientCore,
2262 context: DeriveReconciliationContext,
2263 cancellation: CancellationToken,
2264 timeout: Duration,
2265 ) -> Option<Self> {
2266 let request = Self {
2267 sender: try_get_time_event_sender()?,
2268 next: Arc::new(Mutex::new(None)),
2269 cancellation,
2270 timeout,
2271 };
2272
2273 request.arm(core, context);
2274 Some(request)
2275 }
2276
2277 fn arm(&self, core: ExecutionClientCore, mut template: DeriveReconciliationContext) {
2278 template.orders = AHashMap::new();
2279 let next = Arc::downgrade(&self.next);
2280 let sender = Arc::clone(&self.sender);
2281 let cancellation = self.cancellation.clone();
2282 let timeout = self.timeout;
2283 let ts_init = template.clock.get_time_ns();
2284 let (response, result) = tokio::sync::oneshot::channel();
2285 let response = RefCell::new(Some(response));
2286
2287 let callback = TimeEventCallback::RustLocal(Rc::new(move |_| {
2288 if cancellation.is_cancelled() {
2289 return;
2290 }
2291
2292 let Some(next) = next.upgrade() else {
2293 return;
2294 };
2295
2296 let ts_init = template.clock.get_time_ns();
2297 let (orders, terminal_revision) =
2298 cached_order_bindings(&core, &template.dispatch_state);
2299 let mut context = template.clone();
2300 context.orders = orders;
2301 context.terminal_revision = terminal_revision;
2302
2303 let request = Self {
2304 sender: Arc::clone(&sender),
2305 next,
2306 cancellation: cancellation.clone(),
2307 timeout,
2308 };
2309
2310 request.arm(core.clone(), template.clone());
2311
2312 if let Some(response) = response.borrow_mut().take() {
2313 let _ = response.send((context, ts_init));
2314 }
2315 }));
2316
2317 let message = TimeEventMessage::new(
2318 TimeEvent::new(
2319 Ustr::from("derive-reconciliation"),
2320 UUID4::new(),
2321 ts_init,
2322 ts_init,
2323 ),
2324 callback,
2325 );
2326 *self.next.lock() = Some(PendingReconciliationSnapshot { message, result });
2327 }
2328
2329 async fn snapshot(&self) -> anyhow::Result<(DeriveReconciliationContext, UnixNanos)> {
2330 let pending = self
2331 .next
2332 .lock()
2333 .take()
2334 .context("Derive cache snapshot refresh is already pending")?;
2335 self.sender.send(pending.message);
2336 tokio::select! {
2337 biased;
2338 () = self.cancellation.cancelled() => anyhow::bail!("Derive cache snapshot refresh canceled"),
2339 result = time::timeout(self.timeout, pending.result) => {
2340 result.context("Derive cache snapshot refresh timed out")?
2341 .context("Derive cache snapshot refresh stopped without a result")
2342 }
2343 }
2344 }
2345
2346 fn close(&self) {
2347 let pending = self.next.lock().take();
2348 drop(pending);
2349 }
2350}
2351
2352fn cached_order_bindings(
2353 core: &ExecutionClientCore,
2354 dispatch_state: &WsDispatchState,
2355) -> (AHashMap<ClientOrderId, OrderBinding>, Arc<()>) {
2356 let _delivery = dispatch_state.delivery_guard();
2357 let cache = core.cache();
2358 let orders = cache
2359 .orders_refs(Some(&core.venue), None, None, Some(&core.account_id), None)
2360 .into_iter()
2361 .filter(|order| cache.client_id(&order.client_order_id()) == Some(&core.client_id))
2362 .map(|order| {
2363 (
2364 order.client_order_id(),
2365 OrderBinding {
2366 identity: OrderIdentity {
2367 instrument_id: order.instrument_id(),
2368 strategy_id: order.strategy_id(),
2369 order_side: order.order_side(),
2370 order_type: order.order_type(),
2371 },
2372 venue_order_id: order.venue_order_id(),
2373 venue_order_legs: order.venue_order_ids().into_iter().copied().collect(),
2374 },
2375 )
2376 })
2377 .collect();
2378
2379 (orders, dispatch_state.terminal_revision())
2380}
2381
2382#[derive(Debug, Clone)]
2383struct DeriveReconciliationContext {
2384 http_client: DeriveHttpClient,
2385 emitter: ExecutionEventEmitter,
2386 client_id: ClientId,
2387 account_id: AccountId,
2388 subaccount_id: u64,
2389 clock: &'static AtomicTime,
2390 dispatch_state: Arc<WsDispatchState>,
2391 instruments: Arc<AtomicMap<InstrumentId, DeriveInstrument>>,
2392 orders: AHashMap<ClientOrderId, OrderBinding>,
2393 terminal_revision: Arc<()>,
2394}
2395
2396impl DeriveReconciliationContext {
2397 async fn refresh_account_state(&self) -> anyhow::Result<()> {
2398 let value = self
2399 .http_client
2400 .get_subaccount(&DeriveGetSubaccountParams::new(self.subaccount_id))
2401 .await
2402 .context("failed to fetch Derive subaccount snapshot")?;
2403 self.emit_account_snapshot(&value)
2404 }
2405
2406 fn emit_account_snapshot(&self, snapshot: &DeriveSubaccount) -> anyhow::Result<()> {
2407 let (balances, margins, info) = parse_derive_subaccount_to_balances(snapshot)
2408 .context("failed to parse Derive subaccount balances")?;
2409 let ts_event = self.clock.get_time_ns();
2410 self.emitter
2411 .emit_account_state(balances, margins, true, ts_event, Some(info));
2412 Ok(())
2413 }
2414
2415 async fn recover_after_reconnect(&self, ts_init: UnixNanos) -> anyhow::Result<()> {
2416 self.refresh_account_state().await?;
2417 let mass_status = Box::pin(self.generate_mass_status(None, ts_init, false)).await?;
2418 let order_count = mass_status.order_reports().len();
2419 let fill_count: usize = mass_status.fill_reports().values().map(Vec::len).sum();
2420 let position_count = mass_status.position_reports().len();
2421 self.emitter
2422 .send_execution_report(ExecutionReport::MassStatus(Box::new(mass_status)));
2423 log::info!(
2424 "Derive post-reconnect reconciliation submitted: orders={order_count}, fills={fill_count}, positions={position_count}",
2425 );
2426 Ok(())
2427 }
2428
2429 async fn generate_order_status_report(
2430 &self,
2431 cmd: &GenerateOrderStatusReport,
2432 ) -> anyhow::Result<Option<OrderStatusReport>> {
2433 self.ensure_order_context()?;
2434 self.resolve_pending_modifies(cmd.instrument_id).await?;
2435 if cmd.venue_order_id.is_none() && cmd.client_order_id.is_none() {
2436 log::warn!(
2437 "Derive generate_order_status_report requires venue_order_id or client_order_id"
2438 );
2439 return Ok(None);
2440 }
2441
2442 let order = self.resolve_order_record(cmd).await?;
2443
2444 let Some(mut order) = order else {
2445 return Ok(None);
2446 };
2447
2448 let client_order_id = ClientOrderId::new_checked(order.label)
2449 .ok()
2450 .or(cmd.client_order_id);
2451
2452 if let Ok(venue_order_id) = VenueOrderId::new_checked(&order.order_id) {
2453 validate_report_binding(
2454 &self.dispatch_state,
2455 client_order_id,
2456 venue_order_id,
2457 format_instrument_id(order.instrument_name)?,
2458 )?;
2459 }
2460
2461 if let Some(binding) = client_order_id.and_then(|cid| self.order_binding(cid))
2462 && binding.identity.instrument_id == format_instrument_id(order.instrument_name)?
2463 && let Some(current) = binding.venue_order_id
2464 && order.order_id != current.as_str()
2465 {
2466 anyhow::ensure!(
2467 binding
2468 .venue_order_legs
2469 .iter()
2470 .any(|id| id.as_str() == order.order_id),
2471 "Unknown native Derive order binding"
2472 );
2473 order = fetch_order_record(
2474 &self.http_client,
2475 self.subaccount_id,
2476 current,
2477 Some(binding.identity.instrument_id),
2478 )
2479 .await?;
2480 }
2481
2482 if let Some(instrument_id) = cmd.instrument_id
2483 && format_instrument_id(order.instrument_name)? != instrument_id
2484 {
2485 log::warn!(
2486 "Derive order {} is for {} but report requested {}",
2487 order.order_id,
2488 order.instrument_name.as_str(),
2489 instrument_id,
2490 );
2491 return Ok(None);
2492 }
2493
2494 let instrument_id = format_instrument_id(order.instrument_name)?;
2495 let client_order_id = ClientOrderId::new_checked(order.label)
2496 .ok()
2497 .or(cmd.client_order_id);
2498
2499 if let Ok(venue_order_id) = VenueOrderId::new_checked(&order.order_id) {
2500 validate_report_binding(
2501 &self.dispatch_state,
2502 client_order_id,
2503 venue_order_id,
2504 instrument_id,
2505 )?;
2506 }
2507
2508 if tracked_order_identity(client_order_id, instrument_id, &self.dispatch_state).is_some() {
2509 self.prime_order_bindings(instrument_id).await?;
2510 }
2511
2512 let (price_precision, size_precision) = self.order_precision(&order)?;
2513 let ts_init = self.clock.get_time_ns();
2514
2515 let mut report = parse_derive_order_to_report_with_precision(
2516 &order,
2517 self.account_id,
2518 price_precision,
2519 size_precision,
2520 ts_init,
2521 )?;
2522
2523 if report.client_order_id.is_none()
2526 && let Some(client_order_id) = cmd.client_order_id
2527 {
2528 report = report.with_client_order_id(client_order_id);
2529 }
2530
2531 observe_trigger_order(&self.dispatch_state, &order, &report);
2532 self.project_order_report(&order, report, &[], false)
2533 .await
2534 .map(Some)
2535 }
2536
2537 async fn prime_order_bindings(&self, instrument_id: InstrumentId) -> anyhow::Result<()> {
2538 for open_only in [true, false] {
2539 self.generate_order_status_reports(&GenerateOrderStatusReports::new(
2540 UUID4::new(),
2541 self.clock.get_time_ns(),
2542 open_only,
2543 Some(instrument_id),
2544 None,
2545 (!open_only).then(|| self.clock.get_time_ns()),
2546 None,
2547 None,
2548 ))
2549 .await?;
2550 }
2551
2552 Ok(())
2553 }
2554
2555 async fn resolve_order_record(
2556 &self,
2557 cmd: &GenerateOrderStatusReport,
2558 ) -> anyhow::Result<Option<DeriveOrder>> {
2559 let subaccount_id = self.subaccount_id;
2560
2561 if let Some(venue_order_id) = cmd.venue_order_id {
2562 return match fetch_order_record(
2563 &self.http_client,
2564 subaccount_id,
2565 venue_order_id,
2566 cmd.instrument_id,
2567 )
2568 .await
2569 {
2570 Ok(order) => Ok(Some(order)),
2571 Err(e) => {
2572 let triggers = self
2573 .http_client
2574 .get_trigger_orders(&DeriveGetTriggerOrdersParams::new(subaccount_id))
2575 .await?
2576 .orders;
2577
2578 match triggers
2579 .into_iter()
2580 .find(|order| order.order_id.as_str() == venue_order_id.as_str())
2581 {
2582 Some(order) => Ok(Some(order)),
2583 None => Err(e),
2584 }
2585 }
2586 };
2587 }
2588
2589 let label = cmd
2590 .client_order_id
2591 .expect("report target checked before resolution");
2592 let orders = self
2593 .http_client
2594 .get_open_orders(&DeriveGetOpenOrdersParams::new(subaccount_id))
2595 .await?
2596 .orders;
2597
2598 if let Some(order) = orders
2599 .into_iter()
2600 .find(|order| order.label == label.as_str())
2601 {
2602 return Ok(Some(order));
2603 }
2604
2605 let triggers = self
2606 .http_client
2607 .get_trigger_orders(&DeriveGetTriggerOrdersParams::new(subaccount_id))
2608 .await?
2609 .orders;
2610
2611 if let Some(order) = triggers
2612 .into_iter()
2613 .find(|order| order.label == label.as_str())
2614 {
2615 return Ok(Some(order));
2616 }
2617
2618 let instrument_name = cmd.instrument_id.map(|id| id.symbol.as_str().to_string());
2619 let mut pages = PaginationCursor::new();
2620
2621 loop {
2622 let mut params = DeriveGetOrderHistoryParams::new(
2623 subaccount_id,
2624 pages.page(),
2625 DERIVE_PRIVATE_PAGE_SIZE,
2626 );
2627
2628 if let Some(name) = instrument_name.as_deref() {
2629 params = params.with_instrument_name(name);
2630 }
2631
2632 let result = self.http_client.get_order_history(¶ms).await?;
2633 if pages.restart_if_changed(&result.pagination)? {
2634 continue;
2635 }
2636
2637 let more = pages.advance(
2638 &result.pagination,
2639 result.orders.iter().map(|order| order.order_id.as_str()),
2640 )?;
2641
2642 if let Some(order) = result
2643 .orders
2644 .into_iter()
2645 .find(|order| order.label == label.as_str())
2646 {
2647 return Ok(Some(order));
2648 }
2649
2650 if !more {
2651 return Ok(None);
2652 }
2653 }
2654 }
2655
2656 async fn generate_order_status_reports(
2657 &self,
2658 cmd: &GenerateOrderStatusReports,
2659 ) -> anyhow::Result<CollectedReports<OrderStatusReport>> {
2660 self.ensure_order_context()?;
2661 self.resolve_pending_modifies(cmd.instrument_id).await?;
2662 let orders = self.collect_order_records(cmd).await?;
2663
2664 let mut reports = self
2665 .generate_order_status_reports_from_records(
2666 cmd,
2667 orders,
2668 &AHashSet::new(),
2669 false,
2670 &[],
2671 false,
2672 )
2673 .await?;
2674 retain_order_status_reports(&mut reports.reports, cmd);
2675 Ok(reports)
2676 }
2677
2678 async fn generate_order_status_reports_from_records(
2679 &self,
2680 cmd: &GenerateOrderStatusReports,
2681 orders: Vec<DeriveOrder>,
2682 ambiguous_client_order_ids: &AHashSet<Ustr>,
2683 strict: bool,
2684 ancestors: &[DeriveOrder],
2685 recover_uncached: bool,
2686 ) -> anyhow::Result<CollectedReports<OrderStatusReport>> {
2687 self.ensure_order_context()?;
2688 let mut complete = true;
2689 let mut records_complete = true;
2690
2691 let ts_init = self.clock.get_time_ns();
2692
2693 let orders: Vec<DeriveOrder> = orders
2694 .into_iter()
2695 .filter(|order| {
2696 cmd.instrument_id.is_none_or(|instrument_id| {
2697 format_instrument_id(order.instrument_name)
2698 .map_or(true, |id| id == instrument_id)
2699 })
2700 })
2701 .collect();
2702
2703 let mut reports = Vec::with_capacity(orders.len());
2704
2705 let known_orders: Vec<_> = orders.iter().chain(ancestors).collect();
2706
2707 for order in &orders {
2708 observe_order_binding(&self.dispatch_state, order);
2709 ensure_bindings_resolved(&self.dispatch_state, cmd.instrument_id)?;
2710 let (price_precision, size_precision) = self.order_precision(order)?;
2711 complete &= price_precision.is_some();
2712
2713 match parse_derive_order_to_report_with_precision(
2714 order,
2715 self.account_id,
2716 price_precision,
2717 size_precision,
2718 ts_init,
2719 ) {
2720 Ok(mut report) => {
2721 validate_report_binding(
2722 &self.dispatch_state,
2723 report.client_order_id,
2724 report.venue_order_id,
2725 report.instrument_id,
2726 )?;
2727 observe_trigger_order(&self.dispatch_state, order, &report);
2728 if report
2729 .client_order_id
2730 .and_then(|cid| self.order_binding(cid))
2731 .is_none()
2732 && report.client_order_id.is_some_and(|client_order_id| {
2733 ambiguous_client_order_ids.contains(&client_order_id.inner())
2734 })
2735 {
2736 anyhow::ensure!(
2737 order.replaced_order_id.is_none() || report.order_status.is_closed(),
2738 "Ambiguous native replacement chain for {}",
2739 order.label,
2740 );
2741 report.client_order_id = None;
2742 }
2743
2744 reports.push(
2745 self.project_order_report(order, report, &known_orders, recover_uncached)
2746 .await?,
2747 );
2748 }
2749 Err(e) if cmd.open_only => {
2750 return Err(e.context(format!(
2751 "failed to parse active Derive order {:?} on {}",
2752 order.order_id, order.instrument_name,
2753 )));
2754 }
2755 Err(e) if strict => return Err(e),
2756 Err(e) => {
2757 records_complete = false;
2758 log::warn!(
2759 "Skipping Derive order {:?} on {:?} in status report: {:?}",
2760 order.order_id,
2761 order.instrument_name.as_str(),
2762 format!("{e:#}"),
2763 );
2764 }
2765 }
2766 }
2767
2768 self.ensure_order_context()?;
2769 let reports = deduplicate_order_status_reports(reports)?;
2770 Ok(CollectedReports {
2771 reports,
2772 complete: complete && records_complete,
2773 records_complete,
2774 })
2775 }
2776
2777 async fn resolve_pending_modifies(
2778 &self,
2779 instrument_id: Option<InstrumentId>,
2780 ) -> anyhow::Result<()> {
2781 while let Some(client_order_id) = {
2782 let _delivery = self.dispatch_state.delivery_guard();
2783 self.dispatch_state
2784 .unresolved_client_order_id(instrument_id)
2785 } {
2786 let identity = self
2787 .dispatch_state
2788 .identity(&client_order_id)
2789 .ok_or(UnresolvedOrderBinding(client_order_id))?;
2790 let parent = self
2791 .dispatch_state
2792 .pending_modify(&client_order_id)
2793 .ok_or(UnresolvedOrderBinding(client_order_id))?;
2794 let nonce = self
2795 .dispatch_state
2796 .modify_nonce(&client_order_id)
2797 .ok_or(UnresolvedOrderBinding(client_order_id))?;
2798 let ts_init = self.clock.get_time_ns();
2799 let mut candidate = None;
2800
2801 for open_only in [true, false] {
2802 let records = self
2803 .collect_order_records(&GenerateOrderStatusReports::new(
2804 UUID4::new(),
2805 ts_init,
2806 open_only,
2807 Some(identity.instrument_id),
2808 Some(UnixNanos::from(nonce.saturating_sub(NONCE_FUTURE_NS))),
2809 None,
2810 None,
2811 None,
2812 ))
2813 .await?;
2814 candidate = records.into_iter().find(|order| {
2815 order.label == client_order_id.as_str()
2816 && order.replaced_order_id.as_deref() == Some(parent.as_str())
2817 && order.nonce == nonce
2818 });
2819
2820 if candidate.is_some() {
2821 break;
2822 }
2823 }
2824
2825 let order = candidate.ok_or(UnresolvedOrderBinding(client_order_id))?;
2826 let venue_order_id = VenueOrderId::new_checked(&order.order_id)?;
2827 validate_replacement_order(
2828 &order,
2829 client_order_id,
2830 identity,
2831 &self.dispatch_state,
2832 venue_order_id,
2833 venue_order_id,
2834 )?;
2835 let _delivery = self.dispatch_state.delivery_guard();
2836 self.ensure_order_context()?;
2837 dispatch_order_row(
2838 &order,
2839 &self.emitter,
2840 self.account_id,
2841 self.clock,
2842 &self.dispatch_state,
2843 ts_init,
2844 );
2845 ensure_order_binding_resolved(&self.dispatch_state, client_order_id)?;
2846 }
2847
2848 Ok(())
2849 }
2850
2851 async fn collect_order_records(
2852 &self,
2853 cmd: &GenerateOrderStatusReports,
2854 ) -> anyhow::Result<Vec<DeriveOrder>> {
2855 let instrument_name = cmd.instrument_id.map(|id| id.symbol.as_str().to_string());
2856
2857 let orders = if cmd.open_only {
2858 let mut orders = self
2859 .http_client
2860 .get_open_orders(&DeriveGetOpenOrdersParams::new(self.subaccount_id))
2861 .await?
2862 .orders;
2863 orders.extend(
2864 self.http_client
2865 .get_trigger_orders(&DeriveGetTriggerOrdersParams::new(self.subaccount_id))
2866 .await?
2867 .orders,
2868 );
2869 orders
2870 } else {
2871 let start_ms = cmd.start.map(|t| t.as_millis() as i64);
2872 let end_ms = cmd.end.map(|t| t.as_millis() as i64);
2873 let mut pages = PaginationCursor::new();
2874 let mut collected = Vec::new();
2875
2876 loop {
2877 let mut params = DeriveGetOrderHistoryParams::new(
2878 self.subaccount_id,
2879 pages.page(),
2880 DERIVE_PRIVATE_PAGE_SIZE,
2881 )
2882 .with_window(start_ms, end_ms);
2883
2884 if let Some(name) = instrument_name.as_deref() {
2885 params = params.with_instrument_name(name);
2886 }
2887
2888 let result = self.http_client.get_order_history(¶ms).await?;
2889 if pages.restart_if_changed(&result.pagination)? {
2890 collected.clear();
2891 continue;
2892 }
2893
2894 let more = pages.advance(
2895 &result.pagination,
2896 result.orders.iter().map(|order| order.order_id.as_str()),
2897 )?;
2898 collected.extend(result.orders);
2899
2900 if !more {
2901 anyhow::ensure!(pages.is_complete(), "incomplete Derive order history");
2902 break;
2903 }
2904 }
2905
2906 collected
2907 };
2908
2909 Ok(orders)
2910 }
2911
2912 fn ensure_order_context(&self) -> anyhow::Result<()> {
2913 let _delivery = self.dispatch_state.delivery_guard();
2914 anyhow::ensure!(
2915 Arc::ptr_eq(
2916 &self.terminal_revision,
2917 &self.dispatch_state.terminal_revision()
2918 ),
2919 "Derive order context expired after terminal binding eviction; retry with a fresh cache snapshot",
2920 );
2921 Ok(())
2922 }
2923
2924 fn order_binding(&self, client_order_id: ClientOrderId) -> Option<OrderBinding> {
2925 self.dispatch_state
2926 .order_binding(&client_order_id)
2927 .or_else(|| self.orders.get(&client_order_id).cloned())
2928 }
2929
2930 async fn project_order_report(
2931 &self,
2932 order: &DeriveOrder,
2933 mut report: OrderStatusReport,
2934 known_orders: &[&DeriveOrder],
2935 recover_uncached: bool,
2936 ) -> anyhow::Result<OrderStatusReport> {
2937 self.ensure_order_context()?;
2938
2939 let Some(client_order_id) = report.client_order_id else {
2940 return Ok(report);
2941 };
2942
2943 let Some(binding) = self
2944 .order_binding(client_order_id)
2945 .filter(|binding| binding.identity.instrument_id == report.instrument_id)
2946 else {
2947 if recover_uncached
2948 && report.order_status.is_open()
2949 && order.replaced_order_id.is_some()
2950 {
2951 let identity = OrderIdentity {
2952 instrument_id: report.instrument_id,
2953 strategy_id: StrategyId::from("EXTERNAL"),
2954 order_side: report
2955 .order_side
2956 .context("missing native replacement side")?,
2957 order_type: report.order_type,
2958 };
2959
2960 let legs = replacement_orders(
2961 &self.http_client,
2962 &self.dispatch_state,
2963 client_order_id,
2964 identity,
2965 order.clone(),
2966 Vec::new(),
2967 known_orders,
2968 )
2969 .await
2970 .context(UnresolvedOrderBinding(client_order_id))?;
2971 self.project_replacement_history(order, &legs, &mut report)
2972 .context(UnresolvedOrderBinding(client_order_id))?;
2973 self.ensure_order_context()?;
2974 anyhow::ensure!(
2975 self.order_binding(client_order_id).is_none(),
2976 "Native replacement binding changed during report collection",
2977 );
2978 }
2979
2980 return Ok(report);
2981 };
2982
2983 let Some(current) = binding.venue_order_id else {
2984 return Err(UnresolvedOrderBinding(client_order_id).into());
2985 };
2986
2987 if report.venue_order_id != current {
2988 if !binding.venue_order_legs.contains(&report.venue_order_id) {
2989 return Err(UnresolvedOrderBinding(client_order_id).into());
2990 }
2991
2992 validate_replacement_order(
2993 order,
2994 client_order_id,
2995 binding.identity,
2996 &self.dispatch_state,
2997 report.venue_order_id,
2998 current,
2999 )?;
3000 return Ok(report);
3001 }
3002
3003 if binding.venue_order_legs.len() <= 1 && order.replaced_order_id.is_none() {
3004 return Ok(report);
3005 }
3006
3007 let legs = replacement_orders(
3008 &self.http_client,
3009 &self.dispatch_state,
3010 client_order_id,
3011 binding.identity,
3012 order.clone(),
3013 binding.venue_order_legs.clone(),
3014 &[],
3015 )
3016 .await?;
3017 self.project_replacement_history(order, &legs, &mut report)?;
3018 let _delivery = self.dispatch_state.delivery_guard();
3019 self.ensure_order_context()?;
3020 ensure_bindings_resolved(&self.dispatch_state, Some(report.instrument_id))?;
3021
3022 if self.order_binding(client_order_id).as_ref() != Some(&binding) {
3023 return Err(UnresolvedOrderBinding(client_order_id).into());
3024 }
3025
3026 Ok(report)
3027 }
3028
3029 fn project_replacement_history(
3030 &self,
3031 order: &DeriveOrder,
3032 legs: &[DeriveOrder],
3033 report: &mut OrderStatusReport,
3034 ) -> anyhow::Result<()> {
3035 let mut filled = Decimal::ZERO;
3036 let mut ancestor_filled = Decimal::ZERO;
3037 let mut notional = Decimal::ZERO;
3038 let mut average_complete = true;
3039
3040 for leg in legs {
3041 filled = filled
3042 .checked_add(leg.filled_amount)
3043 .context("native cumulative fills exceed decimal range")?;
3044
3045 if leg.order_id != order.order_id {
3046 ancestor_filled = ancestor_filled
3047 .checked_add(leg.filled_amount)
3048 .context("native ancestor fills exceed decimal range")?;
3049 }
3050
3051 if leg.filled_amount > Decimal::ZERO {
3052 if leg.average_price <= Decimal::ZERO {
3053 average_complete = false;
3054 } else {
3055 let value = leg
3056 .filled_amount
3057 .checked_mul(leg.average_price)
3058 .context("native fill notional exceeds decimal range")?;
3059 notional = notional
3060 .checked_add(value)
3061 .context("native cumulative notional exceeds decimal range")?;
3062 }
3063 }
3064 }
3065
3066 report.ts_accepted = crate::http::parse::ms_to_nanos(
3067 legs.iter()
3068 .map(|leg| leg.creation_timestamp)
3069 .min()
3070 .unwrap_or(order.creation_timestamp),
3071 );
3072 let quantity = order
3073 .amount
3074 .checked_add(ancestor_filled)
3075 .context("native logical quantity exceeds decimal range")?;
3076 let (_, size_precision) = self.order_precision(order)?;
3077 report.quantity = quantity_from_report_decimal(quantity, size_precision)?;
3078 report.filled_qty = quantity_from_report_decimal(filled, size_precision)?;
3079 report.avg_px = if filled > Decimal::ZERO && average_complete {
3080 Some(
3081 notional
3082 .checked_div(filled)
3083 .context("native cumulative average cannot be represented")?,
3084 )
3085 } else {
3086 None
3087 };
3088
3089 if report.order_status == OrderStatus::Accepted && filled > Decimal::ZERO {
3090 report.order_status = OrderStatus::PartiallyFilled;
3091 }
3092
3093 Ok(())
3094 }
3095
3096 async fn generate_fill_reports(
3097 &self,
3098 cmd: GenerateFillReports,
3099 retain_untracked: bool,
3100 ) -> anyhow::Result<CollectedReports<FillReport>> {
3101 self.resolve_pending_modifies(cmd.instrument_id).await?;
3102 let mut complete = true;
3103 let mut records_complete = true;
3104 let instrument_name = cmd.instrument_id.map(|id| id.symbol.as_str().to_string());
3105 let mut pages = PaginationCursor::new();
3106 let mut all_trades: Vec<DeriveTrade> = Vec::new();
3107
3108 loop {
3109 let mut params = DeriveGetTradeHistoryParams::new(
3110 self.subaccount_id,
3111 pages.page(),
3112 DERIVE_PRIVATE_PAGE_SIZE,
3113 )
3114 .with_window(
3115 cmd.start.map(|t| t.as_millis() as i64),
3116 cmd.end.map(|t| t.as_millis() as i64),
3117 );
3118
3119 if let Some(name) = instrument_name.as_deref() {
3120 params = params.with_instrument_name(name);
3121 }
3122
3123 let result = self.http_client.get_private_trade_history(¶ms).await?;
3124 if pages.restart_if_changed(&result.pagination)? {
3125 all_trades.clear();
3126 records_complete = true;
3127 continue;
3128 }
3129
3130 records_complete &= result.records_complete;
3131 let more = pages.advance(
3132 &result.pagination,
3133 result.trades.iter().map(|trade| trade.trade_id.as_str()),
3134 )?;
3135 all_trades.extend(result.trades);
3136
3137 if !more {
3138 break;
3139 }
3140 }
3141
3142 let ts_init = self.clock.get_time_ns();
3143 records_complete &= pages.is_complete();
3144 let venue_order_id_filter = cmd
3145 .venue_order_id
3146 .as_ref()
3147 .map(|id| id.as_str().to_string());
3148
3149 let mut reports = Vec::with_capacity(all_trades.len());
3150 let mut seen_trade_ids = AHashSet::new();
3151
3152 for trade in all_trades {
3153 if let Some(target) = venue_order_id_filter.as_deref()
3154 && !trade.order_id.is_empty()
3155 && trade.order_id != target
3156 {
3157 continue;
3158 }
3159
3160 let (price_precision, size_precision) =
3161 self.report_precision(trade.instrument_name.as_str(), false)?;
3162 complete &= price_precision.is_some();
3163
3164 match parse_derive_trade_to_fill_report_with_precision(
3165 &trade,
3166 self.account_id,
3167 Currency::USDC(),
3168 price_precision,
3169 size_precision,
3170 ts_init,
3171 ) {
3172 Ok(Some(report)) => {
3173 validate_report_binding(
3174 &self.dispatch_state,
3175 report.client_order_id,
3176 report.venue_order_id,
3177 report.instrument_id,
3178 )?;
3179 observe_trigger_fill(&self.dispatch_state, &report);
3180 let retain = retain_untracked
3181 && report
3182 .client_order_id
3183 .is_some_and(|id| self.order_binding(id).is_none());
3184
3185 if self.dispatch_state.contains_trade(&report.trade_id) && !retain
3186 || !seen_trade_ids.insert(report.trade_id)
3187 {
3188 log::debug!(
3189 "Skipping duplicate Derive fill (trade_id={}) in generate_fill_reports",
3190 report.trade_id,
3191 );
3192 continue;
3193 }
3194
3195 reports.push(report);
3196 }
3197 Ok(None) => records_complete = false,
3198 Err(e) if e.is::<CommissionError>() => {
3199 return Err(e.context(format!(
3200 "failed to construct Derive fill {:?} for order {:?} on {:?}",
3201 trade.trade_id,
3202 trade.order_id,
3203 trade.instrument_name.as_str(),
3204 )));
3205 }
3206 Err(e) => {
3207 records_complete = false;
3208 log::warn!(
3209 "Skipping Derive trade {:?} for order {:?} on {:?} in fill report: {:?}",
3210 trade.trade_id,
3211 trade.order_id,
3212 trade.instrument_name.as_str(),
3213 format!("{e:#}"),
3214 );
3215 }
3216 }
3217 }
3218
3219 Ok(CollectedReports {
3220 reports,
3221 complete: complete && records_complete,
3222 records_complete,
3223 })
3224 }
3225
3226 async fn generate_position_status_snapshot(
3227 &self,
3228 cmd: &GeneratePositionStatusReports,
3229 ) -> anyhow::Result<PositionStatusSnapshot> {
3230 let positions = self
3231 .http_client
3232 .get_positions(&DeriveGetPositionsParams::new(self.subaccount_id))
3233 .await?
3234 .positions;
3235 let ts_init = self.clock.get_time_ns();
3236 let mut reports = Vec::with_capacity(positions.len());
3237 let mut instruments = AHashSet::with_capacity(positions.len());
3238
3239 for position in positions {
3240 let instrument_id = format_instrument_id(position.instrument_name)?;
3241
3242 if let Some(target) = cmd.instrument_id
3243 && instrument_id != target
3244 {
3245 continue;
3246 }
3247
3248 instruments.insert(instrument_id);
3249
3250 let (_, size_precision) =
3251 self.report_precision(position.instrument_name.as_str(), true)?;
3252 let report = parse_derive_position_to_report_with_precision(
3253 &position,
3254 self.account_id,
3255 size_precision,
3256 ts_init,
3257 )?;
3258 reports.push(report);
3259 }
3260
3261 Ok(PositionStatusSnapshot {
3262 reports,
3263 instruments,
3264 })
3265 }
3266
3267 async fn generate_mass_status(
3268 &self,
3269 lookback_mins: Option<u64>,
3270 ts_now: UnixNanos,
3271 recover_uncached: bool,
3272 ) -> anyhow::Result<ExecutionMassStatus> {
3273 self.ensure_order_context()?;
3274 log::info!("Generating ExecutionMassStatus (lookback_mins={lookback_mins:?})");
3275
3276 let start = lookback_mins
3277 .map(DurationNanos::try_from_mins)
3278 .transpose()?
3279 .map(|lookback| ts_now.saturating_sub(lookback));
3280
3281 let open_order_cmd = GenerateOrderStatusReports::new(
3282 UUID4::new(),
3283 ts_now,
3284 true,
3285 None,
3286 None,
3287 None,
3288 None,
3289 None,
3290 );
3291
3292 let history_order_cmd = GenerateOrderStatusReports::new(
3293 UUID4::new(),
3294 ts_now,
3295 false,
3296 None,
3297 start,
3298 None,
3299 None,
3300 None,
3301 );
3302 let fill_cmd =
3303 GenerateFillReports::new(UUID4::new(), ts_now, None, None, start, None, None, None);
3304 let position_cmd =
3305 GeneratePositionStatusReports::new(UUID4::new(), ts_now, None, None, None, None, None);
3306
3307 let (history, open, fills, positions) = tokio::join!(
3308 async {
3309 self.ensure_order_context()?;
3310 self.resolve_pending_modifies(None).await?;
3311 self.collect_order_records(&history_order_cmd)
3312 .await
3313 .map(|reports| CollectedReports {
3314 reports,
3315 complete: true,
3316 records_complete: true,
3317 })
3318 },
3319 async {
3320 self.ensure_order_context()?;
3321 self.resolve_pending_modifies(None).await?;
3322 self.collect_order_records(&open_order_cmd)
3323 .await
3324 .map(|reports| CollectedReports {
3325 reports,
3326 complete: true,
3327 records_complete: true,
3328 })
3329 },
3330 self.generate_fill_reports(fill_cmd, recover_uncached),
3331 self.generate_position_status_snapshot(&position_cmd),
3332 );
3333 let history = collect_mass_reports(history, "order history", start.is_some())?;
3334 let open = collect_mass_reports(open, "open orders", start.is_some())?;
3335
3336 let fills = match fills {
3337 Err(e)
3338 if e.downcast_ref::<DeriveHttpError>().is_some_and(|cause| {
3339 matches!(
3340 cause,
3341 DeriveHttpError::Decode(_) | DeriveHttpError::Serde(_)
3342 )
3343 }) =>
3344 {
3345 return Err(e);
3346 }
3347 other => collect_mass_reports(other, "fill history", start.is_some())?,
3348 };
3349
3350 let positions_complete = positions.is_ok();
3351
3352 let position_snapshot = match positions {
3353 Ok(snapshot) => snapshot,
3354 Err(e) if start.is_some() && e.is::<DeriveHttpError>() => {
3355 log::warn!("Derive position source failed: {e:#}");
3356
3357 PositionStatusSnapshot {
3358 reports: Vec::new(),
3359 instruments: AHashSet::new(),
3360 }
3361 }
3362 Err(e) => return Err(e),
3363 };
3364
3365 let mut records_complete = history.records_complete
3366 && open.records_complete
3367 && fills.records_complete
3368 && positions_complete;
3369 let mut reports_complete =
3370 history.complete && open.complete && fills.complete && positions_complete;
3371 let mut fill_reports = fills.reports;
3372 let fill_order_ids: AHashSet<_> = fill_reports
3373 .iter()
3374 .map(|report| report.venue_order_id)
3375 .collect();
3376 let companions = self
3377 .collect_companion_order_records(&history, &open.reports, &fill_reports)
3378 .await?;
3379 reports_complete &= companions.complete;
3380 records_complete &= companions.records_complete;
3381 let ambiguous_client_order_ids = ambiguous_history_client_order_ids(
3382 history
3383 .reports
3384 .iter()
3385 .chain(open.reports.iter())
3386 .chain(companions.reports.iter()),
3387 );
3388 let ambiguous_open_client_order_ids = AHashSet::new();
3389
3390 let replaced_order_ids: AHashSet<_> = history
3391 .reports
3392 .iter()
3393 .chain(open.reports.iter())
3394 .chain(companions.reports.iter())
3395 .filter_map(|order| {
3396 order
3397 .replaced_order_id
3398 .as_deref()
3399 .map(|id| (order.label, id))
3400 })
3401 .collect();
3402
3403 anyhow::ensure!(
3404 !recover_uncached
3405 || !open.reports.iter().any(|order| {
3406 (order.replaced_order_id.is_some()
3407 && ambiguous_client_order_ids.contains(&order.label)
3408 || replaced_order_ids.contains(&(order.label, order.order_id.as_str())))
3409 && ClientOrderId::new_checked(order.label)
3410 .is_ok_and(|cid| self.order_binding(cid).is_none())
3411 }),
3412 "Invalid native replacement head in open orders",
3413 );
3414 let ancestors: Vec<_> = history
3415 .reports
3416 .iter()
3417 .chain(open.reports.iter())
3418 .chain(companions.reports.iter())
3419 .cloned()
3420 .collect();
3421 let (history, open, companions) = tokio::join!(
3422 self.generate_order_status_reports_from_records(
3423 &history_order_cmd,
3424 history.reports,
3425 &ambiguous_client_order_ids,
3426 false,
3427 &ancestors,
3428 recover_uncached,
3429 ),
3430 self.generate_order_status_reports_from_records(
3431 &open_order_cmd,
3432 open.reports,
3433 &ambiguous_open_client_order_ids,
3434 false,
3435 &ancestors,
3436 recover_uncached,
3437 ),
3438 self.generate_order_status_reports_from_records(
3439 &history_order_cmd,
3440 companions.reports,
3441 &ambiguous_client_order_ids,
3442 true,
3443 &ancestors,
3444 recover_uncached,
3445 ),
3446 );
3447 let history = collect_mass_reports(history, "order history", start.is_some())?;
3448 let open = collect_mass_reports(open, "open orders", start.is_some())?;
3449 let companions = companions?;
3450 reports_complete &= history.complete && open.complete && companions.complete;
3451 records_complete &=
3452 history.records_complete && open.records_complete && companions.records_complete;
3453 anyhow::ensure!(
3454 !recover_uncached
3455 || records_complete
3456 || !ancestors.iter().any(|order| {
3457 order.replaced_order_id.is_some()
3458 && derive_status_to_nautilus(
3459 order.order_status,
3460 order.filled_amount,
3461 order.amount,
3462 )
3463 .is_ok_and(|status| status.is_open())
3464 && ClientOrderId::new_checked(order.label)
3465 .is_ok_and(|id| self.order_binding(id).is_none())
3466 }),
3467 "Incomplete reports for uncached replacement",
3468 );
3469 let open_order_reports = open.reports;
3470
3471 if recover_uncached
3472 && let Some(earliest) =
3473 self.replacement_lookback_start(start, &open_order_reports, &ancestors)
3474 {
3475 let lookback = ts_now.saturating_duration_since(earliest).as_mins() + 1;
3476 return Box::pin(self.generate_mass_status(Some(lookback), ts_now, recover_uncached))
3477 .await;
3478 }
3479
3480 let (mut history_order_reports, retained_companion_reports): (Vec<_>, Vec<_>) = history
3481 .reports
3482 .into_iter()
3483 .partition(|report| !fill_order_ids.contains(&report.venue_order_id));
3484 retain_order_status_reports(&mut history_order_reports, &history_order_cmd);
3485 history_order_reports.extend(retained_companion_reports);
3486 history_order_reports.extend(companions.reports);
3487 history_order_reports.sort_by_key(|report| {
3488 let is_current = report
3489 .client_order_id
3490 .and_then(|cid| self.order_binding(cid))
3491 .is_some_and(|binding| binding.venue_order_id == Some(report.venue_order_id));
3492 (is_current, report.ts_last)
3493 });
3494
3495 let order_evidence: AHashSet<_> = history_order_reports
3496 .iter()
3497 .chain(open_order_reports.iter())
3498 .map(|report| (report.venue_order_id, report.instrument_id))
3499 .collect();
3500
3501 for report in &fill_reports {
3502 if !order_evidence.contains(&(report.venue_order_id, report.instrument_id)) {
3503 reports_complete = false;
3504 records_complete = false;
3505 log::warn!(
3506 "Derive fill {} for order {} on {} has no matching order report; coverage is incomplete",
3507 report.trade_id,
3508 report.venue_order_id,
3509 report.instrument_id,
3510 );
3511 }
3512 }
3513
3514 anyhow::ensure!(
3515 start.is_some() || records_complete,
3516 "incomplete unbounded Derive report coverage"
3517 );
3518
3519 let detached_order_ids: AHashSet<VenueOrderId> = history_order_reports
3520 .iter()
3521 .chain(open_order_reports.iter())
3522 .filter(|report| report.client_order_id.is_none())
3523 .map(|report| report.venue_order_id)
3524 .collect();
3525
3526 for report in &mut fill_reports {
3527 if detached_order_ids.contains(&report.venue_order_id) {
3528 report.client_order_id = None;
3529 }
3530 }
3531
3532 log::info!(
3533 "Received {} historical OrderStatusReports",
3534 history_order_reports.len()
3535 );
3536 log::info!(
3537 "Received {} open OrderStatusReports",
3538 open_order_reports.len()
3539 );
3540 log::info!("Received {} FillReports", fill_reports.len());
3541 log::info!(
3542 "Received {} PositionReports",
3543 position_snapshot.reports.len()
3544 );
3545
3546 let mut touched_instruments = AHashSet::new();
3547 for report in history_order_reports
3548 .iter()
3549 .chain(open_order_reports.iter())
3550 {
3551 touched_instruments.insert(report.instrument_id);
3552 }
3553
3554 for report in &fill_reports {
3555 touched_instruments.insert(report.instrument_id);
3556 }
3557
3558 let PositionStatusSnapshot {
3559 reports: position_reports,
3560 instruments: position_instruments,
3561 } = position_snapshot;
3562 let mut mass_status =
3563 ExecutionMassStatus::new(self.client_id, self.account_id, *DERIVE_VENUE, ts_now, None);
3564 mass_status.set_report_window(start, reports_complete || start.is_none());
3565
3566 let order_reports = deduplicate_order_status_reports(
3567 history_order_reports
3568 .into_iter()
3569 .chain(open_order_reports)
3570 .collect(),
3571 )?;
3572
3573 if recover_uncached {
3574 self.ensure_replacement_fills(&order_reports, &fill_reports, &ancestors)?;
3575 }
3576
3577 mass_status.add_order_reports(order_reports);
3578 mass_status.add_fill_reports(fill_reports);
3579 mass_status.add_position_reports(position_reports);
3580
3581 if positions_complete {
3582 add_missing_flat_position_reports(
3583 &mut mass_status,
3584 self.account_id,
3585 touched_instruments,
3586 &position_instruments,
3587 &self.instruments,
3588 ts_now,
3589 );
3590 }
3591
3592 self.ensure_order_context()?;
3593 Ok(mass_status)
3594 }
3595
3596 fn ensure_replacement_fills(
3597 &self,
3598 reports: &[OrderStatusReport],
3599 fills: &[FillReport],
3600 records: &[DeriveOrder],
3601 ) -> anyhow::Result<()> {
3602 for current in reports {
3603 if !current.order_status.is_open()
3604 || current
3605 .client_order_id
3606 .is_none_or(|id| self.order_binding(id).is_some())
3607 || !records.iter().any(|order| {
3608 order.order_id == current.venue_order_id.as_str()
3609 && order.replaced_order_id.is_some()
3610 })
3611 {
3612 continue;
3613 }
3614
3615 let mut filled = Decimal::ZERO;
3616
3617 for report in reports {
3618 if report.client_order_id != current.client_order_id {
3619 continue;
3620 }
3621
3622 let native_filled: Decimal = fills
3623 .iter()
3624 .filter(|fill| fill.venue_order_id == report.venue_order_id)
3625 .map(|fill| fill.last_qty.as_decimal())
3626 .sum();
3627 anyhow::ensure!(
3628 report.venue_order_id == current.venue_order_id
3629 || native_filled == report.filled_qty.as_decimal(),
3630 "Incomplete native fills for uncached replacement {} at {}",
3631 current.venue_order_id,
3632 report.venue_order_id,
3633 );
3634 filled += native_filled;
3635 }
3636
3637 anyhow::ensure!(
3638 filled == current.filled_qty.as_decimal(),
3639 "Incomplete native fills for uncached replacement {}",
3640 current.venue_order_id,
3641 );
3642 }
3643
3644 Ok(())
3645 }
3646
3647 fn replacement_lookback_start(
3648 &self,
3649 start: Option<UnixNanos>,
3650 open: &[OrderStatusReport],
3651 records: &[DeriveOrder],
3652 ) -> Option<UnixNanos> {
3653 let start = start?;
3654 open.iter()
3655 .filter(|report| {
3656 report.ts_accepted < start
3657 && report
3658 .client_order_id
3659 .is_some_and(|id| self.order_binding(id).is_none())
3660 && records.iter().any(|order| {
3661 order.order_id == report.venue_order_id.as_str()
3662 && order.replaced_order_id.is_some()
3663 })
3664 })
3665 .map(|report| report.ts_accepted)
3666 .min()
3667 }
3668
3669 async fn collect_companion_order_records(
3670 &self,
3671 history: &CollectedReports<DeriveOrder>,
3672 open: &[DeriveOrder],
3673 fills: &[FillReport],
3674 ) -> anyhow::Result<CollectedReports<DeriveOrder>> {
3675 if !history.records_complete {
3676 return Ok(CollectedReports {
3677 reports: Vec::new(),
3678 complete: false,
3679 records_complete: false,
3680 });
3681 }
3682
3683 let order_evidence: AHashSet<_> = history
3684 .reports
3685 .iter()
3686 .chain(open.iter())
3687 .filter_map(|order| VenueOrderId::new_checked(&order.order_id).ok())
3688 .collect();
3689 let mut queried_orders: AHashMap<VenueOrderId, Option<usize>> = AHashMap::new();
3690 let mut orders = Vec::new();
3691 let mut records_complete = true;
3692
3693 for fill in fills {
3694 if order_evidence.contains(&fill.venue_order_id) {
3695 continue;
3696 }
3697
3698 let client_order_id = self
3699 .orders
3700 .iter()
3701 .find(|(_, binding)| binding.venue_order_legs.contains(&fill.venue_order_id))
3702 .map(|(cid, _)| *cid)
3703 .or_else(|| {
3704 fill.client_order_id.filter(|cid| {
3705 self.order_binding(*cid).is_some_and(|binding| {
3706 binding.venue_order_legs.contains(&fill.venue_order_id)
3707 })
3708 })
3709 });
3710
3711 let index = match queried_orders.get(&fill.venue_order_id) {
3712 Some(Some(index)) => *index,
3713 Some(None) => continue,
3714 None => {
3715 let order = match fetch_order_record(
3716 &self.http_client,
3717 self.subaccount_id,
3718 fill.venue_order_id,
3719 Some(fill.instrument_id),
3720 )
3721 .await
3722 {
3723 Ok(order) => order,
3724 Err(e)
3725 if client_order_id.is_none()
3726 && e.downcast_ref::<DeriveHttpError>().is_some_and(|cause| {
3727 matches!(cause, DeriveHttpError::JsonRpc { code: 11006, .. })
3728 }) =>
3729 {
3730 records_complete = false;
3731 queried_orders.insert(fill.venue_order_id, None);
3732 log::warn!(
3733 "Derive fill companion {} on {} is unavailable: {e:#}",
3734 fill.venue_order_id,
3735 fill.instrument_id,
3736 );
3737 continue;
3738 }
3739 Err(e) => return Err(e),
3740 };
3741
3742 let index = orders.len();
3743 orders.push(order);
3744 queried_orders.insert(fill.venue_order_id, Some(index));
3745 index
3746 }
3747 };
3748
3749 self.validate_companion_order(&orders[index], fill, client_order_id)?;
3750 }
3751
3752 Ok(CollectedReports {
3753 reports: orders,
3754 complete: records_complete,
3755 records_complete,
3756 })
3757 }
3758
3759 fn validate_companion_order(
3760 &self,
3761 order: &DeriveOrder,
3762 fill: &FillReport,
3763 client_order_id: Option<ClientOrderId>,
3764 ) -> anyhow::Result<()> {
3765 let order_side = match order.direction {
3766 DeriveOrderSide::Buy => OrderSide::Buy,
3767 DeriveOrderSide::Sell => OrderSide::Sell,
3768 };
3769
3770 anyhow::ensure!(
3771 order.order_id == fill.venue_order_id.as_str()
3772 && self.dispatch_state.owns_subaccount(order.subaccount_id)
3773 && format_instrument_id(order.instrument_name)? == fill.instrument_id
3774 && order_side == fill.order_side,
3775 "Native companion order identity does not match",
3776 );
3777
3778 if let Some(client_order_id) = client_order_id {
3779 let binding = self
3780 .order_binding(client_order_id)
3781 .ok_or(UnresolvedOrderBinding(client_order_id))?;
3782 anyhow::ensure!(
3783 order_response_matches(
3784 order,
3785 client_order_id,
3786 binding.identity.instrument_id,
3787 &self.dispatch_state,
3788 ) && fill
3789 .client_order_id
3790 .is_none_or(|cid| cid == client_order_id),
3791 "Native companion order identity does not match",
3792 );
3793 let current = binding
3794 .venue_order_id
3795 .ok_or(UnresolvedOrderBinding(client_order_id))?;
3796 validate_replacement_order(
3797 order,
3798 client_order_id,
3799 binding.identity,
3800 &self.dispatch_state,
3801 fill.venue_order_id,
3802 current,
3803 )?;
3804 }
3805
3806 Ok(())
3807 }
3808
3809 fn order_precision(&self, order: &DeriveOrder) -> anyhow::Result<(Option<u8>, Option<u8>)> {
3810 let active =
3811 derive_status_to_nautilus(order.order_status, order.filled_amount, order.amount)
3812 .is_ok_and(|status| !status.is_closed());
3813 self.report_precision(order.instrument_name.as_str(), active)
3814 }
3815
3816 fn report_precision(
3817 &self,
3818 instrument_name: &str,
3819 required: bool,
3820 ) -> anyhow::Result<(Option<u8>, Option<u8>)> {
3821 let precision = report_precision(&self.dispatch_state, instrument_name);
3822 if precision.0.is_some() {
3823 return Ok(precision);
3824 }
3825
3826 if required {
3827 let instrument_id = format_instrument_id(instrument_name)?;
3828 anyhow::bail!("missing Derive instrument metadata for {instrument_id}");
3829 }
3830
3831 log::warn!(
3832 "Missing Derive instrument metadata for historical record on {instrument_name}; coverage is incomplete"
3833 );
3834 Ok(precision)
3835 }
3836}
3837
3838fn ambiguous_history_client_order_ids<'a>(
3839 orders: impl Iterator<Item = &'a DeriveOrder>,
3840) -> AHashSet<Ustr> {
3841 let mut orders_by_label: AHashMap<Ustr, AHashMap<&str, Option<&str>>> = AHashMap::new();
3842
3843 for order in orders {
3844 if order.label.is_empty() {
3845 continue;
3846 }
3847
3848 orders_by_label
3849 .entry(order.label)
3850 .or_default()
3851 .insert(order.order_id.as_str(), order.replaced_order_id.as_deref());
3852 }
3853
3854 let mut ambiguous_client_order_ids = AHashSet::new();
3855
3856 for (label, orders_by_id) in orders_by_label {
3857 if orders_by_id.len() < 2 {
3858 continue;
3859 }
3860
3861 let predecessors: AHashMap<&str, &str> = orders_by_id
3862 .iter()
3863 .filter_map(|(order_id, replaced_order_id)| {
3864 let replaced_order_id = (*replaced_order_id)?;
3865 orders_by_id
3866 .contains_key(replaced_order_id)
3867 .then_some((*order_id, replaced_order_id))
3868 })
3869 .collect();
3870
3871 let predecessor_ids: AHashSet<&str> = predecessors.values().copied().collect();
3872 let heads: Vec<&str> = orders_by_id
3873 .keys()
3874 .copied()
3875 .filter(|order_id| !predecessor_ids.contains(order_id))
3876 .collect();
3877
3878 let is_linear_chain = predecessors.len() + 1 == orders_by_id.len()
3880 && predecessor_ids.len() == predecessors.len()
3881 && heads.len() == 1
3882 && {
3883 let mut visited = AHashSet::new();
3884 let mut current = Some(heads[0]);
3885 while let Some(order_id) = current {
3886 if !visited.insert(order_id) {
3887 break;
3888 }
3889
3890 current = predecessors.get(order_id).copied();
3891 }
3892
3893 visited.len() == orders_by_id.len()
3894 };
3895
3896 if !is_linear_chain {
3897 ambiguous_client_order_ids.insert(label);
3898 }
3899 }
3900
3901 ambiguous_client_order_ids
3902}
3903
3904#[derive(Debug, thiserror::Error)]
3905#[error("unresolved Derive venue binding for {0}")]
3906struct UnresolvedOrderBinding(ClientOrderId);
3907
3908fn validate_report_binding(
3909 dispatch_state: &WsDispatchState,
3910 client_order_id: Option<ClientOrderId>,
3911 venue_order_id: VenueOrderId,
3912 instrument_id: InstrumentId,
3913) -> anyhow::Result<()> {
3914 let _delivery = dispatch_state.delivery_guard();
3915 if let Some((client_order_id, _)) =
3916 tracked_order_identity(client_order_id, instrument_id, dispatch_state)
3917 {
3918 if !dispatch_state.knows_venue_leg(&client_order_id, venue_order_id) {
3919 dispatch_state.defer_binding(client_order_id, venue_order_id);
3920 }
3921
3922 if dispatch_state.binding_unresolved(&client_order_id) {
3923 return Err(UnresolvedOrderBinding(client_order_id).into());
3924 }
3925 }
3926
3927 Ok(())
3928}
3929
3930fn ensure_bindings_resolved(
3931 dispatch_state: &WsDispatchState,
3932 instrument_id: Option<InstrumentId>,
3933) -> anyhow::Result<()> {
3934 let _delivery = dispatch_state.delivery_guard();
3935 if let Some(client_order_id) = dispatch_state.unresolved_client_order_id(instrument_id) {
3936 return Err(UnresolvedOrderBinding(client_order_id).into());
3937 }
3938
3939 Ok(())
3940}
3941
3942fn ensure_order_binding_resolved(
3943 dispatch_state: &WsDispatchState,
3944 client_order_id: ClientOrderId,
3945) -> anyhow::Result<()> {
3946 let _delivery = dispatch_state.delivery_guard();
3947 if dispatch_state.binding_unresolved(&client_order_id)
3948 || dispatch_state.pending_modify(&client_order_id).is_some()
3949 {
3950 return Err(UnresolvedOrderBinding(client_order_id).into());
3951 }
3952
3953 Ok(())
3954}
3955
3956fn emit_trigger_cancel_result(
3957 canceled_order: &DeriveOrder,
3958 expected_cancel_id: Option<VenueOrderId>,
3959 cmd: &CancelOrder,
3960 emitter: &ExecutionEventEmitter,
3961 dispatch_state: &WsDispatchState,
3962 account_id: AccountId,
3963 clock: &'static AtomicTime,
3964) -> anyhow::Result<()> {
3965 let client_order_id = cmd.client_order_id;
3966
3967 if !order_response_matches(
3968 canceled_order,
3969 cmd.client_order_id,
3970 cmd.instrument_id,
3971 dispatch_state,
3972 ) || expected_cancel_id.is_none_or(|id| id.as_str() != canceled_order.order_id)
3973 || canceled_order.order_status != DeriveOrderStatus::Cancelled
3974 {
3975 log::warn!(
3976 "Derive trigger cancellation response does not match {client_order_id}, awaiting reconciliation"
3977 );
3978 return Ok(());
3979 }
3980
3981 let Ok(canceled_venue_order_id) = VenueOrderId::new_checked(canceled_order.order_id.as_str())
3982 else {
3983 log::warn!(
3984 "Derive cancel outcome for {:?} on {:?} has an invalid response order_id; awaiting reconciliation",
3985 cmd.client_order_id.as_str(),
3986 cmd.instrument_id.to_string(),
3987 );
3988 return Ok(());
3989 };
3990
3991 let ts = clock.get_time_ns();
3992
3993 if !ensure_canceled_emitted(
3994 emitter,
3995 dispatch_state,
3996 cmd.client_order_id,
3997 OrderIdentity {
3998 instrument_id: cmd.instrument_id,
3999 strategy_id: cmd.strategy_id,
4000 order_side: match canceled_order.direction {
4001 DeriveOrderSide::Buy => OrderSide::Buy,
4002 DeriveOrderSide::Sell => OrderSide::Sell,
4003 },
4004 order_type: derive_order_type_to_nautilus_for_order(
4005 canceled_order.order_type,
4006 canceled_order.trigger_type,
4007 )?,
4008 },
4009 canceled_venue_order_id,
4010 account_id,
4011 ts,
4012 ts,
4013 ) {
4014 return Ok(());
4015 }
4016
4017 dispatch_state.forget(&cmd.client_order_id);
4018 Ok(())
4019}
4020
4021async fn resolve_trigger_cancel_id(
4022 http_client: &DeriveHttpClient,
4023 dispatch_state: &WsDispatchState,
4024 subaccount_id: u64,
4025 client_order_id: ClientOrderId,
4026 venue_symbol: &str,
4027) -> Result<VenueOrderId, String> {
4028 let result = http_client
4029 .get_trigger_orders(&DeriveGetTriggerOrdersParams::new(subaccount_id))
4030 .await;
4031
4032 if let Some(venue_order_id) = dispatch_state.bound_venue_order_id(&client_order_id) {
4033 return Ok(venue_order_id);
4034 }
4035
4036 let orders = result
4037 .map_err(|e| format!("failed to resolve trigger order by label: {e}"))?
4038 .orders;
4039
4040 let order = orders
4041 .into_iter()
4042 .find(|order| {
4043 order.label == client_order_id.as_str() && order.instrument_name == venue_symbol
4044 })
4045 .ok_or_else(|| "trigger order not found for client_order_id".to_string())?;
4046
4047 VenueOrderId::new_checked(&order.order_id).map_err(|e| e.to_string())
4048}
4049
4050fn observe_order_binding(dispatch_state: &WsDispatchState, order: &DeriveOrder) {
4051 let _delivery = dispatch_state.delivery_guard();
4052
4053 let (Ok(client_order_id), Ok(instrument_id), Ok(venue_order_id)) = (
4054 ClientOrderId::new_checked(order.label),
4055 format_instrument_id(order.instrument_name),
4056 VenueOrderId::new_checked(&order.order_id),
4057 ) else {
4058 return;
4059 };
4060
4061 if tracked_order_identity(Some(client_order_id), instrument_id, dispatch_state).is_some()
4062 && dispatch_state
4063 .bound_venue_order_id(&client_order_id)
4064 .is_some()
4065 && !dispatch_state.knows_venue_leg(&client_order_id, venue_order_id)
4066 {
4067 dispatch_state.defer_binding(client_order_id, venue_order_id);
4068 }
4069}
4070
4071fn observe_trigger_order(
4072 dispatch_state: &WsDispatchState,
4073 order: &DeriveOrder,
4074 report: &OrderStatusReport,
4075) {
4076 let _delivery = dispatch_state.delivery_guard();
4077 if !dispatch_state.owns_subaccount(order.subaccount_id)
4078 || !matches!(
4079 order.order_status,
4080 DeriveOrderStatus::Open | DeriveOrderStatus::Filled
4081 )
4082 {
4083 return;
4084 }
4085
4086 if let Some((client_order_id, identity)) =
4087 tracked_order_identity(report.client_order_id, report.instrument_id, dispatch_state)
4088 && is_derive_trigger_order_type(identity.order_type)
4089 && report.order_type == identity.order_type
4090 && report.order_side == Some(identity.order_side)
4091 && dispatch_state
4092 .bound_venue_order_id(&client_order_id)
4093 .is_none_or(|id| id == report.venue_order_id)
4094 {
4095 dispatch_state.mark_trigger_active(client_order_id);
4096 }
4097}
4098
4099fn observe_trigger_fill(dispatch_state: &WsDispatchState, report: &FillReport) {
4100 let _delivery = dispatch_state.delivery_guard();
4101 if let Some((client_order_id, identity)) =
4102 tracked_order_identity(report.client_order_id, report.instrument_id, dispatch_state)
4103 && is_derive_trigger_order_type(identity.order_type)
4104 && report.order_side == identity.order_side
4105 && dispatch_state
4106 .bound_venue_order_id(&client_order_id)
4107 .is_none_or(|id| id == report.venue_order_id)
4108 {
4109 dispatch_state.mark_trigger_active(client_order_id);
4110 }
4111}
4112
4113struct CollectedReports<T> {
4114 reports: Vec<T>,
4115 complete: bool,
4116 records_complete: bool,
4117}
4118
4119impl<T> CollectedReports<T> {
4120 fn into_complete(self, source: &str) -> anyhow::Result<Vec<T>> {
4121 anyhow::ensure!(self.complete, "incomplete Derive {source} report coverage");
4122 Ok(self.reports)
4123 }
4124}
4125
4126fn collect_mass_reports<T>(
4127 result: anyhow::Result<CollectedReports<T>>,
4128 source: &str,
4129 allow_partial: bool,
4130) -> anyhow::Result<CollectedReports<T>> {
4131 match result {
4132 Ok(collection) => Ok(collection),
4133 Err(e)
4134 if !allow_partial || e.is::<CommissionError>() || e.is::<UnresolvedOrderBinding>() =>
4135 {
4136 Err(e)
4137 }
4138 Err(e) => {
4139 log::warn!("Derive {source} source failed: {e:#}");
4140 Ok(CollectedReports {
4141 reports: Vec::new(),
4142 complete: false,
4143 records_complete: false,
4144 })
4145 }
4146 }
4147}
4148
4149struct PositionStatusSnapshot {
4150 reports: Vec<PositionStatusReport>,
4151 instruments: AHashSet<InstrumentId>,
4152}
4153
4154fn ws_command_failure(error: &DeriveWsError) -> (CommandFailure, bool) {
4155 let (reason, due_post_only) = ws_rejection_reason(error);
4156 let failure = if is_write_outcome_ambiguous_ws(error) {
4157 CommandFailure::ambiguous(reason)
4158 } else if matches!(error, DeriveWsError::JsonRpc { .. }) {
4159 CommandFailure::venue_rejected(reason)
4160 } else {
4161 CommandFailure::not_sent(reason)
4162 };
4163
4164 log::warn!("Derive order command failed: {}", error.redacted_message());
4165 (failure, due_post_only)
4166}
4167
4168fn command_rejection_reason(failure: CommandFailure) -> Option<String> {
4169 match failure {
4170 CommandFailure::NotSent(reason) | CommandFailure::VenueRejected(reason) => {
4171 Some(strategy_rejection_reason(&reason))
4172 }
4173 CommandFailure::Ambiguous(_) => None,
4174 }
4175}
4176
4177fn emit_submit_failure(
4178 emitter: &ExecutionEventEmitter,
4179 order: &OrderAny,
4180 failure: CommandFailure,
4181 ts: UnixNanos,
4182 due_post_only: bool,
4183) {
4184 if let Some(reason) = command_rejection_reason(failure) {
4185 emitter.emit_order_rejected(order, &reason, ts, due_post_only);
4186 }
4187}
4188
4189fn emit_cancel_failure(
4190 emitter: &ExecutionEventEmitter,
4191 strategy_id: StrategyId,
4192 instrument_id: InstrumentId,
4193 client_order_id: ClientOrderId,
4194 venue_order_id: Option<VenueOrderId>,
4195 failure: CommandFailure,
4196 ts: UnixNanos,
4197) {
4198 if let Some(reason) = command_rejection_reason(failure) {
4199 emitter.emit_order_cancel_rejected_event(
4200 strategy_id,
4201 instrument_id,
4202 client_order_id,
4203 venue_order_id,
4204 &reason,
4205 ts,
4206 );
4207 }
4208}
4209
4210fn emit_modify_failure(
4211 emitter: &ExecutionEventEmitter,
4212 strategy_id: StrategyId,
4213 instrument_id: InstrumentId,
4214 client_order_id: ClientOrderId,
4215 venue_order_id: Option<VenueOrderId>,
4216 failure: CommandFailure,
4217 ts: UnixNanos,
4218) {
4219 if let Some(reason) = command_rejection_reason(failure) {
4220 emitter.emit_order_modify_rejected_event(
4221 strategy_id,
4222 instrument_id,
4223 client_order_id,
4224 venue_order_id,
4225 &reason,
4226 ts,
4227 );
4228 }
4229}
4230
4231fn ws_rejection_reason(error: &DeriveWsError) -> (String, bool) {
4232 match error {
4233 DeriveWsError::JsonRpc { code, message, .. } => (
4234 format!("{code}: {}", strategy_rejection_reason(message))
4235 .chars()
4236 .take(STRATEGY_REASON_MAX_CHARS)
4237 .collect(),
4238 derive_rejection_due_post_only(Some(*code), message),
4239 ),
4240 DeriveWsError::NotConnected => ("WebSocket client is not connected".to_string(), false),
4241 DeriveWsError::Auth(_) | DeriveWsError::Authentication { .. } => {
4242 ("Session authentication failed".to_string(), false)
4243 }
4244 DeriveWsError::MissingCredentials { .. } => {
4245 ("Session credentials are missing".to_string(), false)
4246 }
4247 DeriveWsError::Subscription { .. } => ("Session subscription failed".to_string(), false),
4248 DeriveWsError::RateLimited { .. } => {
4249 ("Local pacing reservation expired".to_string(), false)
4250 }
4251 DeriveWsError::Transport(_) => ("Transport outcome is unconfirmed".to_string(), false),
4252 DeriveWsError::Serde(_) => ("Order response could not be decoded".to_string(), false),
4253 DeriveWsError::Timeout { .. } | DeriveWsError::RequestCancelled { .. } => {
4254 ("Order response is unconfirmed".to_string(), false)
4255 }
4256 }
4257}
4258
4259fn add_missing_flat_position_reports(
4260 mass_status: &mut ExecutionMassStatus,
4261 account_id: AccountId,
4262 touched_instruments: AHashSet<InstrumentId>,
4263 position_instruments: &AHashSet<InstrumentId>,
4264 instruments: &AtomicMap<InstrumentId, DeriveInstrument>,
4265 ts_init: UnixNanos,
4266) {
4267 let mut flat_reports = Vec::new();
4268
4269 let mut touched_instruments = touched_instruments.into_iter().collect::<Vec<_>>();
4270 touched_instruments.sort();
4271 for instrument_id in touched_instruments {
4272 if position_instruments.contains(&instrument_id)
4273 || !instruments
4274 .get_cloned(&instrument_id)
4275 .is_some_and(|instrument| {
4276 matches!(
4277 instrument.instrument_type,
4278 DeriveInstrumentType::Perp | DeriveInstrumentType::Option,
4279 )
4280 })
4281 {
4282 continue;
4283 }
4284
4285 flat_reports.push(PositionStatusReport::new(
4286 account_id,
4287 instrument_id,
4288 PositionSide::Flat,
4289 Quantity::from("0"),
4290 ts_init,
4291 ts_init,
4292 Some(UUID4::new()),
4293 None,
4294 None,
4295 ));
4296 }
4297
4298 if !flat_reports.is_empty() {
4299 log::info!(
4300 "Added {} flat PositionReports for Derive instruments absent from current positions",
4301 flat_reports.len()
4302 );
4303 mass_status.add_position_reports(flat_reports);
4304 }
4305}
4306
4307fn report_precision(
4308 dispatch_state: &WsDispatchState,
4309 instrument_name: &str,
4310) -> (Option<u8>, Option<u8>) {
4311 let Ok(instrument_id) = format_instrument_id(instrument_name) else {
4312 return (None, None);
4313 };
4314
4315 dispatch_state
4316 .instrument_precision(&instrument_id)
4317 .map_or((None, None), |(price, size)| (Some(price), Some(size)))
4318}
4319
4320fn handle_ws_message(
4321 message: DeriveWsMessage,
4322 emitter: &ExecutionEventEmitter,
4323 account_id: AccountId,
4324 clock: &'static AtomicTime,
4325 dispatch_state: &WsDispatchState,
4326) {
4327 flush_pending_delivery(emitter, account_id, clock, dispatch_state);
4328
4329 let payload = match message {
4330 DeriveWsMessage::Subscription(payload) => payload,
4331 DeriveWsMessage::Authenticated
4332 | DeriveWsMessage::Reconnected
4333 | DeriveWsMessage::SessionRecoveryFailed(_)
4334 | DeriveWsMessage::BookSubscription { .. } => return,
4335 };
4336
4337 let channel = DeriveWsChannel::from(payload.channel.as_str());
4338
4339 if let DeriveWsChannel::Orders { subaccount_id }
4340 | DeriveWsChannel::PrivateTrades { subaccount_id }
4341 | DeriveWsChannel::Balances { subaccount_id } = &channel
4342 && *subaccount_id != dispatch_state.subaccount_id()
4343 {
4344 log::warn!("Ignoring Derive private frame for an unexpected subaccount");
4345 return;
4346 }
4347
4348 let is_orders_channel = matches!(channel, DeriveWsChannel::Orders { .. });
4349 let is_trades_channel = matches!(channel, DeriveWsChannel::PrivateTrades { .. });
4350
4351 if is_orders_channel {
4352 let data = match serde_json::from_str::<DeriveOrdersSubscriptionData>(payload.data.get()) {
4353 Ok(data) => data,
4354 Err(e) => {
4355 log::warn!(
4356 "Failed to decode Derive orders frame on channel {}: {e}",
4357 payload.channel,
4358 );
4359 return;
4360 }
4361 };
4362
4363 dispatch_orders_payload(data, emitter, account_id, clock, dispatch_state);
4364 } else if is_trades_channel {
4365 let data = match serde_json::from_str::<DeriveTradesSubscriptionData>(payload.data.get()) {
4366 Ok(data) => data,
4367 Err(e) => {
4368 log::warn!(
4369 "Failed to decode Derive trades frame on channel {}: {e}",
4370 payload.channel,
4371 );
4372 return;
4373 }
4374 };
4375
4376 dispatch_trades_payload(data, emitter, account_id, clock, dispatch_state);
4377 }
4378}
4379
4380pub fn dispatch_orders_payload(
4387 data: DeriveOrdersSubscriptionData,
4388 emitter: &ExecutionEventEmitter,
4389 account_id: AccountId,
4390 clock: &'static AtomicTime,
4391 dispatch_state: &WsDispatchState,
4392) {
4393 let _delivery = dispatch_state.delivery_guard();
4394 let ts_init = clock.get_time_ns();
4395 for order in data.orders {
4396 dispatch_order_row(&order, emitter, account_id, clock, dispatch_state, ts_init);
4397 }
4398}
4399
4400fn dispatch_order_row(
4401 order: &DeriveOrder,
4402 emitter: &ExecutionEventEmitter,
4403 account_id: AccountId,
4404 clock: &'static AtomicTime,
4405 dispatch_state: &WsDispatchState,
4406 ts_init: UnixNanos,
4407) {
4408 if !dispatch_state.owns_subaccount(order.subaccount_id) {
4409 log::warn!("Ignoring Derive order row for an unexpected subaccount");
4410 return;
4411 }
4412
4413 observe_order_binding(dispatch_state, order);
4414
4415 let (price_precision, size_precision) =
4416 report_precision(dispatch_state, order.instrument_name.as_str());
4417
4418 let mut report = match parse_derive_order_to_report_with_precision(
4419 order,
4420 account_id,
4421 price_precision,
4422 size_precision,
4423 ts_init,
4424 ) {
4425 Ok(report) => report,
4426 Err(e) => {
4427 log::warn!(
4428 "Failed to parse Derive order {:?} on {:?} in WS update: {:?}",
4429 order.order_id,
4430 order.instrument_name.as_str(),
4431 format!("{e:#}"),
4432 );
4433 return;
4434 }
4435 };
4436
4437 observe_trigger_order(dispatch_state, order, &report);
4438 if report
4439 .client_order_id
4440 .is_some_and(|cid| dispatch_state.is_terminal(&cid, report.instrument_id))
4441 {
4442 return;
4443 }
4444
4445 let identity =
4446 tracked_order_identity(report.client_order_id, report.instrument_id, dispatch_state);
4447
4448 match identity {
4449 Some((client_order_id, identity)) => {
4450 dispatch_tracked_order_report(
4451 order,
4452 &report,
4453 client_order_id,
4454 identity,
4455 emitter,
4456 account_id,
4457 clock,
4458 dispatch_state,
4459 ts_init,
4460 );
4461 }
4462 None => {
4463 if report.client_order_id.is_some_and(|cid| {
4464 dispatch_state
4465 .known_instrument(&cid)
4466 .is_some_and(|id| id != report.instrument_id)
4467 }) {
4468 report.client_order_id = None;
4469 }
4470
4471 emitter.send_order_status_report(report);
4472 }
4473 }
4474}
4475
4476#[expect(
4477 clippy::too_many_arguments,
4478 reason = "native tracked rows retain the frame identity and delivery context"
4479)]
4480fn dispatch_tracked_order_report(
4481 order: &DeriveOrder,
4482 report: &OrderStatusReport,
4483 client_order_id: ClientOrderId,
4484 identity: OrderIdentity,
4485 emitter: &ExecutionEventEmitter,
4486 account_id: AccountId,
4487 clock: &'static AtomicTime,
4488 dispatch_state: &WsDispatchState,
4489 ts_init: UnixNanos,
4490) {
4491 if dispatch_state
4492 .bound_venue_order_id(&client_order_id)
4493 .is_some_and(|bound| bound != report.venue_order_id)
4494 {
4495 if !dispatch_state.knows_venue_leg(&client_order_id, report.venue_order_id) {
4496 dispatch_state.defer_binding(client_order_id, report.venue_order_id);
4497 }
4498
4499 let Some(old_venue_order_id) = dispatch_state.pending_modify(&client_order_id) else {
4500 return;
4501 };
4502
4503 if order.replaced_order_id.as_deref() != Some(old_venue_order_id.as_str())
4504 || dispatch_state.modify_nonce(&client_order_id) != Some(order.nonce)
4505 || !matches!(
4506 order.order_status,
4507 DeriveOrderStatus::Open
4508 | DeriveOrderStatus::Filled
4509 | DeriveOrderStatus::Cancelled
4510 | DeriveOrderStatus::Expired
4511 )
4512 {
4513 log::debug!(
4514 "Deferring uncorrelated Derive order leg {} for {client_order_id}",
4515 report.venue_order_id
4516 );
4517 return;
4518 }
4519
4520 if !emit_modify_target(
4521 emitter,
4522 dispatch_state,
4523 client_order_id,
4524 identity,
4525 report.venue_order_id,
4526 account_id,
4527 report.ts_last,
4528 ts_init,
4529 ) {
4530 return;
4531 }
4532
4533 dispatch_trades_payload(
4534 DeriveTradesSubscriptionData {
4535 trades: dispatch_state.take_deferred_trades(&client_order_id),
4536 },
4537 emitter,
4538 account_id,
4539 clock,
4540 dispatch_state,
4541 );
4542 }
4543
4544 emit_tracked_order_event(
4545 emitter,
4546 dispatch_state,
4547 client_order_id,
4548 identity,
4549 report,
4550 account_id,
4551 ts_init,
4552 );
4553
4554 if dispatch_state.identity(&client_order_id).is_none() {
4555 dispatch_trades_payload(
4556 DeriveTradesSubscriptionData {
4557 trades: dispatch_state.take_deferred_trades(&client_order_id),
4558 },
4559 emitter,
4560 account_id,
4561 clock,
4562 dispatch_state,
4563 );
4564 }
4565}
4566
4567pub fn dispatch_trades_payload(
4574 data: DeriveTradesSubscriptionData,
4575 emitter: &ExecutionEventEmitter,
4576 account_id: AccountId,
4577 clock: &'static AtomicTime,
4578 dispatch_state: &WsDispatchState,
4579) {
4580 let _delivery = dispatch_state.delivery_guard();
4581 let fee_currency = Currency::USDC();
4582 let ts_init = clock.get_time_ns();
4583
4584 for trade in data.trades {
4585 dispatch_trade_row(
4586 trade,
4587 emitter,
4588 account_id,
4589 clock,
4590 dispatch_state,
4591 ts_init,
4592 fee_currency,
4593 );
4594 }
4595}
4596
4597fn dispatch_trade_row(
4598 trade: DeriveTrade,
4599 emitter: &ExecutionEventEmitter,
4600 account_id: AccountId,
4601 clock: &'static AtomicTime,
4602 dispatch_state: &WsDispatchState,
4603 ts_init: UnixNanos,
4604 fee_currency: Currency,
4605) {
4606 if !dispatch_state.owns_subaccount(trade.subaccount_id) {
4607 log::warn!("Ignoring Derive trade row for an unexpected subaccount");
4608 return;
4609 }
4610
4611 let (price_precision, size_precision) =
4612 report_precision(dispatch_state, trade.instrument_name.as_str());
4613
4614 let mut report = match parse_derive_trade_to_fill_report_with_precision(
4615 &trade,
4616 account_id,
4617 fee_currency,
4618 price_precision,
4619 size_precision,
4620 ts_init,
4621 ) {
4622 Ok(Some(report)) => report,
4623 Ok(None) => return,
4624 Err(e) => {
4625 log::warn!(
4626 "Failed to parse Derive trade {:?} for order {:?} on {:?} in WS update: {:?}",
4627 trade.trade_id,
4628 trade.order_id,
4629 trade.instrument_name.as_str(),
4630 format!("{e:#}")
4631 );
4632 return;
4633 }
4634 };
4635
4636 if dispatch_state.contains_trade(&report.trade_id) {
4637 log::debug!(
4638 "Skipping duplicate Derive fill (trade_id={}) on WS dispatch",
4639 report.trade_id,
4640 );
4641 return;
4642 }
4643
4644 let identity =
4645 tracked_order_identity(report.client_order_id, report.instrument_id, dispatch_state);
4646
4647 let known_side = identity
4648 .map(|(_, identity)| identity.order_side)
4649 .or_else(|| {
4650 report
4651 .client_order_id
4652 .and_then(|cid| dispatch_state.order_binding(&cid))
4653 .filter(|binding| binding.identity.instrument_id == report.instrument_id)
4654 .map(|binding| binding.identity.order_side)
4655 });
4656
4657 if known_side.is_some_and(|side| side != report.order_side) {
4658 log::warn!(
4659 "Deferring Derive fill {} with a conflicting tracked order side",
4660 report.trade_id
4661 );
4662 dispatch_state.retain_trade(trade);
4663 return;
4664 }
4665
4666 if let Some((client_order_id, _)) = identity
4667 && dispatch_state
4668 .bound_venue_order_id(&client_order_id)
4669 .is_some()
4670 && !dispatch_state.knows_venue_leg(&client_order_id, report.venue_order_id)
4671 {
4672 dispatch_state.defer_binding(client_order_id, report.venue_order_id);
4673 dispatch_state.defer_trade(client_order_id, trade);
4674 return;
4675 }
4676
4677 if let Some((client_order_id, _)) = identity
4678 && dispatch_state
4679 .bound_venue_order_id(&client_order_id)
4680 .is_none()
4681 {
4682 dispatch_state.record_venue_order_id(client_order_id, report.venue_order_id);
4683 }
4684
4685 observe_trigger_fill(dispatch_state, &report);
4686
4687 let delivered = match identity {
4688 Some((client_order_id, identity)) => emit_tracked_fill(
4689 emitter,
4690 dispatch_state,
4691 client_order_id,
4692 identity,
4693 &report,
4694 account_id,
4695 ts_init,
4696 ),
4697 None => {
4698 if report.client_order_id.is_some_and(|cid| {
4699 dispatch_state
4700 .known_instrument(&cid)
4701 .is_some_and(|id| id != report.instrument_id)
4702 }) {
4703 report.client_order_id = None;
4704 }
4705
4706 match emitter.try_send_execution_report(ExecutionReport::Fill(Box::new(report.clone())))
4707 {
4708 Ok(()) => true,
4709 Err(e) => {
4710 log::warn!(
4711 "Failed to deliver Derive fill report {}: {e}",
4712 report.trade_id
4713 );
4714 false
4715 }
4716 }
4717 }
4718 };
4719
4720 if delivered {
4721 dispatch_state.check_and_insert_trade(report.trade_id);
4722
4723 if let Some((client_order_id, _)) = identity
4724 && dispatch_state.identity(&client_order_id).is_none()
4725 {
4726 dispatch_trades_payload(
4727 DeriveTradesSubscriptionData {
4728 trades: dispatch_state.take_deferred_trades(&client_order_id),
4729 },
4730 emitter,
4731 account_id,
4732 clock,
4733 dispatch_state,
4734 );
4735 }
4736 } else {
4737 dispatch_state.retain_trade(trade);
4738 }
4739}
4740
4741async fn replacement_amounts(
4742 http_client: &DeriveHttpClient,
4743 dispatch_state: &WsDispatchState,
4744 client_order_id: ClientOrderId,
4745 identity: OrderIdentity,
4746 current_venue_order_id: VenueOrderId,
4747 target_quantity: Decimal,
4748) -> anyhow::Result<(Decimal, Decimal)> {
4749 let current = fetch_order_record(
4750 http_client,
4751 dispatch_state.subaccount_id(),
4752 current_venue_order_id,
4753 Some(identity.instrument_id),
4754 )
4755 .await?;
4756 anyhow::ensure!(
4757 current.order_id == current_venue_order_id.as_str(),
4758 "Native replacement order identity does not match"
4759 );
4760 anyhow::ensure!(
4761 current.order_status == DeriveOrderStatus::Open,
4762 "Native replacement target is not open; current binding requires reconciliation"
4763 );
4764 let current_filled = current.filled_amount.normalize();
4765 let orders = replacement_orders(
4766 http_client,
4767 dispatch_state,
4768 client_order_id,
4769 identity,
4770 current,
4771 dispatch_state.venue_order_legs(&client_order_id),
4772 &[],
4773 )
4774 .await?;
4775 let mut filled = U256::ZERO;
4776
4777 for order in orders {
4778 let leg_filled = decimal_to_scaled_u256(order.filled_amount.normalize())
4779 .map_err(|e| anyhow::anyhow!(e))?;
4780 filled = filled
4781 .checked_add(leg_filled)
4782 .context("native cumulative fills exceed exact integer range")?;
4783 }
4784
4785 let target =
4786 decimal_to_scaled_u256(target_quantity.normalize()).map_err(|e| anyhow::anyhow!(e))?;
4787 let remaining = target
4788 .checked_sub(filled)
4789 .context("requested quantity is below native cumulative fills")?;
4790 anyhow::ensure!(
4791 remaining > U256::ZERO,
4792 "Requested quantity does not leave a positive native replacement amount"
4793 );
4794
4795 let remaining = if filled == U256::ZERO {
4796 target_quantity
4797 } else {
4798 decimal_from_scaled(remaining)?
4799 };
4800
4801 Ok((remaining, current_filled))
4802}
4803
4804async fn replacement_orders(
4805 http_client: &DeriveHttpClient,
4806 dispatch_state: &WsDispatchState,
4807 client_order_id: ClientOrderId,
4808 identity: OrderIdentity,
4809 current: DeriveOrder,
4810 mut proven_legs: Vec<VenueOrderId>,
4811 known_orders: &[&DeriveOrder],
4812) -> anyhow::Result<Vec<DeriveOrder>> {
4813 let current_venue_order_id = VenueOrderId::new_checked(¤t.order_id)?;
4814 proven_legs.retain(|id| *id != current_venue_order_id);
4815 proven_legs.insert(0, current_venue_order_id);
4816 let mut visited = AHashSet::new();
4817 let mut orders = Vec::new();
4818
4819 for mut venue_order_id in proven_legs {
4820 let mut ancestry = AHashSet::new();
4821
4822 loop {
4823 anyhow::ensure!(
4824 ancestry.insert(venue_order_id),
4825 "Cyclic native replacement ancestry"
4826 );
4827
4828 if !visited.insert(venue_order_id) {
4829 break;
4830 }
4831
4832 let order = if venue_order_id == current_venue_order_id {
4833 current.clone()
4834 } else if let Some(order) = known_orders
4835 .iter()
4836 .filter(|order| order.order_id == venue_order_id.as_str())
4837 .max_by_key(|order| order.last_update_timestamp)
4838 {
4839 (**order).clone()
4840 } else {
4841 fetch_order_record(
4842 http_client,
4843 dispatch_state.subaccount_id(),
4844 venue_order_id,
4845 Some(identity.instrument_id),
4846 )
4847 .await?
4848 };
4849
4850 validate_replacement_order(
4851 &order,
4852 client_order_id,
4853 identity,
4854 dispatch_state,
4855 venue_order_id,
4856 current_venue_order_id,
4857 )?;
4858
4859 let previous = order.replaced_order_id.clone();
4860 orders.push(order);
4861
4862 let Some(previous) = previous else {
4863 break;
4864 };
4865
4866 venue_order_id = VenueOrderId::new_checked(previous)?;
4867 }
4868 }
4869
4870 Ok(orders)
4871}
4872
4873fn validate_replacement_order(
4874 order: &DeriveOrder,
4875 client_order_id: ClientOrderId,
4876 identity: OrderIdentity,
4877 dispatch_state: &WsDispatchState,
4878 venue_order_id: VenueOrderId,
4879 current_venue_order_id: VenueOrderId,
4880) -> anyhow::Result<()> {
4881 anyhow::ensure!(
4882 order_response_matches(
4883 order,
4884 client_order_id,
4885 identity.instrument_id,
4886 dispatch_state
4887 ) && order.order_id == venue_order_id.as_str(),
4888 "Native replacement order identity does not match"
4889 );
4890
4891 let side = match order.direction {
4892 DeriveOrderSide::Buy => OrderSide::Buy,
4893 DeriveOrderSide::Sell => OrderSide::Sell,
4894 };
4895
4896 anyhow::ensure!(
4897 side == identity.order_side,
4898 "Native replacement order side does not match"
4899 );
4900 anyhow::ensure!(
4901 derive_order_type_to_nautilus_for_order(order.order_type, order.trigger_type)?
4902 == identity.order_type,
4903 "Native replacement order type does not match"
4904 );
4905 anyhow::ensure!(
4906 order.amount > Decimal::ZERO
4907 && order.filled_amount >= Decimal::ZERO
4908 && order.filled_amount <= order.amount,
4909 "Invalid native replacement amount or filled amount"
4910 );
4911
4912 if venue_order_id != current_venue_order_id {
4913 anyhow::ensure!(
4914 matches!(
4915 order.order_status,
4916 DeriveOrderStatus::Cancelled
4917 | DeriveOrderStatus::Filled
4918 | DeriveOrderStatus::Expired
4919 ),
4920 "Native replacement ancestor is not closed"
4921 );
4922 }
4923
4924 Ok(())
4925}
4926
4927async fn fetch_order_record(
4928 http_client: &DeriveHttpClient,
4929 subaccount_id: u64,
4930 venue_order_id: VenueOrderId,
4931 instrument_id: Option<InstrumentId>,
4932) -> anyhow::Result<DeriveOrder> {
4933 let missing = match http_client
4934 .get_order(&DeriveGetOrderParams::new(
4935 subaccount_id,
4936 venue_order_id.as_str(),
4937 ))
4938 .await
4939 {
4940 Ok(order) => return Ok(order),
4941 Err(e) if matches!(e, DeriveHttpError::JsonRpc { code: 11006, .. }) => e,
4942 Err(e) => return Err(e.into()),
4943 };
4944
4945 let mut pages = PaginationCursor::new();
4946
4947 loop {
4948 let mut params =
4949 DeriveGetOrderHistoryParams::new(subaccount_id, pages.page(), DERIVE_PRIVATE_PAGE_SIZE);
4950
4951 if let Some(instrument_id) = instrument_id {
4952 params = params.with_instrument_name(format_venue_symbol(&instrument_id)?);
4953 }
4954
4955 let result = http_client.get_order_history(¶ms).await?;
4956 if pages.restart_if_changed(&result.pagination)? {
4957 continue;
4958 }
4959
4960 let more = pages.advance(
4961 &result.pagination,
4962 result.orders.iter().map(|order| order.order_id.as_str()),
4963 )?;
4964
4965 if let Some(order) = result
4966 .orders
4967 .into_iter()
4968 .find(|order| order.order_id == venue_order_id.as_str())
4969 {
4970 return Ok(order);
4971 }
4972
4973 if !more {
4974 return Err(missing.into());
4975 }
4976 }
4977}
4978
4979fn quantity_from_report_decimal(value: Decimal, precision: Option<u8>) -> anyhow::Result<Quantity> {
4980 match precision {
4981 Some(precision) => Quantity::from_decimal_dp(value, precision),
4982 None => Quantity::from_decimal(value.normalize()),
4983 }
4984 .context("native cumulative report quantity cannot be represented")
4985}
4986
4987fn decimal_from_scaled(mut value: U256) -> anyhow::Result<Decimal> {
4988 let mut scale = DECIMAL_SCALE.ilog10();
4989 let ten = U256::from(10_u8);
4990 while scale > 0 && value % ten == U256::ZERO {
4991 value /= ten;
4992 scale -= 1;
4993 }
4994
4995 let coefficient =
4996 u128::try_from(value).context("native amount exceeds exact decimal coefficient range")?;
4997 let coefficient = i128::try_from(coefficient)
4998 .context("native amount exceeds signed decimal coefficient range")?;
4999 Decimal::try_from_i128_with_scale(coefficient, scale)
5000 .context("native amount cannot be represented exactly")
5001}
5002
5003fn flush_pending_delivery(
5004 emitter: &ExecutionEventEmitter,
5005 account_id: AccountId,
5006 clock: &'static AtomicTime,
5007 dispatch_state: &WsDispatchState,
5008) {
5009 let _delivery = dispatch_state.delivery_guard();
5010 for (client_order_id, completion) in dispatch_state.replace_completions() {
5011 if dispatch_replace_completion(
5012 client_order_id,
5013 &completion,
5014 emitter,
5015 account_id,
5016 clock,
5017 dispatch_state,
5018 ) {
5019 dispatch_state.clear_replace_completion(&client_order_id);
5020 }
5021 }
5022
5023 dispatch_trades_payload(
5024 DeriveTradesSubscriptionData {
5025 trades: dispatch_state.take_undelivered_trades(),
5026 },
5027 emitter,
5028 account_id,
5029 clock,
5030 dispatch_state,
5031 );
5032}
5033
5034fn dispatch_replace_completion(
5035 client_order_id: ClientOrderId,
5036 completion: &ReplaceCompletion,
5037 emitter: &ExecutionEventEmitter,
5038 account_id: AccountId,
5039 clock: &'static AtomicTime,
5040 dispatch_state: &WsDispatchState,
5041) -> bool {
5042 let identity = completion.identity;
5043 let old_venue_order_id = completion.old_venue_order_id;
5044 let ts_init = clock.get_time_ns();
5045 dispatch_trades_payload(
5046 DeriveTradesSubscriptionData {
5047 trades: completion.trades.clone(),
5048 },
5049 emitter,
5050 account_id,
5051 clock,
5052 dispatch_state,
5053 );
5054
5055 if dispatch_state.pending_modify(&client_order_id) != Some(old_venue_order_id)
5056 && (dispatch_state.identity(&client_order_id).is_none()
5057 || dispatch_state
5058 .bound_venue_order_id(&client_order_id)
5059 .is_some_and(|bound| bound != old_venue_order_id))
5060 {
5061 return true;
5062 }
5063
5064 let Some(outcome) = decode_replace_completion(client_order_id, completion, dispatch_state)
5065 else {
5066 return false;
5067 };
5068
5069 match &outcome {
5070 DeriveReplaceOutcome::Replaced(order) => {
5071 if order.nonce != completion.nonce {
5072 log::warn!(
5073 "Derive replacement nonce does not match {client_order_id}; awaiting reconciliation"
5074 );
5075 return false;
5076 }
5077
5078 let Ok(new_venue_order_id) = VenueOrderId::new_checked(&order.order_id) else {
5079 log::warn!(
5080 "Derive replacement response has an invalid order ID for {client_order_id}, awaiting reconciliation"
5081 );
5082 return false;
5083 };
5084
5085 if dispatch_state.pending_modify(&client_order_id) == Some(old_venue_order_id)
5086 && !emit_modify_target(
5087 emitter,
5088 dispatch_state,
5089 client_order_id,
5090 identity,
5091 new_venue_order_id,
5092 account_id,
5093 ts_init,
5094 ts_init,
5095 )
5096 {
5097 return false;
5098 }
5099
5100 dispatch_trades_payload(
5101 DeriveTradesSubscriptionData {
5102 trades: dispatch_state.take_deferred_trades(&client_order_id),
5103 },
5104 emitter,
5105 account_id,
5106 clock,
5107 dispatch_state,
5108 );
5109
5110 dispatch_order_result(
5111 DeriveOrderResult {
5112 order: order.clone(),
5113 trades: vec![],
5114 },
5115 client_order_id,
5116 emitter,
5117 account_id,
5118 clock,
5119 dispatch_state,
5120 );
5121
5122 true
5123 }
5124 DeriveReplaceOutcome::Canceled {
5125 cancelled_order,
5126 create_order_error,
5127 } => {
5128 if dispatch_state.pending_modify(&client_order_id) != Some(old_venue_order_id) {
5129 return true;
5130 }
5131
5132 if !ensure_canceled_emitted(
5133 emitter,
5134 dispatch_state,
5135 client_order_id,
5136 identity,
5137 old_venue_order_id,
5138 account_id,
5139 ts_init,
5140 ts_init,
5141 ) {
5142 return false;
5143 }
5144
5145 log::warn!(
5146 "Derive cancels {client_order_id} ({}) without creating its replacement: JSON-RPC {}: {}",
5147 cancelled_order.order_id,
5148 create_order_error.code,
5149 create_order_error.message
5150 );
5151 dispatch_state.forget(&client_order_id);
5152 dispatch_trades_payload(
5153 DeriveTradesSubscriptionData {
5154 trades: dispatch_state.take_deferred_trades(&client_order_id),
5155 },
5156 emitter,
5157 account_id,
5158 clock,
5159 dispatch_state,
5160 );
5161
5162 true
5163 }
5164 }
5165}
5166
5167fn decode_replace_completion(
5168 client_order_id: ClientOrderId,
5169 completion: &ReplaceCompletion,
5170 dispatch_state: &WsDispatchState,
5171) -> Option<DeriveReplaceOutcome> {
5172 let identity = completion.identity;
5173 let old_venue_order_id = completion.old_venue_order_id;
5174
5175 let Ok(expected_instrument) = format_venue_symbol(&identity.instrument_id) else {
5176 log::warn!("Cannot correlate Derive replacement instrument for {client_order_id}");
5177 return None;
5178 };
5179
5180 let result = match &completion.result {
5181 Ok(result) => result.clone(),
5182 Err(e) => {
5183 log::warn!(
5184 "Cannot decode Derive replacement authority for {client_order_id}: {e}; awaiting reconciliation"
5185 );
5186 return None;
5187 }
5188 };
5189
5190 match result.into_outcome(
5191 old_venue_order_id.as_str(),
5192 client_order_id.as_str(),
5193 dispatch_state.subaccount_id(),
5194 expected_instrument.as_str(),
5195 ) {
5196 Ok(outcome) => Some(outcome),
5197 Err(e) => {
5198 log::warn!(
5199 "Derive replacement response does not match {client_order_id}: {e}, awaiting reconciliation"
5200 );
5201 None
5202 }
5203 }
5204}
5205
5206fn dispatch_order_result(
5207 result: DeriveOrderResult,
5208 client_order_id: ClientOrderId,
5209 emitter: &ExecutionEventEmitter,
5210 account_id: AccountId,
5211 clock: &'static AtomicTime,
5212 dispatch_state: &WsDispatchState,
5213) {
5214 let _delivery = dispatch_state.delivery_guard();
5215 let mut is_current = false;
5216
5217 if let Some(identity) = dispatch_state.identity(&client_order_id) {
5218 if !order_response_matches(
5219 &result.order,
5220 client_order_id,
5221 identity.instrument_id,
5222 dispatch_state,
5223 ) {
5224 log::warn!(
5225 "Derive order response does not match {client_order_id}, awaiting reconciliation"
5226 );
5227 } else if let Ok(venue_order_id) = VenueOrderId::new_checked(&result.order.order_id) {
5228 is_current = dispatch_state
5229 .bound_venue_order_id(&client_order_id)
5230 .is_none_or(|bound| bound == venue_order_id);
5231 if is_current
5232 && !matches!(
5233 result.order.order_status,
5234 DeriveOrderStatus::Rejected | DeriveOrderStatus::Unknown
5235 )
5236 {
5237 dispatch_state.record_venue_order_id(client_order_id, venue_order_id);
5238 let ts = clock.get_time_ns();
5239 ensure_accepted_emitted(
5240 emitter,
5241 dispatch_state,
5242 client_order_id,
5243 identity,
5244 venue_order_id,
5245 account_id,
5246 ts,
5247 ts,
5248 );
5249 }
5250 } else {
5251 log::warn!(
5252 "Derive submit outcome for {:?} on {:?} has an invalid response order_id; awaiting reconciliation",
5253 client_order_id.as_str(),
5254 identity.instrument_id.to_string(),
5255 );
5256 }
5257 }
5258
5259 dispatch_trades_payload(
5260 DeriveTradesSubscriptionData {
5261 trades: result.trades,
5262 },
5263 emitter,
5264 account_id,
5265 clock,
5266 dispatch_state,
5267 );
5268
5269 if is_current {
5270 dispatch_orders_payload(
5271 DeriveOrdersSubscriptionData {
5272 orders: vec![result.order],
5273 },
5274 emitter,
5275 account_id,
5276 clock,
5277 dispatch_state,
5278 );
5279 }
5280}
5281
5282fn order_response_matches(
5283 order: &DeriveOrder,
5284 client_order_id: ClientOrderId,
5285 instrument_id: InstrumentId,
5286 dispatch_state: &WsDispatchState,
5287) -> bool {
5288 dispatch_state.owns_subaccount(order.subaccount_id)
5289 && order.label == client_order_id.as_str()
5290 && format_instrument_id(order.instrument_name).is_ok_and(|id| id == instrument_id)
5291}
5292
5293fn tracked_order_identity(
5294 client_order_id: Option<ClientOrderId>,
5295 instrument_id: InstrumentId,
5296 dispatch_state: &WsDispatchState,
5297) -> Option<(ClientOrderId, OrderIdentity)> {
5298 client_order_id.and_then(|cid| {
5299 dispatch_state
5300 .identity(&cid)
5301 .filter(|identity| identity.instrument_id == instrument_id)
5302 .map(|identity| (cid, identity))
5303 })
5304}
5305
5306#[expect(clippy::too_many_arguments)]
5311fn ensure_accepted_emitted(
5312 emitter: &ExecutionEventEmitter,
5313 dispatch_state: &WsDispatchState,
5314 client_order_id: ClientOrderId,
5315 identity: OrderIdentity,
5316 venue_order_id: VenueOrderId,
5317 account_id: AccountId,
5318 ts_event: UnixNanos,
5319 ts_init: UnixNanos,
5320) -> bool {
5321 let _delivery = dispatch_state.delivery_guard();
5322 if dispatch_state.contains_accepted(&client_order_id) {
5323 return true;
5324 }
5325
5326 let accepted = OrderAccepted::new(
5327 emitter.trader_id(),
5328 identity.strategy_id,
5329 identity.instrument_id,
5330 client_order_id,
5331 venue_order_id,
5332 account_id,
5333 UUID4::new(),
5334 ts_event,
5335 ts_init,
5336 false,
5337 );
5338
5339 if let Err(e) = emitter.try_send_order_event(OrderEventAny::Accepted(accepted)) {
5340 log::warn!("Failed to deliver Derive accepted for {client_order_id}: {e}");
5341 return false;
5342 }
5343
5344 dispatch_state.mark_accepted(client_order_id);
5345 true
5346}
5347
5348#[expect(clippy::too_many_arguments)]
5349fn ensure_canceled_emitted(
5350 emitter: &ExecutionEventEmitter,
5351 dispatch_state: &WsDispatchState,
5352 client_order_id: ClientOrderId,
5353 identity: OrderIdentity,
5354 venue_order_id: VenueOrderId,
5355 account_id: AccountId,
5356 ts_event: UnixNanos,
5357 ts_init: UnixNanos,
5358) -> bool {
5359 let _delivery = dispatch_state.delivery_guard();
5360 if dispatch_state.contains_canceled(&client_order_id) {
5361 return true;
5362 }
5363
5364 let canceled = OrderCanceled::new(
5365 emitter.trader_id(),
5366 identity.strategy_id,
5367 identity.instrument_id,
5368 client_order_id,
5369 UUID4::new(),
5370 ts_event,
5371 ts_init,
5372 false,
5373 Some(venue_order_id),
5374 Some(account_id),
5375 None,
5376 );
5377
5378 if let Err(e) = emitter.try_send_order_event(OrderEventAny::Canceled(canceled)) {
5379 log::warn!("Failed to deliver Derive canceled for {client_order_id}: {e}");
5380 return false;
5381 }
5382
5383 dispatch_state.mark_canceled(client_order_id);
5384 true
5385}
5386
5387fn emit_tracked_order_event(
5388 emitter: &ExecutionEventEmitter,
5389 dispatch_state: &WsDispatchState,
5390 client_order_id: ClientOrderId,
5391 identity: OrderIdentity,
5392 report: &OrderStatusReport,
5393 account_id: AccountId,
5394 ts_init: UnixNanos,
5395) {
5396 let venue_order_id = report.venue_order_id;
5397 let ts_accepted = report.ts_accepted;
5398 let ts_event = report.ts_last;
5399
5400 if report.order_status == OrderStatus::Canceled
5406 && dispatch_state.pending_modify(&client_order_id) == Some(venue_order_id)
5407 {
5408 log::debug!(
5409 "Skipping cancel-replace leg for {client_order_id}: stale venue_order_id={venue_order_id}",
5410 );
5411 return;
5412 }
5413
5414 if let Some(bound) = dispatch_state.bound_venue_order_id(&client_order_id)
5415 && bound != venue_order_id
5416 {
5417 log::debug!(
5418 "Skipping stale {:?} for {client_order_id}: venue_order_id={venue_order_id} superseded by {bound}",
5419 report.order_status
5420 );
5421 return;
5422 }
5423
5424 if dispatch_state.binding_unresolved(&client_order_id) && report.order_status.is_closed() {
5425 log::debug!(
5426 "Deferring terminal Derive status for {client_order_id} while venue binding is unresolved"
5427 );
5428 return;
5429 }
5430
5431 if matches!(
5432 report.order_status,
5433 OrderStatus::Accepted | OrderStatus::PartiallyFilled
5434 ) && dispatch_state.contains_filled(&client_order_id)
5435 {
5436 log::debug!("Skipping stale Accepted for {client_order_id} (already filled)");
5437 return;
5438 }
5439
5440 if matches!(
5441 report.order_status,
5442 OrderStatus::Accepted | OrderStatus::PartiallyFilled | OrderStatus::Filled
5443 ) {
5444 dispatch_state.record_venue_order_id(client_order_id, venue_order_id);
5445 }
5446
5447 if matches!(
5448 report.order_status,
5449 OrderStatus::Accepted
5450 | OrderStatus::PartiallyFilled
5451 | OrderStatus::Filled
5452 | OrderStatus::Canceled
5453 | OrderStatus::Expired
5454 ) && !ensure_accepted_emitted(
5455 emitter,
5456 dispatch_state,
5457 client_order_id,
5458 identity,
5459 venue_order_id,
5460 account_id,
5461 ts_accepted,
5462 ts_init,
5463 ) {
5464 return;
5465 }
5466
5467 match report.order_status {
5468 OrderStatus::Accepted | OrderStatus::PartiallyFilled => {}
5469 OrderStatus::Filled => {
5470 dispatch_state.mark_filled(client_order_id);
5472 }
5473 OrderStatus::Canceled => {
5474 if !ensure_canceled_emitted(
5475 emitter,
5476 dispatch_state,
5477 client_order_id,
5478 identity,
5479 venue_order_id,
5480 account_id,
5481 ts_event,
5482 ts_init,
5483 ) {
5484 return;
5485 }
5486
5487 dispatch_state.forget(&client_order_id);
5488 }
5489 OrderStatus::Expired | OrderStatus::Rejected => {
5490 emit_expiry_or_rejection(
5491 emitter,
5492 dispatch_state,
5493 client_order_id,
5494 identity,
5495 report,
5496 account_id,
5497 ts_init,
5498 );
5499 }
5500 other => {
5501 log::debug!(
5502 "Unhandled tracked order status {other:?} for {client_order_id}, sending as report",
5503 );
5504 emitter.send_order_status_report(report.clone());
5505 }
5506 }
5507}
5508
5509fn emit_expiry_or_rejection(
5510 emitter: &ExecutionEventEmitter,
5511 dispatch_state: &WsDispatchState,
5512 client_order_id: ClientOrderId,
5513 identity: OrderIdentity,
5514 report: &OrderStatusReport,
5515 account_id: AccountId,
5516 ts_init: UnixNanos,
5517) {
5518 let venue_order_id = report.venue_order_id;
5519 let ts_event = report.ts_last;
5520
5521 let (event, status) = if report.order_status == OrderStatus::Rejected {
5522 if dispatch_state.identity(&client_order_id).is_none() {
5523 return;
5524 }
5525
5526 let reason = report
5527 .cancel_reason
5528 .as_deref()
5529 .unwrap_or("Order rejected by Derive");
5530 let due_post_only = derive_rejection_due_post_only(None, reason);
5531
5532 let event = OrderRejected::new(
5533 emitter.trader_id(),
5534 identity.strategy_id,
5535 identity.instrument_id,
5536 client_order_id,
5537 account_id,
5538 Ustr::from(&strategy_rejection_reason(reason)),
5539 UUID4::new(),
5540 ts_event,
5541 ts_init,
5542 false,
5543 due_post_only,
5544 );
5545 (OrderEventAny::Rejected(event), "rejected")
5546 } else {
5547 let event = OrderExpired::new(
5548 emitter.trader_id(),
5549 identity.strategy_id,
5550 identity.instrument_id,
5551 client_order_id,
5552 UUID4::new(),
5553 ts_event,
5554 ts_init,
5555 false,
5556 Some(venue_order_id),
5557 Some(account_id),
5558 );
5559 (OrderEventAny::Expired(event), "expired")
5560 };
5561
5562 if let Err(e) = emitter.try_send_order_event(event) {
5563 log::warn!("Failed to deliver Derive {status} for {client_order_id}: {e}");
5564 return;
5565 }
5566
5567 dispatch_state.forget(&client_order_id);
5568}
5569
5570#[expect(clippy::too_many_arguments)]
5571fn emit_modify_target(
5572 emitter: &ExecutionEventEmitter,
5573 dispatch_state: &WsDispatchState,
5574 client_order_id: ClientOrderId,
5575 identity: OrderIdentity,
5576 venue_order_id: VenueOrderId,
5577 account_id: AccountId,
5578 ts_event: UnixNanos,
5579 ts_init: UnixNanos,
5580) -> bool {
5581 if let Some((quantity, price)) = dispatch_state.modify_target(&client_order_id) {
5582 if let Err(e) = emitter.try_send_order_event(OrderEventAny::Updated(OrderUpdated::new(
5583 emitter.trader_id(),
5584 identity.strategy_id,
5585 identity.instrument_id,
5586 client_order_id,
5587 quantity,
5588 UUID4::new(),
5589 ts_event,
5590 ts_init,
5591 false,
5592 Some(venue_order_id),
5593 Some(account_id),
5594 price,
5595 None,
5596 None,
5597 false,
5598 ))) {
5599 log::warn!("Failed to deliver Derive modify for {client_order_id}: {e}");
5600 return false;
5601 }
5602
5603 dispatch_state.record_order_shape(client_order_id, quantity, price);
5604 dispatch_state.take_modify_target(&client_order_id);
5605 }
5606
5607 if let Some(old_venue_order_id) = dispatch_state.pending_modify(&client_order_id) {
5608 dispatch_state.take_pending_modify(
5609 &client_order_id,
5610 old_venue_order_id,
5611 Some(venue_order_id),
5612 );
5613 }
5614
5615 true
5616}
5617
5618fn emit_tracked_fill(
5619 emitter: &ExecutionEventEmitter,
5620 dispatch_state: &WsDispatchState,
5621 client_order_id: ClientOrderId,
5622 identity: OrderIdentity,
5623 report: &FillReport,
5624 account_id: AccountId,
5625 ts_init: UnixNanos,
5626) -> bool {
5627 if !ensure_accepted_emitted(
5628 emitter,
5629 dispatch_state,
5630 client_order_id,
5631 identity,
5632 report.venue_order_id,
5633 account_id,
5634 report.ts_event,
5635 ts_init,
5636 ) {
5637 return false;
5638 }
5639
5640 let filled = OrderFilled::new(
5641 emitter.trader_id(),
5642 identity.strategy_id,
5643 identity.instrument_id,
5644 client_order_id,
5645 report.venue_order_id,
5646 account_id,
5647 report.trade_id,
5648 identity.order_side,
5649 identity.order_type,
5650 report.last_qty,
5651 report.last_px,
5652 report.commission.currency,
5653 report.liquidity_side,
5654 UUID4::new(),
5655 report.ts_event,
5656 ts_init,
5657 false,
5658 report.venue_position_id,
5659 Some(report.commission),
5660 None,
5661 );
5662
5663 if let Err(e) = emitter.try_send_order_event(OrderEventAny::Filled(filled)) {
5664 log::warn!("Failed to deliver Derive fill for {client_order_id}: {e}");
5665 return false;
5666 }
5667
5668 dispatch_state.record_fill(client_order_id, report.last_qty);
5669 true
5670}
5671
5672fn market_order_limit_price(
5683 quote: &QuoteTick,
5684 side: OrderSide,
5685 slippage_bps: u32,
5686 tick_size: Decimal,
5687) -> Option<Decimal> {
5688 let bps = Decimal::from(slippage_bps);
5689 let scale = Decimal::from(10_000_u32);
5690 let one = Decimal::ONE;
5691 let raw = match side {
5692 OrderSide::Buy => quote.ask_price.as_decimal() * (one + bps / scale),
5693 OrderSide::Sell => quote.bid_price.as_decimal() * (one - bps / scale),
5694 };
5695
5696 let rounded = round_to_tick(raw, tick_size, side);
5697 if rounded <= Decimal::ZERO {
5698 return None;
5699 }
5700
5701 Some(rounded)
5702}
5703
5704fn trigger_market_limit_price(
5705 trigger_price: Decimal,
5706 side: OrderSide,
5707 slippage_bps: u32,
5708 tick_size: Decimal,
5709) -> Option<Decimal> {
5710 let bps = Decimal::from(slippage_bps);
5711 let scale = Decimal::from(10_000_u32);
5712 let one = Decimal::ONE;
5713 let raw = match side {
5714 OrderSide::Buy => trigger_price * (one + bps / scale),
5715 OrderSide::Sell => trigger_price * (one - bps / scale),
5716 };
5717
5718 let rounded = round_to_tick(raw, tick_size, side);
5719 if rounded <= Decimal::ZERO {
5720 return None;
5721 }
5722
5723 Some(rounded)
5724}
5725
5726fn is_derive_trigger_order_type(order_type: OrderType) -> bool {
5727 matches!(
5728 order_type,
5729 OrderType::StopMarket
5730 | OrderType::StopLimit
5731 | OrderType::MarketIfTouched
5732 | OrderType::LimitIfTouched
5733 )
5734}
5735
5736fn resolve_submit_nonce(
5737 nonce: Result<u64, NonceError>,
5738 emitter: &ExecutionEventEmitter,
5739 dispatch_state: &WsDispatchState,
5740 order: &OrderAny,
5741 clock: &'static AtomicTime,
5742) -> Option<u64> {
5743 match nonce {
5744 Ok(nonce) => Some(nonce),
5745 Err(e) => {
5746 let reason = format!("nonce allocation failed: {e}");
5747 log::warn!("Cannot submit order {}: {reason}", order.client_order_id());
5748 dispatch_state.forget(&order.client_order_id());
5749 emit_submit_failure(
5750 emitter,
5751 order,
5752 CommandFailure::not_sent(&reason),
5753 clock.get_time_ns(),
5754 false,
5755 );
5756 None
5757 }
5758 }
5759}
5760
5761fn resolve_modify_nonce(
5762 nonce: Result<u64, NonceError>,
5763 emitter: &ExecutionEventEmitter,
5764 strategy_id: StrategyId,
5765 instrument_id: InstrumentId,
5766 client_order_id: ClientOrderId,
5767 venue_order_id: VenueOrderId,
5768 clock: &'static AtomicTime,
5769) -> Option<u64> {
5770 match nonce {
5771 Ok(nonce) => Some(nonce),
5772 Err(e) => {
5773 let reason = format!("nonce allocation failed: {e}");
5774 log::warn!("Cannot modify order {client_order_id}: {reason}");
5775 emit_modify_failure(
5776 emitter,
5777 strategy_id,
5778 instrument_id,
5779 client_order_id,
5780 Some(venue_order_id),
5781 CommandFailure::not_sent(&reason),
5782 clock.get_time_ns(),
5783 );
5784 None
5785 }
5786 }
5787}
5788
5789fn normal_order_signature_expiry(
5790 clock: &'static AtomicTime,
5791 signature_expiry_secs: u64,
5792) -> anyhow::Result<i64> {
5793 let min_ttl_secs = MIN_SIGNATURE_TTL.as_secs();
5794 if signature_expiry_secs <= min_ttl_secs {
5795 anyhow::bail!(
5796 "signature_expiry_secs {signature_expiry_secs}s must be greater than the Derive minimum {min_ttl_secs}s"
5797 );
5798 }
5799
5800 if signature_expiry_secs > 120 * 24 * 60 * 60 {
5801 anyhow::bail!("signature_expiry_secs exceeds the Derive maximum of 120 days");
5802 }
5803
5804 let now_secs_u64 = clock.get_time_ns().as_u64() / 1_000_000_000;
5805
5806 let now_secs = i64::try_from(now_secs_u64).with_context(|| {
5807 format!("current UNIX time {now_secs_u64}s cannot fit in Derive signature_expiry_sec")
5808 })?;
5809
5810 let ttl_secs = i64::try_from(signature_expiry_secs).with_context(|| {
5811 format!(
5812 "signature_expiry_secs {signature_expiry_secs}s cannot fit in Derive signature_expiry_sec"
5813 )
5814 })?;
5815
5816 now_secs.checked_add(ttl_secs).ok_or_else(|| {
5817 anyhow::anyhow!(
5818 "signature expiry overflows Derive signature_expiry_sec: now {now_secs}s plus TTL {ttl_secs}s"
5819 )
5820 })
5821}
5822
5823async fn refresh_market_order_quote(
5824 http_client: &DeriveHttpClient,
5825 venue_symbol: &str,
5826 instrument: &DeriveInstrument,
5827 clock: &'static AtomicTime,
5828) -> anyhow::Result<QuoteTick> {
5829 let ticker = http_client.get_ticker(venue_symbol).await?;
5830 let price_precision = Price::from_decimal(instrument.tick_size)
5831 .with_context(|| format!("invalid Derive tick_size for {venue_symbol}"))?
5832 .precision;
5833 let size_precision = Quantity::from_decimal(instrument.amount_step)
5834 .with_context(|| format!("invalid Derive amount_step for {venue_symbol}"))?
5835 .precision;
5836
5837 parse_ticker_quote_from_rest(
5838 &ticker,
5839 price_precision,
5840 size_precision,
5841 clock.get_time_ns(),
5842 )
5843}
5844
5845fn round_to_tick(value: Decimal, tick_size: Decimal, side: OrderSide) -> Decimal {
5850 if tick_size <= Decimal::ZERO {
5851 return value;
5852 }
5853
5854 let ratio = value / tick_size;
5855 let ticks = match side {
5856 OrderSide::Buy => ratio.ceil(),
5857 OrderSide::Sell => ratio.floor(),
5858 };
5859
5860 ticks * tick_size
5861}
5862
5863async fn cached_or_fetch_instrument(
5864 http_client: &DeriveHttpClient,
5865 instruments: &Arc<AtomicMap<InstrumentId, DeriveInstrument>>,
5866 instrument_id: &InstrumentId,
5867 venue_symbol: &str,
5868) -> anyhow::Result<DeriveInstrument> {
5869 if let Some(cached) = instruments.get_cloned(instrument_id) {
5870 return Ok(cached);
5871 }
5872
5873 let instrument = http_client
5874 .get_instrument(venue_symbol)
5875 .await
5876 .with_context(|| format!("failed to fetch instrument {venue_symbol}"))?;
5877 instruments.insert(*instrument_id, instrument.clone());
5878 Ok(instrument)
5879}
5880
5881#[cfg(test)]
5882mod tests {
5883 use std::{cell::RefCell, rc::Rc};
5884
5885 use nautilus_common::{
5886 cache::Cache,
5887 messages::{ExecutionEvent, ExecutionReport},
5888 };
5889 use nautilus_core::UnixNanos;
5890 use nautilus_live::ExecutionClientCore;
5891 use nautilus_model::{
5892 data::QuoteTick,
5893 enums::{AccountType, LiquiditySide, OmsType, TimeInForce},
5894 events::OrderFillVoided,
5895 identifiers::{AccountId, ClientId, InstrumentId, StrategyId, TradeId, TraderId},
5896 orders::OrderTestBuilder,
5897 types::{Money, Price, Quantity},
5898 };
5899 use rstest::rstest;
5900 use rust_decimal_macros::dec;
5901
5902 use super::*;
5903 use crate::{
5904 client_logging,
5905 common::{
5906 consts::DERIVE,
5907 enums::{DeriveEnvironment, DeriveOrderStatus, DeriveOrderType},
5908 },
5909 websocket::WsSubscriptionPayload,
5910 };
5911
5912 #[derive(Debug)]
5913 struct ReconciliationTimeSender(tokio::sync::mpsc::UnboundedSender<TimeEventMessage>);
5914
5915 impl TimeEventSender for ReconciliationTimeSender {
5916 fn send(&self, message: TimeEventMessage) {
5917 let _ = self.0.send(message);
5918 }
5919 }
5920
5921 #[rstest]
5922 #[case::markup("<b>Insufficient</b>\n\tmargin\u{0}", "11008: Insufficient margin")]
5923 #[case::blank("<p>\u{0}\n</p>", "11008: Order command rejected")]
5924 #[case::unicode(" Échec de\r budget ", "11008: Échec de budget")]
5925 fn test_ws_rejection_reason_is_bounded_venue_text(
5926 #[case] message: &str,
5927 #[case] expected: &str,
5928 ) {
5929 let error = DeriveWsError::JsonRpc {
5930 code: 11008,
5931 message: message.to_string(),
5932 data: None,
5933 };
5934
5935 let (reason, due_post_only) = ws_rejection_reason(&error);
5936 assert_eq!(reason, expected);
5937 assert!(due_post_only);
5938 assert_eq!(
5939 match error {
5940 DeriveWsError::JsonRpc { message, .. } => message,
5941 _ => unreachable!(),
5942 },
5943 message
5944 );
5945 }
5946
5947 #[rstest]
5948 fn test_ws_rejection_reason_limits_unicode_characters() {
5949 let message = "é".repeat(300);
5950
5951 let error = DeriveWsError::JsonRpc {
5952 code: 11009,
5953 message,
5954 data: None,
5955 };
5956
5957 let (reason, due_post_only) = ws_rejection_reason(&error);
5958 assert_eq!(reason, format!("11009: {}", "é".repeat(249)));
5959 assert_eq!(reason.chars().count(), 256);
5960 assert!(!due_post_only);
5961 }
5962
5963 #[rstest]
5964 #[case::not_connected(
5965 DeriveWsError::NotConnected,
5966 CommandFailure::not_sent("WebSocket client is not connected"),
5967 false
5968 )]
5969 #[case::local_pacing(
5970 DeriveWsError::RateLimited { retry_after: Duration::from_millis(125) },
5971 CommandFailure::not_sent("Local pacing reservation expired"),
5972 false
5973 )]
5974 #[case::credentials(DeriveWsError::MissingCredentials { operation: "private/order".into() }, CommandFailure::not_sent("Session credentials are missing"), false)]
5975 #[case::authentication(DeriveWsError::Authentication { operation: "private/order".into(), reason: "opaque sensitive detail".into() }, CommandFailure::not_sent("Session authentication failed"), false)]
5976 #[case::transport(
5977 DeriveWsError::transport("opaque sensitive URL"),
5978 CommandFailure::ambiguous("Transport outcome is unconfirmed"),
5979 false
5980 )]
5981 #[case::timeout(DeriveWsError::Timeout { method: "private/order".into() }, CommandFailure::ambiguous("Order response is unconfirmed"), false)]
5982 #[case::cancelled(DeriveWsError::RequestCancelled { method: "private/order".into() }, CommandFailure::ambiguous("Order response is unconfirmed"), false)]
5983 #[case::venue(DeriveWsError::JsonRpc { code: 11008, message: "Post only order cannot cross the market".into(), data: None }, CommandFailure::venue_rejected("11008: Post only order cannot cross the market"), true)]
5984 #[case::internal(DeriveWsError::JsonRpc { code: -32603, message: "Internal error".into(), data: None }, CommandFailure::ambiguous("-32603: Internal error"), false)]
5985 fn test_ws_command_failure_keeps_evidence_separate_from_retry(
5986 #[case] error: DeriveWsError,
5987 #[case] expected: CommandFailure,
5988 #[case] due_post_only: bool,
5989 ) {
5990 assert_eq!(ws_command_failure(&error), (expected, due_post_only));
5991 }
5992
5993 const TEST_WALLET: &str = "0x0000000000000000000000000000000000001234";
5994 const TEST_SESSION_KEY: &str =
5995 "0x2ae8be44db8a590d20bffbe3b6872df9b569147d3bf6801a35a28281a4816bbd";
5996 const TEST_SUBACCOUNT: u64 = 30769;
5997
5998 #[rstest]
5999 #[case::newer(200, OrderStatus::Accepted, OrderStatus::Accepted, true)]
6000 #[case::older(50, OrderStatus::Canceled, OrderStatus::Accepted, false)]
6001 #[case::same_time_active(100, OrderStatus::Canceled, OrderStatus::Accepted, false)]
6002 #[case::same_time_terminal(100, OrderStatus::Accepted, OrderStatus::Canceled, true)]
6003 fn test_duplicate_reports_keep_latest_and_terminal_at_equal_time(
6004 #[case] timestamp: u64,
6005 #[case] previous_status: OrderStatus,
6006 #[case] status: OrderStatus,
6007 #[case] choose_next: bool,
6008 ) {
6009 let native: DeriveOrder = serde_json::from_str(include_str!(
6010 "../test_data/perps/http_order_eth_partially_filled.json"
6011 ))
6012 .unwrap();
6013 let mut previous = parse_derive_order_to_report_with_precision(
6014 &native,
6015 AccountId::from("DERIVE-001"),
6016 Some(2),
6017 Some(3),
6018 UnixNanos::from(77),
6019 )
6020 .unwrap();
6021 previous.ts_last = UnixNanos::from(100);
6022 previous.order_status = previous_status;
6023 let mut next = previous.clone();
6024 next.ts_last = UnixNanos::from(timestamp);
6025 next.order_status = status;
6026 next.report_id = UUID4::new();
6027 let expected = if choose_next {
6028 next.clone()
6029 } else {
6030 previous.clone()
6031 };
6032
6033 let actual = deduplicate_order_status_reports(vec![previous, next]).unwrap();
6034
6035 assert_eq!(actual, vec![expected]);
6036 }
6037
6038 #[rstest]
6039 fn test_duplicate_reports_reject_reopened_terminal_id(#[values(false, true)] reversed: bool) {
6040 let native: DeriveOrder = serde_json::from_str(include_str!(
6041 "../test_data/perps/http_order_eth_partially_filled.json"
6042 ))
6043 .unwrap();
6044 let mut terminal = parse_derive_order_to_report_with_precision(
6045 &native,
6046 AccountId::from("DERIVE-001"),
6047 Some(2),
6048 Some(3),
6049 UnixNanos::from(77),
6050 )
6051 .unwrap();
6052 terminal.order_status = OrderStatus::Canceled;
6053 terminal.ts_last = UnixNanos::from(100);
6054 let mut active = terminal.clone();
6055 active.order_status = OrderStatus::Accepted;
6056 active.ts_last = UnixNanos::from(200);
6057
6058 let reports = if reversed {
6059 vec![active, terminal]
6060 } else {
6061 vec![terminal, active]
6062 };
6063
6064 let result = deduplicate_order_status_reports(reports);
6065
6066 assert_eq!(
6067 result.unwrap_err().to_string(),
6068 format!("Conflicting Derive order status for {}", native.order_id)
6069 );
6070 }
6071
6072 #[rstest]
6073 #[case::account(0)]
6074 #[case::instrument(1)]
6075 #[case::side(2)]
6076 #[case::order_type(3)]
6077 #[case::client_order(4)]
6078 fn test_duplicate_reports_reject_conflicting_identity(#[case] field: u8) {
6079 let native: DeriveOrder = serde_json::from_str(include_str!(
6080 "../test_data/perps/http_order_eth_partially_filled.json"
6081 ))
6082 .unwrap();
6083 let previous = parse_derive_order_to_report_with_precision(
6084 &native,
6085 AccountId::from("DERIVE-001"),
6086 Some(2),
6087 Some(3),
6088 UnixNanos::from(77),
6089 )
6090 .unwrap();
6091 let mut next = previous.clone();
6092 next.ts_last = UnixNanos::from(previous.ts_last.as_u64() + 1);
6093 match field {
6094 0 => next.account_id = AccountId::from("DERIVE-002"),
6095 1 => next.instrument_id = InstrumentId::from("BTC-PERP.DERIVE"),
6096 2 => next.order_side = Some(OrderSide::Sell),
6097 3 => next.order_type = OrderType::Limit,
6098 4 => next.client_order_id = Some(ClientOrderId::from("OTHER-ORDER")),
6099 _ => unreachable!(),
6100 }
6101
6102 let result = deduplicate_order_status_reports(vec![previous, next]);
6103
6104 assert_eq!(
6105 result.unwrap_err().to_string(),
6106 format!("Conflicting Derive order identity for {}", native.order_id)
6107 );
6108 }
6109
6110 #[rstest]
6111 fn test_duplicate_reports_preserve_known_active_attribution(
6112 #[values(false, true)] reversed: bool,
6113 ) {
6114 let native: DeriveOrder = serde_json::from_str(include_str!(
6115 "../test_data/perps/http_order_eth_partially_filled.json"
6116 ))
6117 .unwrap();
6118 let mut known = parse_derive_order_to_report_with_precision(
6119 &native,
6120 AccountId::from("DERIVE-001"),
6121 Some(2),
6122 Some(3),
6123 UnixNanos::from(77),
6124 )
6125 .unwrap();
6126 known.order_status = OrderStatus::Accepted;
6127 known.client_order_id = Some(ClientOrderId::from("KNOWN-ORDER"));
6128 let mut unattributed = known.clone();
6129 unattributed.client_order_id = None;
6130 let expected = known.clone();
6131
6132 let reports = if reversed {
6133 vec![known, unattributed]
6134 } else {
6135 vec![unattributed, known]
6136 };
6137
6138 let actual = deduplicate_order_status_reports(reports).unwrap();
6139
6140 assert_eq!(actual, vec![expected]);
6141 }
6142
6143 #[rstest]
6144 #[case([0, 1, 2])]
6145 #[case([0, 2, 1])]
6146 #[case([1, 0, 2])]
6147 #[case([1, 2, 0])]
6148 #[case([2, 0, 1])]
6149 #[case([2, 1, 0])]
6150 fn test_duplicate_reports_retain_all_conflict_evidence(
6151 #[case] permutation: [usize; 3],
6152 #[values(false, true)] identity_conflict: bool,
6153 ) {
6154 let native: DeriveOrder = serde_json::from_str(include_str!(
6155 "../test_data/perps/http_order_eth_partially_filled.json"
6156 ))
6157 .unwrap();
6158 let report = parse_derive_order_to_report_with_precision(
6159 &native,
6160 AccountId::from("DERIVE-001"),
6161 Some(2),
6162 Some(3),
6163 UnixNanos::from(77),
6164 )
6165 .unwrap();
6166 let mut observations = [report.clone(), report.clone(), report];
6167
6168 let timestamps = if identity_conflict {
6169 [100, 200, 300]
6170 } else {
6171 [100, 300, 200]
6172 };
6173
6174 for (index, observation) in observations.iter_mut().enumerate() {
6175 observation.ts_last = UnixNanos::from(timestamps[index]);
6176 observation.order_status = if identity_conflict || index == 2 {
6177 OrderStatus::Accepted
6178 } else {
6179 OrderStatus::Canceled
6180 };
6181 }
6182
6183 if identity_conflict {
6184 observations[0].client_order_id = Some(ClientOrderId::from("FIRST-ORDER"));
6185 observations[1].client_order_id = None;
6186 observations[2].client_order_id = Some(ClientOrderId::from("OTHER-ORDER"));
6187 }
6188
6189 let reports = permutation
6190 .into_iter()
6191 .map(|index| observations[index].clone())
6192 .collect();
6193 let result = deduplicate_order_status_reports(reports);
6194
6195 let conflict = if identity_conflict {
6196 "identity"
6197 } else {
6198 "status"
6199 };
6200
6201 assert_eq!(
6202 result.unwrap_err().to_string(),
6203 format!(
6204 "Conflicting Derive order {conflict} for {}",
6205 native.order_id
6206 )
6207 );
6208 }
6209
6210 fn test_core() -> ExecutionClientCore {
6211 let cache = Rc::new(RefCell::new(Cache::default()));
6212
6213 ExecutionClientCore::new(
6214 TraderId::from("TRADER-001"),
6215 ClientId::from(DERIVE),
6216 *DERIVE_VENUE,
6217 OmsType::Netting,
6218 AccountId::from("DERIVE-001"),
6219 AccountType::Margin,
6220 None,
6221 cache,
6222 )
6223 }
6224
6225 fn test_config() -> DeriveExecutionClientConfig {
6226 DeriveExecutionClientConfig {
6227 wallet_address: Some(TEST_WALLET.to_string()),
6228 session_key: Some(TEST_SESSION_KEY.into()),
6229 subaccount_id: Some(TEST_SUBACCOUNT),
6230 environment: DeriveEnvironment::Testnet,
6231 domain_separator: Some(
6232 "0x2222222222222222222222222222222222222222222222222222222222222222".to_string(),
6233 ),
6234 action_typehash: Some(
6235 "0x1111111111111111111111111111111111111111111111111111111111111111".to_string(),
6236 ),
6237 trade_module_address: Some("0x000000000000000000000000000000000000bbbb".to_string()),
6238 max_fee_per_contract: Some(dec!(1000)),
6239 ..DeriveExecutionClientConfig::default()
6240 }
6241 }
6242
6243 #[tokio::test]
6244 async fn test_execution_task_log_omits_provider_error() {
6245 let _capture = client_logging::capture_logs().await;
6246 let (sender, _events) = tokio::sync::mpsc::unbounded_channel();
6247 nautilus_common::live::runner::replace_exec_event_sender(sender);
6248 let client = DeriveExecutionClient::new(test_core(), test_config()).unwrap();
6249 client.spawn_task("Synthetic order command PUB-ORDER".to_string(), async {
6250 anyhow::bail!("SYN-secret-provider-error")
6251 });
6252
6253 tokio::time::timeout(Duration::from_secs(2), async {
6254 while !client_logging::messages().contains(&(
6255 log::Level::Warn,
6256 "Synthetic order command PUB-ORDER failed".to_string(),
6257 )) {
6258 tokio::task::yield_now().await;
6259 }
6260 })
6261 .await
6262 .unwrap();
6263
6264 client.pending_tasks.begin_shutdown();
6265 client
6266 .pending_tasks
6267 .finish_shutdown(Duration::from_secs(1), Duration::from_secs(1))
6268 .await
6269 .unwrap();
6270 let messages = client_logging::messages();
6271 assert!(messages.contains(&(
6272 log::Level::Warn,
6273 "Synthetic order command PUB-ORDER failed".to_string()
6274 )));
6275 assert!(
6276 !messages
6277 .iter()
6278 .any(|(_, message)| message.contains("SYN-secret"))
6279 );
6280 }
6281
6282 #[tokio::test]
6283 async fn test_execution_session_failure_log_omits_provider_reason() {
6284 let _capture = client_logging::capture_logs().await;
6285 let (sender, mut events) = tokio::sync::mpsc::unbounded_channel();
6286 nautilus_common::live::runner::replace_exec_event_sender(sender);
6287 let client = DeriveExecutionClient::new(test_core(), test_config()).unwrap();
6288 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
6289 client.is_connected.store(true, Ordering::Release);
6290 client.start_ws_dispatch(rx).unwrap();
6291 tx.send(DeriveWsMessage::SessionRecoveryFailed(
6292 "SYN-secret-provider-reason".to_string(),
6293 ))
6294 .unwrap();
6295
6296 tokio::time::timeout(Duration::from_secs(2), async {
6297 while client.is_connected() {
6298 tokio::task::yield_now().await;
6299 }
6300 })
6301 .await
6302 .unwrap();
6303
6304 drop(tx);
6305 client.session_tasks.begin_shutdown();
6306 client
6307 .session_tasks
6308 .finish_shutdown(Duration::from_secs(1), Duration::from_secs(1))
6309 .await
6310 .unwrap();
6311 let messages = client_logging::messages();
6312 assert!(!client.is_connected());
6313 assert!(events.try_recv().is_err());
6314 assert!(messages.contains(&(
6315 log::Level::Error,
6316 "Derive execution WebSocket recovery failed".to_string()
6317 )));
6318 assert!(
6319 !messages
6320 .iter()
6321 .any(|(_, message)| message.contains("SYN-secret"))
6322 );
6323 }
6324
6325 #[tokio::test]
6326 async fn test_private_stream_closure_marks_disconnected_without_terminal_events() {
6327 let (sender, mut events) = tokio::sync::mpsc::unbounded_channel();
6328 nautilus_common::live::runner::replace_exec_event_sender(sender);
6329 let client = DeriveExecutionClient::new(test_core(), test_config()).unwrap();
6330 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
6331 client.is_connected.store(true, Ordering::Release);
6332 client.start_ws_dispatch(rx).unwrap();
6333 assert!(client.is_connected());
6334 drop(tx);
6335 client.session_tasks.begin_shutdown();
6336 client
6337 .session_tasks
6338 .finish_shutdown(Duration::from_secs(1), Duration::from_secs(1))
6339 .await
6340 .unwrap();
6341 assert!(!client.is_connected());
6342 assert!(events.try_recv().is_err());
6343 }
6344
6345 #[tokio::test]
6346 async fn test_reconnect_context_rejects_evicted_replacement_proof() {
6347 let cache = Rc::new(RefCell::new(Cache::default()));
6348
6349 let core = ExecutionClientCore::new(
6350 TraderId::from("TRADER-001"),
6351 ClientId::from(DERIVE),
6352 *DERIVE_VENUE,
6353 OmsType::Netting,
6354 AccountId::from("DERIVE-001"),
6355 AccountType::Margin,
6356 None,
6357 cache.clone(),
6358 );
6359 let client = DeriveExecutionClient::new(core, test_config()).unwrap();
6360 let stale = client.reconciliation_context();
6361 let (time_sender, mut time_events) = tokio::sync::mpsc::unbounded_channel();
6362 nautilus_common::runner::set_time_event_sender(Arc::new(ReconciliationTimeSender(
6363 time_sender,
6364 )));
6365 let request = ReconciliationSnapshotRequest::new(
6366 client.core.clone(),
6367 stale.clone(),
6368 client.cancellation_token.clone(),
6369 Duration::from_secs(1),
6370 )
6371 .unwrap();
6372 let cid = ClientOrderId::from("AFTER-CONNECT-REPLACEMENT");
6373 let instrument_id = InstrumentId::from("ETH-PERP.DERIVE");
6374 let parent = VenueOrderId::from("expired-proof-parent");
6375 let current = VenueOrderId::from("expired-proof-current");
6376 let account_id = AccountId::from("DERIVE-001");
6377 let mut order = OrderTestBuilder::new(OrderType::Limit)
6378 .trader_id(client.core.trader_id)
6379 .strategy_id(StrategyId::from("S-1"))
6380 .instrument_id(instrument_id)
6381 .client_order_id(cid)
6382 .side(OrderSide::Buy)
6383 .quantity(Quantity::from("1.000"))
6384 .price(Price::from("3500.00"))
6385 .build();
6386 order
6387 .apply(OrderEventAny::Accepted(OrderAccepted::new(
6388 order.trader_id(),
6389 order.strategy_id(),
6390 instrument_id,
6391 cid,
6392 parent,
6393 account_id,
6394 UUID4::new(),
6395 UnixNanos::from(1),
6396 UnixNanos::from(1),
6397 false,
6398 )))
6399 .unwrap();
6400 order
6401 .apply(OrderEventAny::Updated(OrderUpdated::new(
6402 order.trader_id(),
6403 order.strategy_id(),
6404 instrument_id,
6405 cid,
6406 Quantity::from("1.500"),
6407 UUID4::new(),
6408 UnixNanos::from(2),
6409 UnixNanos::from(2),
6410 false,
6411 Some(current),
6412 Some(account_id),
6413 Some(Price::from("3505.00")),
6414 None,
6415 None,
6416 false,
6417 )))
6418 .unwrap();
6419 client.dispatch_state.restore_order(&order);
6420 client.dispatch_state.forget(&cid);
6421 order
6422 .apply(OrderEventAny::Canceled(OrderCanceled::new(
6423 order.trader_id(),
6424 order.strategy_id(),
6425 instrument_id,
6426 cid,
6427 UUID4::new(),
6428 UnixNanos::from(3),
6429 UnixNanos::from(3),
6430 false,
6431 Some(current),
6432 Some(account_id),
6433 None,
6434 )))
6435 .unwrap();
6436 cache
6437 .borrow_mut()
6438 .add_order(order, None, Some(client.core.client_id), false)
6439 .unwrap();
6440
6441 let identity = OrderIdentity {
6442 instrument_id,
6443 strategy_id: StrategyId::from("S-1"),
6444 order_side: OrderSide::Buy,
6445 order_type: OrderType::Limit,
6446 };
6447
6448 for index in 0..crate::websocket::dispatch::ORDER_DEDUP_CAPACITY {
6449 let other = ClientOrderId::from(format!("EVICT-{index}").as_str());
6450 client.dispatch_state.register_identity(other, identity);
6451 client.dispatch_state.record_venue_order_id(
6452 other,
6453 VenueOrderId::from(format!("eviction-{index}").as_str()),
6454 );
6455 client.dispatch_state.forget(&other);
6456 }
6457
6458 let mut native: DeriveOrder = serde_json::from_str(include_str!(
6459 "../test_data/perps/http_order_eth_partially_filled.json"
6460 ))
6461 .unwrap();
6462 native.order_id = current.to_string();
6463 native.label = cid.inner();
6464 native.amount = dec!(1.2);
6465 native.filled_amount = dec!(0.2);
6466 native.average_price = dec!(3505);
6467 native.order_status = DeriveOrderStatus::Open;
6468 native.order_type = DeriveOrderType::Limit;
6469 native.time_in_force = crate::common::enums::DeriveTimeInForce::Gtc;
6470 native.replaced_order_id = None;
6471 let report = parse_derive_order_to_report_with_precision(
6472 &native,
6473 account_id,
6474 Some(2),
6475 Some(3),
6476 UnixNanos::from(4),
6477 )
6478 .unwrap();
6479 let result = stale.project_order_report(&native, report, &[], true).await;
6480 let fresh = client.reconciliation_context();
6481 let binding = fresh.order_binding(cid).unwrap();
6482 let mut legs = binding.venue_order_legs.clone();
6483 legs.sort_unstable();
6484 assert_eq!(stale.order_binding(cid), None);
6485 assert_eq!(binding.identity, identity);
6486 assert_eq!(binding.venue_order_id, Some(current));
6487 assert_eq!(legs, vec![current, parent]);
6488 assert_eq!(
6489 result.unwrap_err().to_string(),
6490 "Derive order context expired after terminal binding eviction; retry with a fresh cache snapshot"
6491 );
6492
6493 let worker_request = request.clone();
6494 let snapshot_task = tokio::spawn(async move { worker_request.snapshot().await });
6495 assert!(time_events.recv().await.unwrap().dispatch());
6496 let (snapshot, _) = snapshot_task.await.unwrap().unwrap();
6497 let refreshed = snapshot.order_binding(cid).unwrap();
6498 let mut refreshed_legs = refreshed.venue_order_legs.clone();
6499 refreshed_legs.sort_unstable();
6500
6501 assert_eq!(refreshed.identity, identity);
6502 assert_eq!(refreshed.venue_order_id, Some(current));
6503 assert_eq!(refreshed_legs, vec![current, parent]);
6504 snapshot.ensure_order_context().unwrap();
6505 request.close();
6506
6507 let empty_query = GenerateOrderStatusReport::new(
6508 UUID4::new(),
6509 UnixNanos::from(5),
6510 None,
6511 None,
6512 None,
6513 None,
6514 None,
6515 );
6516 assert!(
6517 fresh
6518 .generate_order_status_report(&empty_query)
6519 .await
6520 .unwrap()
6521 .is_none()
6522 );
6523 }
6524
6525 #[tokio::test]
6526 async fn test_canceled_reconciliation_callback_never_reads_or_rearms_cache() {
6527 let cache = Rc::new(RefCell::new(Cache::default()));
6528
6529 let core = ExecutionClientCore::new(
6530 TraderId::from("TRADER-001"),
6531 ClientId::from(DERIVE),
6532 *DERIVE_VENUE,
6533 OmsType::Netting,
6534 AccountId::from("DERIVE-001"),
6535 AccountType::Margin,
6536 None,
6537 cache.clone(),
6538 );
6539 let client = DeriveExecutionClient::new(core, test_config()).unwrap();
6540 let (sender, _events) = tokio::sync::mpsc::unbounded_channel();
6541 nautilus_common::runner::set_time_event_sender(Arc::new(ReconciliationTimeSender(sender)));
6542 let request = ReconciliationSnapshotRequest::new(
6543 client.core.clone(),
6544 client.reconciliation_context(),
6545 client.cancellation_token.clone(),
6546 Duration::from_secs(1),
6547 )
6548 .unwrap();
6549 let pending = request.next.lock().take().unwrap();
6550 client.cancellation_token.cancel();
6551 request.close();
6552 let cache_borrow = cache.borrow_mut();
6553 let dispatched = pending.message.dispatch();
6554 drop(cache_borrow);
6555
6556 assert!(dispatched);
6557 assert!(pending.result.await.is_err());
6558 assert!(request.next.lock().is_none());
6559 assert_eq!(Rc::strong_count(&cache), 2);
6560 }
6561
6562 #[rstest]
6563 fn test_market_order_limit_price_buy_lifts_ask_and_rounds_up_to_tick() {
6564 let quote = QuoteTick::new(
6565 InstrumentId::from("ETH-PERP.DERIVE"),
6566 Price::from("3500.00"),
6567 Price::from("3501.00"),
6568 Quantity::from("1.000"),
6569 Quantity::from("1.000"),
6570 UnixNanos::from(0),
6571 UnixNanos::from(0),
6572 );
6573 let price = market_order_limit_price("e, OrderSide::Buy, 50, dec!(0.01)).unwrap();
6575 assert_eq!(price, dec!(3518.51));
6576 }
6577
6578 #[rstest]
6579 fn test_market_order_limit_price_sell_drops_bid_rounds_down_and_denies_non_positive() {
6580 let quote = QuoteTick::new(
6581 InstrumentId::from("ETH-PERP.DERIVE"),
6582 Price::from("3500.00"),
6583 Price::from("3501.00"),
6584 Quantity::from("1.000"),
6585 Quantity::from("1.000"),
6586 UnixNanos::from(0),
6587 UnixNanos::from(0),
6588 );
6589 let price = market_order_limit_price("e, OrderSide::Sell, 50, dec!(0.01)).unwrap();
6591 assert_eq!(price, dec!(3482.5));
6592
6593 let zero = market_order_limit_price("e, OrderSide::Sell, 20_000, dec!(0.01));
6595 assert!(zero.is_none());
6596 }
6597
6598 #[rstest]
6599 fn test_trigger_market_limit_price_uses_trigger_price_bound() {
6600 let buy = trigger_market_limit_price(dec!(3600), OrderSide::Buy, 50, dec!(0.01)).unwrap();
6601 let sell = trigger_market_limit_price(dec!(3600), OrderSide::Sell, 50, dec!(0.01)).unwrap();
6602 let zero = trigger_market_limit_price(dec!(1), OrderSide::Sell, 20_000, dec!(0.01));
6603
6604 assert_eq!(buy, dec!(3618));
6605 assert_eq!(sell, dec!(3582));
6606 assert!(zero.is_none());
6607 }
6608
6609 #[rstest]
6610 fn test_normal_order_signature_expiry_accepts_ttl_above_minimum() {
6611 let clock = get_atomic_clock_realtime();
6612 let start_secs = (clock.get_time_ns().as_u64() / 1_000_000_000) as i64;
6613 let ttl_secs = MIN_SIGNATURE_TTL.as_secs() + 1;
6614
6615 let expiry = normal_order_signature_expiry(clock, ttl_secs).expect("expiry is valid");
6616
6617 assert!(expiry >= start_secs + ttl_secs as i64);
6618 }
6619
6620 #[rstest]
6621 #[case(MIN_SIGNATURE_TTL.as_secs(), "must be greater than the Derive minimum")]
6622 #[case(MIN_SIGNATURE_TTL.as_secs() - 1, "must be greater than the Derive minimum")]
6623 fn test_normal_order_signature_expiry_rejects_minimum_or_lower_ttl(
6624 #[case] ttl_secs: u64,
6625 #[case] reason_fragment: &str,
6626 ) {
6627 let clock = get_atomic_clock_realtime();
6628
6629 let err = normal_order_signature_expiry(clock, ttl_secs).expect_err("TTL is too short");
6630
6631 assert!(
6632 err.to_string().contains(reason_fragment),
6633 "unexpected error: {err}",
6634 );
6635 }
6636
6637 #[rstest]
6638 #[case(i64::MAX as u64, "exceeds the Derive maximum")]
6639 #[case(u64::MAX, "exceeds the Derive maximum")]
6640 fn test_normal_order_signature_expiry_rejects_extreme_ttl(
6641 #[case] ttl_secs: u64,
6642 #[case] reason_fragment: &str,
6643 ) {
6644 let clock = get_atomic_clock_realtime();
6645
6646 let err = normal_order_signature_expiry(clock, ttl_secs).expect_err("TTL is invalid");
6647
6648 assert!(
6649 err.to_string().contains(reason_fragment),
6650 "unexpected error: {err}",
6651 );
6652 }
6653
6654 #[rstest]
6655 #[case(None, "max_fee_per_contract is required")]
6656 #[case(Some(dec!(0)), "max_fee_per_contract must be greater than zero")]
6657 #[case(Some(dec!(-1)), "max_fee_per_contract must be greater than zero")]
6658 fn test_new_rejects_invalid_max_fee_per_contract(
6659 #[case] max_fee_per_contract: Option<Decimal>,
6660 #[case] expected: &str,
6661 ) {
6662 let mut config = test_config();
6663 config.max_fee_per_contract = max_fee_per_contract;
6664
6665 let err = DeriveExecutionClient::new(test_core(), config).expect_err("must reject");
6666
6667 assert_eq!(err.to_string(), expected);
6668 }
6669
6670 #[rstest]
6671 #[case(OrderType::StopMarket, true)]
6672 #[case(OrderType::StopLimit, true)]
6673 #[case(OrderType::MarketIfTouched, true)]
6674 #[case(OrderType::LimitIfTouched, true)]
6675 #[case(OrderType::Market, false)]
6676 #[case(OrderType::Limit, false)]
6677 #[case(OrderType::MarketToLimit, false)]
6678 #[case(OrderType::TrailingStopMarket, false)]
6679 fn test_is_derive_trigger_order_type(#[case] order_type: OrderType, #[case] expected: bool) {
6680 assert_eq!(is_derive_trigger_order_type(order_type), expected);
6681 }
6682
6683 #[rstest]
6684 fn test_resolve_submit_nonce_emits_rejection_and_forgets_identity() {
6685 let clock = get_atomic_clock_realtime();
6686 let instrument_id = InstrumentId::from("ETH-PERP.DERIVE");
6687 let strategy_id = StrategyId::from("S-1");
6688 let client_order_id = ClientOrderId::from("NONCE-SUBMIT-1");
6689 let order = OrderTestBuilder::new(OrderType::Limit)
6690 .trader_id(TraderId::from("TRADER-001"))
6691 .strategy_id(strategy_id)
6692 .instrument_id(instrument_id)
6693 .client_order_id(client_order_id)
6694 .side(OrderSide::Buy)
6695 .quantity(Quantity::from("1.000"))
6696 .price(Price::from("3500.00"))
6697 .build();
6698
6699 let identity = OrderIdentity {
6700 instrument_id,
6701 strategy_id,
6702 order_side: OrderSide::Buy,
6703 order_type: OrderType::Limit,
6704 };
6705
6706 let state = WsDispatchState::new(42);
6707 state.register_identity(client_order_id, identity);
6708 let (emitter, mut rx) = test_emitter(clock);
6709
6710 let nonce = resolve_submit_nonce(
6711 Err(NonceError::ClockBeforeEpoch),
6712 &emitter,
6713 &state,
6714 &order,
6715 clock,
6716 );
6717 let event = rx.try_recv().expect("OrderRejected event");
6718
6719 assert!(nonce.is_none());
6720 assert!(state.identity(&client_order_id).is_none());
6721
6722 if let ExecutionEvent::Order(OrderEventAny::Rejected(rejected)) = event {
6723 assert_eq!(rejected.client_order_id, client_order_id);
6724 assert_eq!(
6725 rejected.reason,
6726 "nonce allocation failed: system clock is before UNIX epoch",
6727 );
6728 } else {
6729 panic!("expected OrderRejected, event was {event:?}");
6730 }
6731 }
6732
6733 #[rstest]
6734 fn test_resolve_modify_nonce_emits_modify_rejection() {
6735 let clock = get_atomic_clock_realtime();
6736 let instrument_id = InstrumentId::from("ETH-PERP.DERIVE");
6737 let strategy_id = StrategyId::from("S-1");
6738 let client_order_id = ClientOrderId::from("NONCE-MODIFY-1");
6739 let venue_order_id = VenueOrderId::from("ord-nonce-modify-1");
6740 let (emitter, mut rx) = test_emitter(clock);
6741
6742 let nonce = resolve_modify_nonce(
6743 Err(NonceError::ClockBeforeEpoch),
6744 &emitter,
6745 strategy_id,
6746 instrument_id,
6747 client_order_id,
6748 venue_order_id,
6749 clock,
6750 );
6751 let event = rx.try_recv().expect("OrderModifyRejected event");
6752
6753 assert!(nonce.is_none());
6754
6755 if let ExecutionEvent::Order(OrderEventAny::ModifyRejected(rejected)) = event {
6756 assert_eq!(rejected.client_order_id, client_order_id);
6757 assert_eq!(rejected.venue_order_id, Some(venue_order_id));
6758 assert_eq!(
6759 rejected.reason,
6760 "nonce allocation failed: system clock is before UNIX epoch",
6761 );
6762 } else {
6763 panic!("expected OrderModifyRejected, event was {event:?}");
6764 }
6765 }
6766
6767 #[rstest]
6768 #[case(dec!(0))]
6769 #[case(dec!(-1))]
6770 fn test_round_to_tick_treats_non_positive_tick_as_no_op(#[case] tick: Decimal) {
6771 assert_eq!(
6774 round_to_tick(dec!(3501.55), tick, OrderSide::Buy),
6775 dec!(3501.55)
6776 );
6777 assert_eq!(
6778 round_to_tick(dec!(3501.55), tick, OrderSide::Sell),
6779 dec!(3501.55)
6780 );
6781 }
6782
6783 #[rstest]
6784 fn test_resolve_signing_context_rejects_placeholder_domain_separator() {
6785 let mut config = test_config();
6789 config.environment = DeriveEnvironment::Mainnet;
6790 config.domain_separator =
6791 Some("0x<paste_from_docs.derive.xyz_protocol_constants>".to_string());
6792 let err = DeriveExecutionClient::new(test_core(), config).expect_err("must reject");
6793 let msg = err.to_string();
6794 assert!(msg.contains("placeholder"), "unexpected error: {msg}",);
6795 }
6796
6797 #[rstest]
6798 fn test_resolve_signing_context_uses_mainnet_defaults() {
6799 let mut config = test_config();
6800 config.environment = DeriveEnvironment::Mainnet;
6801 config.domain_separator = None;
6802 config.action_typehash = None;
6803 config.trade_module_address = None;
6804
6805 DeriveExecutionClient::new(test_core(), config).expect("mainnet defaults should parse");
6806 }
6807
6808 #[rstest]
6809 fn test_resolve_signing_context_uses_testnet_defaults() {
6810 let mut config = test_config();
6811 config.environment = DeriveEnvironment::Testnet;
6812 config.domain_separator = None;
6813 config.action_typehash = None;
6814 config.trade_module_address = None;
6815
6816 DeriveExecutionClient::new(test_core(), config).expect("testnet defaults should parse");
6817 }
6818
6819 #[rstest]
6820 fn test_market_order_limit_price_rounds_to_coarse_tick() {
6821 let quote = QuoteTick::new(
6824 InstrumentId::from("ETH-20260627-3500-C.DERIVE"),
6825 Price::from("3500"),
6826 Price::from("3501"),
6827 Quantity::from("1.000"),
6828 Quantity::from("1.000"),
6829 UnixNanos::from(0),
6830 UnixNanos::from(0),
6831 );
6832 let buy = market_order_limit_price("e, OrderSide::Buy, 50, dec!(1)).unwrap();
6833 assert_eq!(buy, dec!(3519));
6834 let sell = market_order_limit_price("e, OrderSide::Sell, 50, dec!(1)).unwrap();
6835 assert_eq!(sell, dec!(3482));
6836 }
6837
6838 #[rstest]
6839 fn test_new_populates_identity() {
6840 let core = test_core();
6841 let client = DeriveExecutionClient::new(core, test_config()).unwrap();
6842
6843 assert_eq!(client.client_id(), ClientId::from(DERIVE));
6844 assert_eq!(client.account_id(), AccountId::from("DERIVE-001"));
6845 assert_eq!(client.venue(), *DERIVE_VENUE);
6846 assert_eq!(client.oms_type(), OmsType::Netting);
6847 assert_eq!(client.subaccount_id(), TEST_SUBACCOUNT);
6848 assert!(!client.is_connected());
6849 }
6850
6851 #[rstest]
6852 fn test_cache_instrument_registers_report_precision() {
6853 let client = DeriveExecutionClient::new(test_core(), test_config()).unwrap();
6854 let instrument = sample_derive_instrument();
6855 let instrument_id = format_instrument_id(instrument.instrument_name).unwrap();
6856
6857 client.cache_instrument(instrument).unwrap();
6858
6859 assert_eq!(
6860 client.dispatch_state.instrument_precision(&instrument_id),
6861 Some((2, 3)),
6862 );
6863 }
6864
6865 #[rstest]
6866 fn test_native_instrument_retains_signing_metadata() {
6867 let client = test_client_with_instrument();
6868 let expected = sample_derive_instrument();
6869 let instrument_id = format_instrument_id(expected.instrument_name).unwrap();
6870 let actual = client.instruments.get_cloned(&instrument_id).unwrap();
6871 assert_eq!(
6872 serde_json::to_value(actual).unwrap(),
6873 serde_json::to_value(expected).unwrap()
6874 );
6875 assert_eq!(
6876 client.dispatch_state.instrument_precision(&instrument_id),
6877 Some((2, 3))
6878 );
6879 }
6880
6881 #[rstest]
6882 #[tokio::test]
6883 async fn test_bootstrap_restores_cached_instrument_context() {
6884 let cache = Rc::new(RefCell::new(Cache::default()));
6885
6886 let core = ExecutionClientCore::new(
6887 TraderId::from("TRADER-001"),
6888 ClientId::from(DERIVE),
6889 *DERIVE_VENUE,
6890 OmsType::Netting,
6891 AccountId::from("DERIVE-001"),
6892 AccountType::Margin,
6893 None,
6894 cache.clone(),
6895 );
6896 let client = DeriveExecutionClient::new(core, test_config()).unwrap();
6897 let expected = sample_derive_instrument();
6898 let instrument_id = format_instrument_id(expected.instrument_name).unwrap();
6899 let native = parse_derive_instrument_any(&expected, UnixNanos::default())
6900 .unwrap()
6901 .unwrap();
6902 cache.borrow_mut().add_instrument(native).unwrap();
6903 client.ensure_instruments_initialized().await.unwrap();
6904 let actual = client.instruments.get_cloned(&instrument_id).unwrap();
6905 assert_eq!(
6906 serde_json::to_value(actual).unwrap(),
6907 serde_json::to_value(expected).unwrap()
6908 );
6909 assert_eq!(
6910 client.dispatch_state.instrument_precision(&instrument_id),
6911 Some((2, 3))
6912 );
6913 assert!(client.core.instruments_initialized());
6914 }
6915
6916 #[rstest]
6917 fn test_order_dispatch_uses_registered_instrument_precision() {
6918 let clock = get_atomic_clock_realtime();
6919 let client = test_client_with_instrument();
6920
6921 let mut order: DeriveOrder = serde_json::from_str(include_str!(
6922 "../test_data/perps/http_order_eth_partially_filled.json"
6923 ))
6924 .unwrap();
6925 order.subaccount_id = client.subaccount_id() as i64;
6926 order.amount = Decimal::from_str_exact("25.000").unwrap();
6927 order.filled_amount = Decimal::from_str_exact("5.000").unwrap();
6928 order.limit_price = Decimal::from_str_exact("25.000").unwrap();
6929 order.order_status = DeriveOrderStatus::Open;
6930 order.order_type = DeriveOrderType::Limit;
6931
6932 let (emitter, mut rx) = test_emitter(clock);
6933 dispatch_orders_payload(
6934 DeriveOrdersSubscriptionData {
6935 orders: vec![order],
6936 },
6937 &emitter,
6938 AccountId::from("DERIVE-001"),
6939 clock,
6940 &client.dispatch_state,
6941 );
6942
6943 let event = rx.try_recv().unwrap();
6944
6945 let ExecutionEvent::Report(ExecutionReport::Order(report)) = event else {
6946 panic!("Expected OrderStatusReport");
6947 };
6948
6949 assert_eq!(report.price, Some(Price::from("25.00")));
6950 assert_eq!(report.price.unwrap().precision, 2);
6951 assert_eq!(report.quantity, Quantity::from("25.000"));
6952 assert_eq!(report.quantity.precision, 3);
6953 assert_eq!(report.filled_qty, Quantity::from("5.000"));
6954 assert_eq!(report.filled_qty.precision, 3);
6955 }
6956
6957 #[rstest]
6958 fn test_trade_dispatch_uses_registered_instrument_precision() {
6959 let clock = get_atomic_clock_realtime();
6960 let client = test_client_with_instrument();
6961 let mut trade: DeriveTrade = serde_json::from_str(include_str!(
6962 "../test_data/perps/http_private_trade_eth.json"
6963 ))
6964 .unwrap();
6965 trade.subaccount_id = client.subaccount_id() as i64;
6966 trade.trade_amount = Decimal::from_str_exact("25.000").unwrap();
6967 trade.trade_price = Decimal::from_str_exact("25.000").unwrap();
6968
6969 let (emitter, mut rx) = test_emitter(clock);
6970 dispatch_trades_payload(
6971 DeriveTradesSubscriptionData {
6972 trades: vec![trade],
6973 },
6974 &emitter,
6975 AccountId::from("DERIVE-001"),
6976 clock,
6977 &client.dispatch_state,
6978 );
6979
6980 let event = rx.try_recv().unwrap();
6981
6982 let ExecutionEvent::Report(ExecutionReport::Fill(report)) = event else {
6983 panic!("Expected FillReport");
6984 };
6985
6986 assert_eq!(report.last_px, Price::from("25.00"));
6987 assert_eq!(report.last_px.precision, 2);
6988 assert_eq!(report.last_qty, Quantity::from("25.000"));
6989 assert_eq!(report.last_qty.precision, 3);
6990 }
6991
6992 #[rstest]
6993 fn test_emit_tracked_event_suppresses_in_flight_replace_cancel_leg() {
6994 let clock = get_atomic_clock_realtime();
7001 let account_id = AccountId::from("DERIVE-001");
7002 let instrument_id = InstrumentId::from("ETH-PERP.DERIVE");
7003 let cid = ClientOrderId::from("STRAT-MOD-INFLIGHT");
7004 let stale_voi = VenueOrderId::from("ord-stale-1");
7005
7006 let identity = OrderIdentity {
7007 instrument_id,
7008 strategy_id: StrategyId::from("S-1"),
7009 order_side: OrderSide::Buy,
7010 order_type: OrderType::Limit,
7011 };
7012
7013 let report = OrderStatusReport::new(
7016 account_id,
7017 instrument_id,
7018 Some(cid),
7019 stale_voi,
7020 OrderSide::Buy.into(),
7021 OrderType::Limit,
7022 TimeInForce::Gtc,
7023 OrderStatus::Canceled,
7024 Quantity::from("1.000"),
7025 Quantity::from("0.000"),
7026 UnixNanos::from(1_000),
7027 UnixNanos::from(2_000),
7028 UnixNanos::from(3_000),
7029 None,
7030 );
7031
7032 let (emitter, mut rx) = test_emitter(clock);
7035 let state = WsDispatchState::new(42);
7036 state.mark_pending_modify(cid, stale_voi);
7037 emit_tracked_order_event(
7038 &emitter,
7039 &state,
7040 cid,
7041 identity,
7042 &report,
7043 account_id,
7044 UnixNanos::from(0),
7045 );
7046 let suppressed = rx.try_recv().is_err();
7047
7048 let (emitter, mut rx) = test_emitter(clock);
7051 let state = WsDispatchState::new(42);
7052 state.mark_pending_modify(cid, VenueOrderId::from("ord-other"));
7053 emit_tracked_order_event(
7054 &emitter,
7055 &state,
7056 cid,
7057 identity,
7058 &report,
7059 account_id,
7060 UnixNanos::from(0),
7061 );
7062 let mut saw_canceled = false;
7063
7064 while let Ok(event) = rx.try_recv() {
7065 if matches!(event, ExecutionEvent::Order(OrderEventAny::Canceled(_))) {
7066 saw_canceled = true;
7067 }
7068 }
7069
7070 assert!(
7071 suppressed,
7072 "in-flight cancel-of-old leg must be suppressed by the pending-modify marker",
7073 );
7074 assert!(
7075 saw_canceled,
7076 "a pending-modify marker for a different venue order id must not suppress",
7077 );
7078 }
7079
7080 #[rstest]
7081 fn test_ensure_canceled_emitted_is_idempotent() {
7082 let clock = get_atomic_clock_realtime();
7083 let account_id = AccountId::from("DERIVE-001");
7084 let client_order_id = ClientOrderId::from("TRIGGER-CANCEL-1");
7085
7086 let identity = OrderIdentity {
7087 instrument_id: InstrumentId::from("ETH-PERP.DERIVE"),
7088 strategy_id: StrategyId::from("S-1"),
7089 order_side: OrderSide::Buy,
7090 order_type: OrderType::StopMarket,
7091 };
7092
7093 let venue_order_id = VenueOrderId::from("trigger-cancel-1");
7094 let state = WsDispatchState::new(42);
7095 let (emitter, mut rx) = test_emitter(clock);
7096
7097 for _ in 0..2 {
7098 ensure_canceled_emitted(
7099 &emitter,
7100 &state,
7101 client_order_id,
7102 identity,
7103 venue_order_id,
7104 account_id,
7105 UnixNanos::from(1_000),
7106 UnixNanos::from(1_000),
7107 );
7108 }
7109
7110 assert!(matches!(
7111 rx.try_recv(),
7112 Ok(ExecutionEvent::Order(OrderEventAny::Canceled(_)))
7113 ));
7114 assert!(rx.try_recv().is_err(), "duplicate OrderCanceled emitted");
7115 }
7116
7117 fn test_emitter(
7118 clock: &'static AtomicTime,
7119 ) -> (
7120 ExecutionEventEmitter,
7121 tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
7122 ) {
7123 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
7124
7125 let mut emitter = ExecutionEventEmitter::new(
7126 clock,
7127 TraderId::from("TRADER-001"),
7128 AccountId::from("DERIVE-001"),
7129 AccountType::Margin,
7130 Some(Currency::USDC()),
7131 );
7132 emitter.set_sender(tx);
7133 (emitter, rx)
7134 }
7135
7136 #[rstest]
7137 #[case(DeriveOrderStatus::Cancelled)]
7138 #[case(DeriveOrderStatus::Expired)]
7139 #[case(DeriveOrderStatus::Rejected)]
7140 fn test_terminal_delivery_failure_preserves_identity(#[case] status: DeriveOrderStatus) {
7141 let clock = get_atomic_clock_realtime();
7142 let state = WsDispatchState::new(42);
7143 let client_order_id = ClientOrderId::from("TERMINAL-DELIVERY-1");
7144
7145 let identity = OrderIdentity {
7146 instrument_id: InstrumentId::from("ETH-PERP.DERIVE"),
7147 strategy_id: StrategyId::from("S-1"),
7148 order_side: OrderSide::Buy,
7149 order_type: OrderType::Limit,
7150 };
7151
7152 state.register_identity(client_order_id, identity);
7153 let mut order: DeriveOrder = serde_json::from_str(include_str!(
7154 "../test_data/perps/http_order_eth_partially_filled.json"
7155 ))
7156 .unwrap();
7157 order.label = client_order_id.as_str().into();
7158 order.order_status = status;
7159 let (emitter, rx) = test_emitter(clock);
7160 drop(rx);
7161 dispatch_orders_payload(
7162 DeriveOrdersSubscriptionData {
7163 orders: vec![order],
7164 },
7165 &emitter,
7166 AccountId::from("DERIVE-001"),
7167 clock,
7168 &state,
7169 );
7170
7171 assert_eq!(state.identity(&client_order_id), Some(identity));
7172 assert!(!state.contains_accepted(&client_order_id));
7173 assert!(!state.contains_canceled(&client_order_id));
7174 }
7175
7176 #[rstest]
7177 #[case::stop(0)]
7178 #[case::reset(1)]
7179 #[case::dispose(2)]
7180 #[tokio::test]
7181 async fn test_lifecycle_shutdown_closes_owned_generations(#[case] operation: u8) {
7182 let mut client = DeriveExecutionClient::new(test_core(), test_config()).unwrap();
7183 client.core.set_connected();
7184 client.is_connected.store(true, Ordering::Release);
7185 let session_spawner = client.session_tasks.spawner().unwrap();
7186 let pending_spawner = client.pending_tasks.spawner().unwrap();
7187 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
7188
7189 for group in [&client.session_tasks, &client.pending_tasks] {
7190 let cancellation = group.cancellation_token();
7191 let tx = tx.clone();
7192 group
7193 .spawn(async move {
7194 tx.send(()).unwrap();
7195 cancellation.cancelled().await;
7196 })
7197 .unwrap();
7198 }
7199
7200 for _ in 0..2 {
7201 tokio::time::timeout(Duration::from_secs(1), rx.recv())
7202 .await
7203 .unwrap()
7204 .unwrap();
7205 }
7206
7207 for _ in 0..2 {
7208 match operation {
7209 0 => client.stop().unwrap(),
7210 1 => client.reset().unwrap(),
7211 2 => client.dispose().unwrap(),
7212 _ => unreachable!(),
7213 }
7214 }
7215
7216 let closed = (
7217 !client.session_tasks.is_open(),
7218 !client.pending_tasks.is_open(),
7219 client.cancellation_token.is_cancelled(),
7220 client.core.is_disconnected(),
7221 !client.is_connected(),
7222 );
7223
7224 let stale_session_rejected = session_spawner.spawn(async {}).is_err();
7225
7226 let stale_pending_rejected = pending_spawner.spawn(async {}).is_err();
7227 client.disconnect().await.unwrap();
7228
7229 assert_eq!(closed, (true, true, true, true, true));
7230 assert!(stale_session_rejected);
7231 assert!(stale_pending_rejected);
7232 assert!(client.session_tasks.is_empty());
7233 assert!(client.pending_tasks.is_empty());
7234 }
7235
7236 #[rstest]
7237 #[case(Some(ClientId::from(DERIVE)), "DERIVE-001", false, true)]
7238 #[case(Some(ClientId::from("OTHER")), "DERIVE-001", false, false)]
7239 #[case(None, "DERIVE-001", false, false)]
7240 #[case(Some(ClientId::from(DERIVE)), "DERIVE-OTHER", false, false)]
7241 #[case(Some(ClientId::from(DERIVE)), "DERIVE-001", true, false)]
7242 fn test_restores_only_owned_active_orders(
7243 #[case] assigned_client: Option<ClientId>,
7244 #[case] account: &str,
7245 #[case] closed: bool,
7246 #[case] restored: bool,
7247 #[values(false, true)] voided: bool,
7248 #[values(false, true)] register_external: bool,
7249 #[values(false, true)] replacement: bool,
7250 ) {
7251 use nautilus_common::live::runner::replace_exec_event_sender;
7252
7253 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
7254 replace_exec_event_sender(tx);
7255 let cache = Rc::new(RefCell::new(Cache::default()));
7256
7257 let core = ExecutionClientCore::new(
7258 TraderId::from("TRADER-001"),
7259 ClientId::from(DERIVE),
7260 *DERIVE_VENUE,
7261 OmsType::Netting,
7262 AccountId::from("DERIVE-001"),
7263 AccountType::Margin,
7264 None,
7265 cache.clone(),
7266 );
7267 let cid = ClientOrderId::from("RESTORED-1");
7268 let instrument_id = InstrumentId::from("ETH-PERP.DERIVE");
7269 let account_id = AccountId::from(account);
7270 let mut order = OrderTestBuilder::new(OrderType::Limit)
7271 .trader_id(core.trader_id)
7272 .strategy_id(StrategyId::from("RESTORED-STRATEGY"))
7273 .instrument_id(instrument_id)
7274 .client_order_id(cid)
7275 .side(OrderSide::Buy)
7276 .quantity(Quantity::from("1.500"))
7277 .price(Price::from("3499.00"))
7278 .build();
7279 let mut trade: DeriveTrade = serde_json::from_str(include_str!(
7280 "../test_data/perps/http_private_trade_eth.json"
7281 ))
7282 .unwrap();
7283 trade.subaccount_id = TEST_SUBACCOUNT as i64;
7284 trade.label = cid.as_str().into();
7285
7286 trade.trade_amount = if closed { dec!(1.5) } else { dec!(0.3) };
7287 let report = parse_derive_trade_to_fill_report_with_precision(
7288 &trade,
7289 account_id,
7290 Currency::USDC(),
7291 Some(2),
7292 Some(3),
7293 UnixNanos::from(3),
7294 )
7295 .unwrap()
7296 .unwrap();
7297 order
7298 .apply(OrderEventAny::Accepted(OrderAccepted::new(
7299 order.trader_id(),
7300 order.strategy_id(),
7301 instrument_id,
7302 cid,
7303 report.venue_order_id,
7304 account_id,
7305 UUID4::new(),
7306 UnixNanos::from(1),
7307 UnixNanos::from(2),
7308 false,
7309 )))
7310 .unwrap();
7311 order
7312 .apply(OrderEventAny::Filled(OrderFilled::new(
7313 order.trader_id(),
7314 order.strategy_id(),
7315 instrument_id,
7316 cid,
7317 report.venue_order_id,
7318 account_id,
7319 report.trade_id,
7320 order.order_side(),
7321 order.order_type(),
7322 report.last_qty,
7323 report.last_px,
7324 Currency::USDC(),
7325 report.liquidity_side,
7326 UUID4::new(),
7327 report.ts_event,
7328 report.ts_init,
7329 false,
7330 None,
7331 Some(report.commission),
7332 None,
7333 )))
7334 .unwrap();
7335
7336 if voided {
7337 order
7338 .apply(OrderEventAny::FillVoided(OrderFillVoided::new(
7339 order.trader_id(),
7340 order.strategy_id(),
7341 instrument_id,
7342 cid,
7343 report.venue_order_id,
7344 account_id,
7345 Ustr::from("restore-correction"),
7346 report.trade_id,
7347 report.last_qty,
7348 Some(report.commission),
7349 order.order_side(),
7350 order.order_type(),
7351 report.last_px,
7352 Currency::USDC(),
7353 report.liquidity_side,
7354 None,
7355 None,
7356 None,
7357 UUID4::new(),
7358 UnixNanos::from(4),
7359 UnixNanos::from(5),
7360 false,
7361 false,
7362 )))
7363 .unwrap();
7364 assert!(order.trade_ids().is_empty());
7365 }
7366
7367 let current = if replacement && !closed {
7368 let current = VenueOrderId::from("restored-current-child");
7369 order
7370 .apply(OrderEventAny::Updated(OrderUpdated::new(
7371 order.trader_id(),
7372 order.strategy_id(),
7373 instrument_id,
7374 cid,
7375 order.quantity(),
7376 UUID4::new(),
7377 UnixNanos::from(6),
7378 UnixNanos::from(7),
7379 false,
7380 Some(current),
7381 Some(account_id),
7382 order.price(),
7383 None,
7384 None,
7385 false,
7386 )))
7387 .unwrap();
7388 current
7389 } else {
7390 report.venue_order_id
7391 };
7392
7393 let identity = OrderIdentity {
7394 instrument_id,
7395 strategy_id: order.strategy_id(),
7396 order_side: order.order_side(),
7397 order_type: order.order_type(),
7398 };
7399
7400 let mut client = DeriveExecutionClient::new(core, test_config()).unwrap();
7401 if register_external {
7402 client.start().unwrap();
7403 }
7404
7405 cache
7406 .borrow_mut()
7407 .add_order(order, None, assigned_client, false)
7408 .unwrap();
7409
7410 if register_external {
7411 client.register_external_order(
7412 cid,
7413 current,
7414 instrument_id,
7415 identity.strategy_id,
7416 UnixNanos::from(8),
7417 );
7418 } else {
7419 client.start().unwrap();
7420 }
7421
7422 assert_eq!(
7423 client.dispatch_state.identity(&cid),
7424 restored.then_some(identity)
7425 );
7426 assert_eq!(
7427 client.dispatch_state.bound_venue_order_id(&cid),
7428 restored.then_some(current)
7429 );
7430 let expected_legs = restored
7431 .then_some(report.venue_order_id)
7432 .into_iter()
7433 .chain((restored && replacement).then_some(current))
7434 .collect::<AHashSet<_>>();
7435 assert_eq!(
7436 client
7437 .dispatch_state
7438 .venue_order_legs(&cid)
7439 .into_iter()
7440 .collect::<AHashSet<_>>(),
7441 expected_legs,
7442 );
7443 assert_eq!(client.dispatch_state.contains_accepted(&cid), restored);
7444 assert_eq!(
7445 client.dispatch_state.contains_trade(&report.trade_id),
7446 restored
7447 );
7448 assert!(rx.try_recv().is_err());
7449
7450 if restored {
7451 dispatch_trades_payload(
7452 DeriveTradesSubscriptionData {
7453 trades: vec![trade.clone()],
7454 },
7455 &client.emitter,
7456 account_id,
7457 client.clock,
7458 &client.dispatch_state,
7459 );
7460
7461 assert!(rx.try_recv().is_err());
7462 let current_venue_order_id = VenueOrderId::from("restored-current-child");
7463 client
7464 .dispatch_state
7465 .record_venue_order_id(cid, current_venue_order_id);
7466 client.start().unwrap();
7467 assert_eq!(
7468 client.dispatch_state.bound_venue_order_id(&cid),
7469 Some(current_venue_order_id)
7470 );
7471 trade.order_id = current_venue_order_id.as_str().into();
7472 trade.trade_id = "restored-final-fill".into();
7473 trade.trade_amount = dec!(1.2);
7474 dispatch_trades_payload(
7475 DeriveTradesSubscriptionData {
7476 trades: vec![trade],
7477 },
7478 &client.emitter,
7479 account_id,
7480 client.clock,
7481 &client.dispatch_state,
7482 );
7483
7484 let ExecutionEvent::Order(OrderEventAny::Filled(fill)) = rx.try_recv().unwrap() else {
7485 panic!("Expected only the unapplied fill");
7486 };
7487
7488 assert_eq!(fill.client_order_id, cid);
7489 assert_eq!(fill.instrument_id, instrument_id);
7490 assert_eq!(fill.strategy_id, identity.strategy_id);
7491 assert_eq!(fill.venue_order_id, current_venue_order_id);
7492 assert_eq!(fill.last_qty, Quantity::from("1.200"));
7493 assert_eq!(fill.last_px, Price::from("3499.00"));
7494 assert!(rx.try_recv().is_err());
7495 assert_eq!(client.dispatch_state.identity(&cid), None);
7496 assert!(client.dispatch_state.is_terminal(&cid, instrument_id));
7497 }
7498 }
7499
7500 #[rstest]
7501 #[case(false)]
7502 #[case(true)]
7503 fn test_authoritative_sender_installed_before_start_and_reinstalled(#[case] restart: bool) {
7504 use nautilus_common::live::runner::replace_exec_event_sender;
7505
7506 let (first_tx, mut first_rx) = tokio::sync::mpsc::unbounded_channel();
7507 replace_exec_event_sender(first_tx);
7508 let mut client = DeriveExecutionClient::new(test_core(), test_config()).unwrap();
7509 let (second_tx, mut second_rx) = tokio::sync::mpsc::unbounded_channel();
7510
7511 if restart {
7512 client.start().unwrap();
7513 replace_exec_event_sender(second_tx);
7514 client.start().unwrap();
7515 }
7516
7517 let trade: DeriveTrade = serde_json::from_str(include_str!(
7518 "../test_data/perps/http_private_trade_eth.json"
7519 ))
7520 .unwrap();
7521 let report = parse_derive_trade_to_fill_report_with_precision(
7522 &trade,
7523 client.account_id(),
7524 Currency::USDC(),
7525 None,
7526 None,
7527 UnixNanos::from(23),
7528 )
7529 .unwrap()
7530 .unwrap();
7531 client
7532 .emitter
7533 .try_send_execution_report(ExecutionReport::Fill(Box::new(report.clone())))
7534 .unwrap();
7535
7536 let event = if restart {
7537 second_rx.try_recv().unwrap()
7538 } else {
7539 first_rx.try_recv().unwrap()
7540 };
7541
7542 let ExecutionEvent::Report(ExecutionReport::Fill(actual)) = event else {
7543 panic!("authoritative sender must receive the report");
7544 };
7545
7546 assert_eq!(*actual, report);
7547 assert!(first_rx.try_recv().is_err());
7548 assert!(second_rx.try_recv().is_err());
7549 }
7550
7551 #[rstest]
7552 fn test_acceptance_delivery_failure_preserves_replay() {
7553 let clock = get_atomic_clock_realtime();
7554 let state = WsDispatchState::new(42);
7555 let client_order_id = ClientOrderId::from("DELIVERY-1");
7556 let venue_order_id = VenueOrderId::from("venue-delivery-1");
7557 let account_id = AccountId::from("DERIVE-001");
7558
7559 let identity = OrderIdentity {
7560 instrument_id: InstrumentId::from("ETH-PERP.DERIVE"),
7561 strategy_id: StrategyId::from("S-1"),
7562 order_side: OrderSide::Buy,
7563 order_type: OrderType::Limit,
7564 };
7565
7566 state.register_identity(client_order_id, identity);
7567 let (emitter, rx) = test_emitter(clock);
7568 drop(rx);
7569 ensure_accepted_emitted(
7570 &emitter,
7571 &state,
7572 client_order_id,
7573 identity,
7574 venue_order_id,
7575 account_id,
7576 UnixNanos::from(11),
7577 UnixNanos::from(17),
7578 );
7579 assert!(!state.contains_accepted(&client_order_id));
7580 let (emitter, mut rx) = test_emitter(clock);
7581 ensure_accepted_emitted(
7582 &emitter,
7583 &state,
7584 client_order_id,
7585 identity,
7586 venue_order_id,
7587 account_id,
7588 UnixNanos::from(11),
7589 UnixNanos::from(17),
7590 );
7591
7592 let ExecutionEvent::Order(OrderEventAny::Accepted(event)) = rx.try_recv().unwrap() else {
7593 panic!("acceptance must remain deliverable");
7594 };
7595
7596 assert_eq!(event.trader_id, TraderId::from("TRADER-001"));
7597 assert_eq!(event.strategy_id, identity.strategy_id);
7598 assert_eq!(event.instrument_id, identity.instrument_id);
7599 assert_eq!(event.client_order_id, client_order_id);
7600 assert_eq!(event.venue_order_id, venue_order_id);
7601 assert_eq!(event.account_id, account_id);
7602 assert_eq!(event.ts_event, UnixNanos::from(11));
7603 assert_eq!(event.ts_init, UnixNanos::from(17));
7604 assert!(!event.reconciliation);
7605 assert!(state.contains_accepted(&client_order_id));
7606 assert_eq!(state.identity(&client_order_id), Some(identity));
7607 assert!(rx.try_recv().is_err());
7608 }
7609
7610 #[rstest]
7611 #[case("valid")]
7612 #[case("foreign_account")]
7613 #[case("foreign_instrument")]
7614 #[case("wrong_side")]
7615 #[case("unknown_child")]
7616 #[case("old_leg")]
7617 #[case("terminal")]
7618 fn test_trigger_activation_requires_current_native_ownership(
7619 #[case] scope: &str,
7620 #[values(false, true)] fill: bool,
7621 ) {
7622 let clock = get_atomic_clock_realtime();
7623 let state = WsDispatchState::new(42);
7624 let cid = ClientOrderId::from("ACTIVATION-SCOPE");
7625 let native_id = VenueOrderId::from("activation-current");
7626
7627 let identity = OrderIdentity {
7628 instrument_id: InstrumentId::from("ETH-PERP.DERIVE"),
7629 strategy_id: StrategyId::from("S-1"),
7630 order_side: OrderSide::Buy,
7631 order_type: OrderType::StopLimit,
7632 };
7633
7634 state.register_identity(cid, identity);
7635 state.record_venue_order_id(cid, native_id);
7636 state.record_order_shape(cid, Quantity::from("2.000"), None);
7637 if scope == "old_leg" {
7638 state.record_venue_leg(cid, VenueOrderId::from("activation-old"));
7639 }
7640
7641 if scope == "terminal" {
7642 state.forget(&cid);
7643 }
7644
7645 let (emitter, rx) = test_emitter(clock);
7646 drop(rx);
7647
7648 if fill {
7649 let mut trade: DeriveTrade = serde_json::from_str(include_str!(
7650 "../test_data/perps/http_private_trade_eth.json"
7651 ))
7652 .unwrap();
7653 trade.label = cid.to_string().into();
7654 trade.order_id = native_id.to_string();
7655 match scope {
7656 "foreign_account" => trade.subaccount_id = 43,
7657 "foreign_instrument" => trade.instrument_name = "BTC-PERP".into(),
7658 "wrong_side" => trade.direction = DeriveOrderSide::Sell,
7659 "unknown_child" => trade.order_id = "activation-unknown".into(),
7660 "old_leg" => trade.order_id = "activation-old".into(),
7661 _ => {}
7662 }
7663
7664 dispatch_trades_payload(
7665 DeriveTradesSubscriptionData {
7666 trades: vec![trade],
7667 },
7668 &emitter,
7669 AccountId::from("DERIVE-001"),
7670 clock,
7671 &state,
7672 );
7673 } else {
7674 let mut order: DeriveOrder = serde_json::from_str(include_str!(
7675 "../test_data/perps/http_order_eth_partially_filled.json"
7676 ))
7677 .unwrap();
7678 order.label = cid.to_string().into();
7679 order.order_id = native_id.to_string();
7680 order.order_type = crate::common::enums::DeriveOrderType::Limit;
7681 order.order_status = DeriveOrderStatus::Open;
7682 order.trigger_type = Some(crate::common::enums::DeriveTriggerType::Stoploss);
7683 match scope {
7684 "foreign_account" => order.subaccount_id = 43,
7685 "foreign_instrument" => order.instrument_name = "BTC-PERP".into(),
7686 "wrong_side" => order.direction = DeriveOrderSide::Sell,
7687 "unknown_child" => order.order_id = "activation-unknown".into(),
7688 "old_leg" => order.order_id = "activation-old".into(),
7689 _ => {}
7690 }
7691
7692 dispatch_orders_payload(
7693 DeriveOrdersSubscriptionData {
7694 orders: vec![order],
7695 },
7696 &emitter,
7697 AccountId::from("DERIVE-001"),
7698 clock,
7699 &state,
7700 );
7701 }
7702
7703 assert_eq!(state.trigger_active(&cid), scope == "valid");
7704 assert_eq!(
7705 state.bound_venue_order_id(&cid),
7706 (scope != "terminal").then_some(native_id)
7707 );
7708 state.forget(&cid);
7709 assert!(!state.trigger_active(&cid));
7710 }
7711
7712 #[rstest]
7713 fn test_completed_tracked_fill_retires_identity_and_preserves_late_fill_report() {
7714 let clock = get_atomic_clock_realtime();
7715 let state = WsDispatchState::new(42);
7716 let client_order_id = ClientOrderId::from("FILL-RETIRE-1");
7717 let account_id = AccountId::from("DERIVE-001");
7718
7719 let identity = OrderIdentity {
7720 instrument_id: InstrumentId::from("ETH-PERP.DERIVE"),
7721 strategy_id: StrategyId::from("S-1"),
7722 order_side: OrderSide::Buy,
7723 order_type: OrderType::Limit,
7724 };
7725
7726 state.register_identity(client_order_id, identity);
7727 state.record_order_shape(client_order_id, Quantity::from("0.500"), None);
7728 let mut trade: DeriveTrade = serde_json::from_str(include_str!(
7729 "../test_data/perps/http_private_trade_eth.json"
7730 ))
7731 .unwrap();
7732 trade.label = client_order_id.as_str().into();
7733 let (emitter, mut rx) = test_emitter(clock);
7734 dispatch_trades_payload(
7735 DeriveTradesSubscriptionData {
7736 trades: vec![trade.clone()],
7737 },
7738 &emitter,
7739 account_id,
7740 clock,
7741 &state,
7742 );
7743
7744 assert!(matches!(
7745 rx.try_recv(),
7746 Ok(ExecutionEvent::Order(OrderEventAny::Accepted(_)))
7747 ));
7748 assert!(matches!(
7749 rx.try_recv(),
7750 Ok(ExecutionEvent::Order(OrderEventAny::Filled(_)))
7751 ));
7752 assert_eq!(state.identity(&client_order_id), None);
7753 assert!(state.is_terminal(&client_order_id, identity.instrument_id));
7754 dispatch_trades_payload(
7755 DeriveTradesSubscriptionData {
7756 trades: vec![trade.clone()],
7757 },
7758 &emitter,
7759 account_id,
7760 clock,
7761 &state,
7762 );
7763
7764 assert!(rx.try_recv().is_err());
7765 trade.trade_id = "late-fill-2".to_string();
7766 trade.trade_amount = dec!(0.2);
7767 dispatch_trades_payload(
7768 DeriveTradesSubscriptionData {
7769 trades: vec![trade],
7770 },
7771 &emitter,
7772 account_id,
7773 clock,
7774 &state,
7775 );
7776
7777 let ExecutionEvent::Report(ExecutionReport::Fill(report)) = rx.try_recv().unwrap() else {
7778 panic!("a distinct late fill must reach reconciliation");
7779 };
7780
7781 assert_eq!(report.account_id, account_id);
7782 assert_eq!(report.instrument_id, identity.instrument_id);
7783 assert_eq!(report.client_order_id, Some(client_order_id));
7784 assert_eq!(report.venue_order_id, VenueOrderId::from("order-abc"));
7785 assert_eq!(report.trade_id.as_str(), "late-fill-2");
7786 assert_eq!(report.order_side, OrderSide::Buy);
7787 assert_eq!(report.last_qty.as_decimal(), dec!(0.2));
7788 assert_eq!(report.last_px.as_decimal(), dec!(3499));
7789 assert_eq!(
7790 report.commission,
7791 Money::from_decimal(dec!(0.02), Currency::USDC()).unwrap()
7792 );
7793 assert_eq!(report.liquidity_side, LiquiditySide::Maker);
7794 assert_eq!(report.ts_event, UnixNanos::from(1_700_000_000_000_000_000));
7795 assert_eq!(report.avg_px, None);
7796 assert_eq!(report.venue_position_id, None);
7797 assert!(rx.try_recv().is_err());
7798 }
7799
7800 #[rstest]
7801 #[case(false, false)]
7802 #[case(false, true)]
7803 #[case(true, false)]
7804 #[case(true, true)]
7805 fn test_foreign_instrument_label_preserves_external_identity(
7806 #[case] terminal: bool,
7807 #[case] fill: bool,
7808 ) {
7809 let clock = get_atomic_clock_realtime();
7810 let state = WsDispatchState::new(42);
7811 let cid = ClientOrderId::from("FOREIGN-LABEL-1");
7812
7813 let identity = OrderIdentity {
7814 instrument_id: InstrumentId::from("ETH-PERP.DERIVE"),
7815 strategy_id: StrategyId::from("S-1"),
7816 order_side: OrderSide::Buy,
7817 order_type: OrderType::Limit,
7818 };
7819
7820 state.register_identity(cid, identity);
7821 if terminal {
7822 state.forget(&cid);
7823 }
7824
7825 let (emitter, mut rx) = test_emitter(clock);
7826 let account_id = AccountId::from("DERIVE-001");
7827 let instrument_id = InstrumentId::from("BTC-PERP.DERIVE");
7828
7829 if fill {
7830 let mut trade: DeriveTrade = serde_json::from_str(include_str!(
7831 "../test_data/perps/http_private_trade_eth.json"
7832 ))
7833 .unwrap();
7834 trade.label = cid.as_str().into();
7835 trade.instrument_name = "BTC-PERP".into();
7836 dispatch_trades_payload(
7837 DeriveTradesSubscriptionData {
7838 trades: vec![trade],
7839 },
7840 &emitter,
7841 account_id,
7842 clock,
7843 &state,
7844 );
7845
7846 let ExecutionEvent::Report(ExecutionReport::Fill(report)) = rx.try_recv().unwrap()
7847 else {
7848 panic!("Expected external fill report");
7849 };
7850
7851 assert_eq!(report.client_order_id, None);
7852 assert_eq!(report.instrument_id, instrument_id);
7853 assert_eq!(report.account_id, account_id);
7854 assert_eq!(report.venue_order_id, VenueOrderId::from("order-abc"));
7855 assert_eq!(report.trade_id.as_str(), "trade-xyz");
7856 assert_eq!(report.last_qty.as_decimal(), dec!(0.5));
7857 assert_eq!(report.last_px.as_decimal(), dec!(3499));
7858 assert_eq!(report.commission.as_decimal(), dec!(0.02));
7859 } else {
7860 let mut order: DeriveOrder = serde_json::from_str(include_str!(
7861 "../test_data/perps/http_order_eth_partially_filled.json"
7862 ))
7863 .unwrap();
7864 order.label = cid.as_str().into();
7865 order.instrument_name = "BTC-PERP".into();
7866 let venue_order_id = VenueOrderId::from(order.order_id.as_str());
7867 dispatch_orders_payload(
7868 DeriveOrdersSubscriptionData {
7869 orders: vec![order],
7870 },
7871 &emitter,
7872 account_id,
7873 clock,
7874 &state,
7875 );
7876
7877 let ExecutionEvent::Report(ExecutionReport::Order(report)) = rx.try_recv().unwrap()
7878 else {
7879 panic!("Expected external order report");
7880 };
7881
7882 assert_eq!(report.client_order_id, None);
7883 assert_eq!(report.instrument_id, instrument_id);
7884 assert_eq!(report.account_id, account_id);
7885 assert_eq!(report.venue_order_id, venue_order_id);
7886 }
7887
7888 assert_eq!(state.identity(&cid), (!terminal).then_some(identity));
7889 assert_eq!(state.is_terminal(&cid, identity.instrument_id), terminal);
7890 assert!(rx.try_recv().is_err());
7891 }
7892
7893 #[rstest]
7894 fn test_order_result_retired_identity_preserves_distinct_trades() {
7895 let clock = get_atomic_clock_realtime();
7896 let state = WsDispatchState::new(42);
7897 let account_id = AccountId::from("DERIVE-001");
7898 let client_order_id = ClientOrderId::from("STRAT-RETIRED-RPC");
7899 let instrument_id = InstrumentId::from("ETH-PERP.DERIVE");
7900 state.register_identity(
7901 client_order_id,
7902 OrderIdentity {
7903 instrument_id,
7904 strategy_id: StrategyId::from("S-1"),
7905 order_side: OrderSide::Buy,
7906 order_type: OrderType::Limit,
7907 },
7908 );
7909
7910 state.forget(&client_order_id);
7911 let mut order: DeriveOrder = serde_json::from_str(include_str!(
7912 "../test_data/perps/http_order_eth_partially_filled.json"
7913 ))
7914 .unwrap();
7915 order.label = client_order_id.as_str().into();
7916 let mut trade: DeriveTrade = serde_json::from_str(include_str!(
7917 "../test_data/perps/http_private_trade_eth.json"
7918 ))
7919 .unwrap();
7920 trade.label = client_order_id.as_str().into();
7921 trade.trade_id = "rpc-distinct-late-fill".to_string();
7922 trade.trade_amount = dec!(0.23);
7923
7924 let result = DeriveOrderResult {
7925 order,
7926 trades: vec![trade],
7927 };
7928
7929 let (emitter, mut rx) = test_emitter(clock);
7930
7931 dispatch_order_result(
7932 result.clone(),
7933 client_order_id,
7934 &emitter,
7935 account_id,
7936 clock,
7937 &state,
7938 );
7939 dispatch_order_result(result, client_order_id, &emitter, account_id, clock, &state);
7940
7941 let ExecutionEvent::Report(ExecutionReport::Fill(report)) = rx.try_recv().unwrap() else {
7942 panic!("a distinct late RPC fill must reach reconciliation");
7943 };
7944
7945 assert_eq!(report.account_id, account_id);
7946 assert_eq!(report.instrument_id, instrument_id);
7947 assert_eq!(report.client_order_id, Some(client_order_id));
7948 assert_eq!(report.venue_order_id, VenueOrderId::from("order-abc"));
7949 assert_eq!(report.trade_id.as_str(), "rpc-distinct-late-fill");
7950 assert_eq!(report.order_side, OrderSide::Buy);
7951 assert_eq!(report.last_qty.as_decimal(), dec!(0.23));
7952 assert_eq!(report.last_px.as_decimal(), dec!(3499));
7953 assert_eq!(
7954 report.commission,
7955 Money::from_decimal(dec!(0.02), Currency::USDC()).unwrap()
7956 );
7957 assert_eq!(report.liquidity_side, LiquiditySide::Maker);
7958 assert_eq!(report.ts_event, UnixNanos::from(1_700_000_000_000_000_000));
7959 assert_eq!(report.avg_px, None);
7960 assert_eq!(report.venue_position_id, None);
7961 assert_eq!(state.identity(&client_order_id), None);
7962 assert!(state.is_terminal(&client_order_id, instrument_id));
7963 assert!(rx.try_recv().is_err());
7964 }
7965
7966 #[rstest]
7967 #[case("orders")]
7968 #[case("trades")]
7969 #[case("rpc")]
7970 fn test_private_rows_require_native_account_identity(#[case] source: &str) {
7971 let clock = get_atomic_clock_realtime();
7972 let state = WsDispatchState::new(42);
7973 let account_id = AccountId::from("DERIVE-001");
7974 let client_order_id = ClientOrderId::from("STRAT-O-1");
7975
7976 let identity = OrderIdentity {
7977 instrument_id: InstrumentId::from("ETH-PERP.DERIVE"),
7978 strategy_id: StrategyId::from("S-1"),
7979 order_side: OrderSide::Buy,
7980 order_type: OrderType::Limit,
7981 };
7982
7983 state.register_identity(client_order_id, identity);
7984 let order: DeriveOrder = serde_json::from_str(include_str!(
7985 "../test_data/perps/http_order_eth_partially_filled.json"
7986 ))
7987 .unwrap();
7988 let trade: DeriveTrade = serde_json::from_str(include_str!(
7989 "../test_data/perps/http_private_trade_eth.json"
7990 ))
7991 .unwrap();
7992 let trade_id = TradeId::new(&trade.trade_id);
7993 let mut foreign_order = order.clone();
7994 foreign_order.subaccount_id = 43;
7995 let mut foreign_trade = trade.clone();
7996 foreign_trade.subaccount_id = 43;
7997 let (emitter, mut rx) = test_emitter(clock);
7998
7999 match source {
8000 "orders" => dispatch_orders_payload(
8001 DeriveOrdersSubscriptionData {
8002 orders: vec![foreign_order],
8003 },
8004 &emitter,
8005 account_id,
8006 clock,
8007 &state,
8008 ),
8009 "trades" => dispatch_trades_payload(
8010 DeriveTradesSubscriptionData {
8011 trades: vec![foreign_trade],
8012 },
8013 &emitter,
8014 account_id,
8015 clock,
8016 &state,
8017 ),
8018 "rpc" => dispatch_order_result(
8019 DeriveOrderResult {
8020 order: foreign_order,
8021 trades: vec![foreign_trade],
8022 },
8023 client_order_id,
8024 &emitter,
8025 account_id,
8026 clock,
8027 &state,
8028 ),
8029 _ => unreachable!(),
8030 }
8031
8032 assert!(
8033 rx.try_recv().is_err(),
8034 "foreign-account evidence must emit nothing"
8035 );
8036 assert_eq!(state.bound_venue_order_id(&client_order_id), None);
8037 assert_eq!(state.identity(&client_order_id), Some(identity));
8038 assert!(!state.contains_trade(&trade_id));
8039
8040 match source {
8041 "orders" => dispatch_orders_payload(
8042 DeriveOrdersSubscriptionData {
8043 orders: vec![order],
8044 },
8045 &emitter,
8046 account_id,
8047 clock,
8048 &state,
8049 ),
8050 "trades" => dispatch_trades_payload(
8051 DeriveTradesSubscriptionData {
8052 trades: vec![trade],
8053 },
8054 &emitter,
8055 account_id,
8056 clock,
8057 &state,
8058 ),
8059 "rpc" => dispatch_order_result(
8060 DeriveOrderResult {
8061 order,
8062 trades: vec![trade],
8063 },
8064 client_order_id,
8065 &emitter,
8066 account_id,
8067 clock,
8068 &state,
8069 ),
8070 _ => unreachable!(),
8071 }
8072
8073 assert!(rx.try_recv().is_ok(), "matching native evidence must emit");
8074 }
8075
8076 #[rstest]
8077 #[case("orders")]
8078 #[case("trades")]
8079 fn test_private_channel_requires_native_account_identity(#[case] channel: &str) {
8080 let clock = get_atomic_clock_realtime();
8081 let state = WsDispatchState::new(42);
8082
8083 let data = if channel == "orders" {
8084 serde_json::json!([serde_json::from_str::<serde_json::Value>(include_str!(
8085 "../test_data/perps/http_order_eth_partially_filled.json"
8086 ))
8087 .unwrap()])
8088 } else {
8089 serde_json::json!([serde_json::from_str::<serde_json::Value>(include_str!(
8090 "../test_data/perps/http_private_trade_eth.json"
8091 ))
8092 .unwrap()])
8093 };
8094
8095 let payload = WsSubscriptionPayload {
8096 channel: format!("43.{channel}").into(),
8097 data: serde_json::value::to_raw_value(&data).unwrap(),
8098 };
8099
8100 let (emitter, mut rx) = test_emitter(clock);
8101 handle_ws_message(
8102 DeriveWsMessage::Subscription(payload.clone()),
8103 &emitter,
8104 AccountId::from("DERIVE-001"),
8105 clock,
8106 &state,
8107 );
8108 assert!(
8109 rx.try_recv().is_err(),
8110 "foreign channel must emit nothing even with own rows"
8111 );
8112
8113 let payload = WsSubscriptionPayload {
8114 channel: format!("42.{channel}").into(),
8115 ..payload
8116 };
8117
8118 handle_ws_message(
8119 DeriveWsMessage::Subscription(payload),
8120 &emitter,
8121 AccountId::from("DERIVE-001"),
8122 clock,
8123 &state,
8124 );
8125 assert!(rx.try_recv().is_ok(), "matching channel must emit");
8126 }
8127
8128 #[rstest]
8129 #[case(None)]
8130 #[case(Some("unrelated-leg"))]
8131 #[case(Some("original-leg"))]
8132 fn test_replacement_order_requires_native_link_before_binding(
8133 #[case] replaced_id: Option<&str>,
8134 ) {
8135 let clock = get_atomic_clock_realtime();
8136 let state = WsDispatchState::new(42);
8137 let cid = ClientOrderId::from("REPLACE-AUTHORITY");
8138 let instrument_id = InstrumentId::from("ETH-PERP.DERIVE");
8139 let old_id = VenueOrderId::from("original-leg");
8140 let new_id = VenueOrderId::from("replacement-leg");
8141
8142 let identity = OrderIdentity {
8143 instrument_id,
8144 strategy_id: StrategyId::from("S-1"),
8145 order_side: OrderSide::Buy,
8146 order_type: OrderType::Limit,
8147 };
8148
8149 state.register_identity(cid, identity);
8150 state.record_venue_order_id(cid, old_id);
8151 state.mark_accepted(cid);
8152 state.mark_pending_modify(cid, old_id);
8153 state.record_modify_target(cid, Quantity::from("2.000"), Some(Price::from("3505.00")));
8154 let mut order: DeriveOrder = serde_json::from_str(include_str!(
8155 "../test_data/perps/http_order_eth_partially_filled.json"
8156 ))
8157 .unwrap();
8158 order.label = cid.as_str().into();
8159 order.order_id = new_id.to_string();
8160 order.order_status = DeriveOrderStatus::Open;
8161 order.replaced_order_id = replaced_id.map(str::to_owned);
8162 state.record_modify_nonce(cid, order.nonce);
8163 let (emitter, mut rx) = test_emitter(clock);
8164 dispatch_orders_payload(
8165 DeriveOrdersSubscriptionData {
8166 orders: vec![order],
8167 },
8168 &emitter,
8169 AccountId::from("DERIVE-001"),
8170 clock,
8171 &state,
8172 );
8173
8174 if replaced_id == Some(old_id.as_str()) {
8175 let ExecutionEvent::Order(OrderEventAny::Updated(updated)) = rx.try_recv().unwrap()
8176 else {
8177 panic!("expected authoritative update");
8178 };
8179
8180 assert_eq!(updated.client_order_id, cid);
8181 assert_eq!(updated.instrument_id, instrument_id);
8182 assert_eq!(updated.venue_order_id, Some(new_id));
8183 assert_eq!(updated.quantity, Quantity::from("2.000"));
8184 assert_eq!(updated.price, Some(Price::from("3505.00")));
8185 assert_eq!(state.bound_venue_order_id(&cid), Some(new_id));
8186 } else {
8187 assert!(
8188 rx.try_recv().is_err(),
8189 "a label without a matching replaced_order_id is not replacement authority"
8190 );
8191 assert_eq!(state.bound_venue_order_id(&cid), Some(old_id));
8192 assert_eq!(state.pending_modify(&cid), Some(old_id));
8193 assert_eq!(
8194 state.modify_target(&cid),
8195 Some((Quantity::from("2.000"), Some(Price::from("3505.00"))))
8196 );
8197 }
8198
8199 assert!(rx.try_recv().is_err());
8200 }
8201
8202 #[rstest]
8203 #[case(None)]
8204 #[case(Some(101))]
8205 #[case(Some(202))]
8206 fn test_replacement_requires_prepared_request_nonce(#[case] prepared_nonce: Option<u64>) {
8207 let clock = get_atomic_clock_realtime();
8208 let state = WsDispatchState::new(42);
8209 let cid = ClientOrderId::from("REPLACE-NOT-PREPARED");
8210 let instrument_id = InstrumentId::from("ETH-PERP.DERIVE");
8211 let old_id = VenueOrderId::from("original-leg");
8212
8213 let identity = OrderIdentity {
8214 instrument_id,
8215 strategy_id: StrategyId::from("S-1"),
8216 order_side: OrderSide::Buy,
8217 order_type: OrderType::Limit,
8218 };
8219
8220 state.register_identity(cid, identity);
8221 state.record_venue_order_id(cid, old_id);
8222 state.mark_accepted(cid);
8223 state.mark_pending_modify(cid, old_id);
8224 state.record_modify_target(cid, Quantity::from("2.000"), Some(Price::from("3505.00")));
8225 let mut order: DeriveOrder = serde_json::from_str(include_str!(
8226 "../test_data/perps/http_order_eth_partially_filled.json"
8227 ))
8228 .unwrap();
8229 order.label = cid.as_str().into();
8230 order.order_id = "prior-replacement-leg".to_string();
8231 order.nonce = 202;
8232
8233 if let Some(nonce) = prepared_nonce {
8234 state.record_modify_nonce(cid, nonce);
8235 }
8236
8237 order.order_status = DeriveOrderStatus::Open;
8238 order.replaced_order_id = Some(old_id.to_string());
8239 let (emitter, mut rx) = test_emitter(clock);
8240 dispatch_orders_payload(
8241 DeriveOrdersSubscriptionData {
8242 orders: vec![order],
8243 },
8244 &emitter,
8245 AccountId::from("DERIVE-001"),
8246 clock,
8247 &state,
8248 );
8249
8250 if prepared_nonce == Some(202) {
8251 let ExecutionEvent::Order(OrderEventAny::Updated(updated)) = rx.try_recv().unwrap()
8252 else {
8253 panic!("expected matching command update");
8254 };
8255
8256 assert_eq!(updated.client_order_id, cid);
8257 assert_eq!(updated.instrument_id, instrument_id);
8258 assert_eq!(updated.quantity, Quantity::from("2.000"));
8259 assert_eq!(updated.price, Some(Price::from("3505.00")));
8260 assert_eq!(
8261 updated.venue_order_id,
8262 Some(VenueOrderId::from("prior-replacement-leg"))
8263 );
8264 assert_eq!(state.bound_venue_order_id(&cid), updated.venue_order_id);
8265 assert_eq!(state.pending_modify(&cid), None);
8266 assert_eq!(state.modify_target(&cid), None);
8267 assert_eq!(state.modify_nonce(&cid), None);
8268 } else {
8269 assert!(
8270 rx.try_recv().is_err(),
8271 "a linked child cannot claim an unprepared or different request"
8272 );
8273 assert_eq!(state.bound_venue_order_id(&cid), Some(old_id));
8274 assert_eq!(state.pending_modify(&cid), Some(old_id));
8275 assert_eq!(
8276 state.modify_target(&cid),
8277 Some((Quantity::from("2.000"), Some(Price::from("3505.00"))))
8278 );
8279 assert_eq!(state.modify_nonce(&cid), prepared_nonce);
8280 }
8281
8282 assert!(rx.try_recv().is_err());
8283 }
8284
8285 #[rstest]
8286 fn test_replacement_trade_defers_until_native_order_authority() {
8287 let clock = get_atomic_clock_realtime();
8288 let state = WsDispatchState::new(42);
8289 let cid = ClientOrderId::from("REPLACE-DEFERRED-FILL");
8290 let instrument_id = InstrumentId::from("ETH-PERP.DERIVE");
8291 let old_id = VenueOrderId::from("original-leg");
8292 let new_id = VenueOrderId::from("replacement-leg");
8293
8294 let identity = OrderIdentity {
8295 instrument_id,
8296 strategy_id: StrategyId::from("S-1"),
8297 order_side: OrderSide::Buy,
8298 order_type: OrderType::Limit,
8299 };
8300
8301 state.register_identity(cid, identity);
8302 state.record_venue_order_id(cid, old_id);
8303 state.mark_accepted(cid);
8304 state.record_order_shape(cid, Quantity::from("2.000"), None);
8305 state.mark_pending_modify(cid, old_id);
8306 state.record_modify_target(cid, Quantity::from("2.000"), Some(Price::from("3505.00")));
8307 let mut trade: DeriveTrade = serde_json::from_str(include_str!(
8308 "../test_data/perps/http_private_trade_eth.json"
8309 ))
8310 .unwrap();
8311 trade.label = cid.as_str().into();
8312 trade.order_id = new_id.to_string();
8313 trade.trade_id = "deferred-fill".to_owned();
8314 trade.trade_amount = dec!(0.3);
8315 let (emitter, mut rx) = test_emitter(clock);
8316 dispatch_trades_payload(
8317 DeriveTradesSubscriptionData {
8318 trades: vec![trade],
8319 },
8320 &emitter,
8321 AccountId::from("DERIVE-001"),
8322 clock,
8323 &state,
8324 );
8325
8326 assert!(
8327 rx.try_recv().is_err(),
8328 "trade label cannot authorize Updated or Filled"
8329 );
8330 assert_eq!(state.bound_venue_order_id(&cid), Some(old_id));
8331 assert_eq!(state.pending_modify(&cid), Some(old_id));
8332 assert!(!state.contains_trade(&TradeId::from("deferred-fill")));
8333 let mut order: DeriveOrder = serde_json::from_str(include_str!(
8334 "../test_data/perps/http_order_eth_partially_filled.json"
8335 ))
8336 .unwrap();
8337 order.label = cid.as_str().into();
8338 order.order_id = new_id.to_string();
8339 order.order_status = DeriveOrderStatus::Open;
8340 order.replaced_order_id = Some(old_id.to_string());
8341 state.record_modify_nonce(cid, order.nonce);
8342 dispatch_orders_payload(
8343 DeriveOrdersSubscriptionData {
8344 orders: vec![order],
8345 },
8346 &emitter,
8347 AccountId::from("DERIVE-001"),
8348 clock,
8349 &state,
8350 );
8351
8352 let ExecutionEvent::Order(OrderEventAny::Updated(updated)) = rx.try_recv().unwrap() else {
8353 panic!("expected update before fill");
8354 };
8355
8356 let ExecutionEvent::Order(OrderEventAny::Filled(fill)) = rx.try_recv().unwrap() else {
8357 panic!("expected deferred fill");
8358 };
8359
8360 assert_eq!(updated.venue_order_id, Some(new_id));
8361 assert_eq!(fill.client_order_id, cid);
8362 assert_eq!(fill.venue_order_id, new_id);
8363 assert_eq!(fill.trade_id, TradeId::from("deferred-fill"));
8364 assert_eq!(fill.last_qty, Quantity::from("0.300000"));
8365 assert!(rx.try_recv().is_err());
8366 }
8367
8368 #[rstest]
8369 #[case("rpc")]
8370 #[case("stream")]
8371 fn test_undelivered_trade_survives_authoritative_sender_reinstallation(#[case] source: &str) {
8372 use nautilus_common::live::runner::replace_exec_event_sender;
8373 let (first_tx, first_rx) = tokio::sync::mpsc::unbounded_channel();
8374 replace_exec_event_sender(first_tx);
8375 let mut client = DeriveExecutionClient::new(test_core(), test_config()).unwrap();
8376 drop(first_rx);
8377 let cid = ClientOrderId::from("UNDELIVERED-FINANCIAL");
8378 let instrument_id = InstrumentId::from("ETH-PERP.DERIVE");
8379 let venue_order_id = VenueOrderId::from("owned-financial-leg");
8380
8381 let identity = OrderIdentity {
8382 instrument_id,
8383 strategy_id: StrategyId::from("S-1"),
8384 order_side: OrderSide::Buy,
8385 order_type: OrderType::Limit,
8386 };
8387
8388 client.dispatch_state.register_identity(cid, identity);
8389 client
8390 .dispatch_state
8391 .register_instrument_precision(instrument_id, 2, 3);
8392 client
8393 .dispatch_state
8394 .record_order_shape(cid, Quantity::from("2.000"), None);
8395 client
8396 .dispatch_state
8397 .record_venue_order_id(cid, venue_order_id);
8398 client.dispatch_state.mark_accepted(cid);
8399 let mut trade: DeriveTrade = serde_json::from_str(include_str!(
8400 "../test_data/perps/http_private_trade_eth.json"
8401 ))
8402 .unwrap();
8403 trade.subaccount_id = client.subaccount_id() as i64;
8404 trade.label = cid.as_str().into();
8405 trade.order_id = venue_order_id.to_string();
8406 trade.trade_id = "undelivered-financial".to_owned();
8407
8408 if source == "rpc" {
8409 let mut order: DeriveOrder = serde_json::from_str(include_str!(
8410 "../test_data/perps/http_order_eth_partially_filled.json"
8411 ))
8412 .unwrap();
8413 order.subaccount_id = client.subaccount_id() as i64;
8414 order.label = cid.as_str().into();
8415 order.order_id = venue_order_id.to_string();
8416 order.order_status = DeriveOrderStatus::Open;
8417 dispatch_order_result(
8418 DeriveOrderResult {
8419 order,
8420 trades: vec![trade],
8421 },
8422 cid,
8423 &client.emitter,
8424 client.account_id(),
8425 client.clock,
8426 &client.dispatch_state,
8427 );
8428 } else {
8429 dispatch_trades_payload(
8430 DeriveTradesSubscriptionData {
8431 trades: vec![trade],
8432 },
8433 &client.emitter,
8434 client.account_id(),
8435 client.clock,
8436 &client.dispatch_state,
8437 );
8438 }
8439
8440 let (second_tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
8441 replace_exec_event_sender(second_tx);
8442 client.start().unwrap();
8443
8444 let ExecutionEvent::Order(OrderEventAny::Filled(fill)) = rx
8445 .try_recv()
8446 .expect("undelivered raw trade must survive without venue replay")
8447 else {
8448 panic!("expected retained fill");
8449 };
8450
8451 assert_eq!(fill.client_order_id, cid);
8452 assert_eq!(fill.instrument_id, instrument_id);
8453 assert_eq!(fill.strategy_id, identity.strategy_id);
8454 assert_eq!(fill.venue_order_id, venue_order_id);
8455 assert_eq!(fill.trade_id, TradeId::from("undelivered-financial"));
8456 assert_eq!(fill.last_qty, Quantity::from("0.500"));
8457 assert_eq!(fill.last_px, Price::from("3499.00"));
8458 assert_eq!(fill.commission, Some(Money::from("0.02 USDC")));
8459 client.start().unwrap();
8460 assert!(rx.try_recv().is_err());
8461 }
8462
8463 #[rstest]
8464 fn test_first_native_partial_fill_binds_before_delivery(
8465 #[values(false, true)] failed_delivery: bool,
8466 ) {
8467 let clock = get_atomic_clock_realtime();
8468 let state = WsDispatchState::new(42);
8469 let client_order_id = ClientOrderId::from("NATIVE-FIRST-FILL");
8470
8471 let identity = OrderIdentity {
8472 instrument_id: InstrumentId::from("ETH-PERP.DERIVE"),
8473 strategy_id: StrategyId::from("S-1"),
8474 order_side: OrderSide::Buy,
8475 order_type: OrderType::Limit,
8476 };
8477
8478 state.register_identity(client_order_id, identity);
8479 state.record_order_shape(
8480 client_order_id,
8481 Quantity::from("1.0"),
8482 Some(Price::from("3500")),
8483 );
8484 let mut trade: DeriveTrade = serde_json::from_str(include_str!(
8485 "../test_data/perps/http_private_trade_eth.json"
8486 ))
8487 .unwrap();
8488 trade.label = client_order_id.to_string().into();
8489 let venue_order_id = VenueOrderId::from(trade.order_id.as_str());
8490 let trade_id = TradeId::from(trade.trade_id.as_str());
8491 let account_id = AccountId::from("DERIVE-001");
8492 let (emitter, rx) = test_emitter(clock);
8493 let mut rx = if failed_delivery {
8494 drop(rx);
8495 None
8496 } else {
8497 Some(rx)
8498 };
8499
8500 dispatch_trades_payload(
8501 DeriveTradesSubscriptionData {
8502 trades: vec![trade],
8503 },
8504 &emitter,
8505 account_id,
8506 clock,
8507 &state,
8508 );
8509
8510 assert_eq!(
8511 state.bound_venue_order_id(&client_order_id),
8512 Some(venue_order_id)
8513 );
8514 assert_eq!(state.identity(&client_order_id), Some(identity));
8515 assert_eq!(state.contains_trade(&trade_id), !failed_delivery);
8516 assert_eq!(state.contains_accepted(&client_order_id), !failed_delivery);
8517
8518 if let Some(rx) = rx.as_mut() {
8519 let ExecutionEvent::Order(OrderEventAny::Accepted(accepted)) = rx.try_recv().unwrap()
8520 else {
8521 panic!("Expected acceptance before fill");
8522 };
8523
8524 let ExecutionEvent::Order(OrderEventAny::Filled(fill)) = rx.try_recv().unwrap() else {
8525 panic!("Expected native partial fill");
8526 };
8527
8528 assert_eq!(accepted.trader_id, TraderId::from("TRADER-001"));
8529 assert_eq!(accepted.strategy_id, identity.strategy_id);
8530 assert_eq!(accepted.instrument_id, identity.instrument_id);
8531 assert_eq!(accepted.client_order_id, client_order_id);
8532 assert_eq!(accepted.venue_order_id, venue_order_id);
8533 assert_eq!(accepted.account_id, account_id);
8534 assert_eq!(fill.trader_id, TraderId::from("TRADER-001"));
8535 assert_eq!(fill.strategy_id, identity.strategy_id);
8536 assert_eq!(fill.instrument_id, identity.instrument_id);
8537 assert_eq!(fill.client_order_id, client_order_id);
8538 assert_eq!(fill.venue_order_id, venue_order_id);
8539 assert_eq!(fill.account_id, account_id);
8540 assert_eq!(fill.trade_id, trade_id);
8541 assert_eq!(fill.order_side, OrderSide::Buy);
8542 assert_eq!(fill.order_type, OrderType::Limit);
8543 assert_eq!(fill.last_qty.as_decimal(), dec!(0.5));
8544 assert_eq!(fill.last_px.as_decimal(), dec!(3499));
8545 assert_eq!(fill.currency, Currency::USDC());
8546 assert_eq!(
8547 fill.commission,
8548 Some(Money::from_decimal(dec!(0.02), Currency::USDC()).unwrap())
8549 );
8550 assert_eq!(fill.liquidity_side, LiquiditySide::Maker);
8551 assert_eq!(fill.ts_event, UnixNanos::from(1_700_000_000_000_000_000));
8552 assert_eq!(fill.position_id, None);
8553 assert!(rx.try_recv().is_err());
8554 }
8555 }
8556
8557 #[rstest]
8558 fn test_native_fill_side_conflict_preserves_authority(
8559 #[values(false, true)] failed_delivery: bool,
8560 #[values(false, true)] bound: bool,
8561 #[values(OrderSide::Buy, OrderSide::Sell)] side: OrderSide,
8562 ) {
8563 let clock = get_atomic_clock_realtime();
8564 let state = WsDispatchState::new(42);
8565 let client_order_id = ClientOrderId::from("NATIVE-SIDE-CONFLICT");
8566
8567 let identity = OrderIdentity {
8568 instrument_id: InstrumentId::from("ETH-PERP.DERIVE"),
8569 strategy_id: StrategyId::from("S-1"),
8570 order_side: side,
8571 order_type: OrderType::Limit,
8572 };
8573
8574 let venue_order_id = VenueOrderId::from("native-owned");
8575 state.register_identity(client_order_id, identity);
8576 if bound {
8577 state.record_venue_order_id(client_order_id, venue_order_id);
8578 }
8579
8580 let mut trade: DeriveTrade = serde_json::from_str(include_str!(
8581 "../test_data/perps/http_private_trade_eth.json"
8582 ))
8583 .unwrap();
8584 trade.label = client_order_id.to_string().into();
8585 trade.order_id = "native-contradictory".into();
8586
8587 trade.direction = match side {
8588 OrderSide::Buy => DeriveOrderSide::Sell,
8589 OrderSide::Sell => DeriveOrderSide::Buy,
8590 };
8591
8592 let trade_id = TradeId::from(trade.trade_id.as_str());
8593 let (emitter, rx) = test_emitter(clock);
8594 let mut rx = if failed_delivery {
8595 drop(rx);
8596 None
8597 } else {
8598 Some(rx)
8599 };
8600
8601 dispatch_trades_payload(
8602 DeriveTradesSubscriptionData {
8603 trades: vec![trade.clone()],
8604 },
8605 &emitter,
8606 AccountId::from("DERIVE-001"),
8607 clock,
8608 &state,
8609 );
8610
8611 assert_eq!(
8612 state.bound_venue_order_id(&client_order_id),
8613 bound.then_some(venue_order_id)
8614 );
8615 assert_eq!(state.identity(&client_order_id), Some(identity));
8616 assert!(!state.knows_venue_leg(
8617 &client_order_id,
8618 VenueOrderId::from(trade.order_id.as_str())
8619 ));
8620 assert!(!state.contains_trade(&trade_id));
8621 assert!(!state.contains_accepted(&client_order_id));
8622 assert_eq!(
8623 serde_json::to_value(state.take_undelivered_trades()).unwrap(),
8624 serde_json::to_value(vec![trade]).unwrap()
8625 );
8626
8627 if let Some(rx) = rx.as_mut() {
8628 assert!(rx.try_recv().is_err());
8629 }
8630 }
8631
8632 #[rstest]
8633 fn test_native_fill_side_conflict_stays_deferred_after_terminal(
8634 #[values(OrderSide::Buy, OrderSide::Sell)] side: OrderSide,
8635 ) {
8636 let clock = get_atomic_clock_realtime();
8637 let state = WsDispatchState::new(42);
8638 let client_order_id = ClientOrderId::from("NATIVE-TERMINAL-SIDE-CONFLICT");
8639
8640 let identity = OrderIdentity {
8641 instrument_id: InstrumentId::from("ETH-PERP.DERIVE"),
8642 strategy_id: StrategyId::from("S-1"),
8643 order_side: side,
8644 order_type: OrderType::Limit,
8645 };
8646
8647 state.register_identity(client_order_id, identity);
8648 state.record_order_shape(client_order_id, Quantity::from("0.5"), None);
8649 let mut trade: DeriveTrade = serde_json::from_str(include_str!(
8650 "../test_data/perps/http_private_trade_eth.json"
8651 ))
8652 .unwrap();
8653 trade.label = client_order_id.to_string().into();
8654
8655 trade.direction = match side {
8656 OrderSide::Buy => DeriveOrderSide::Sell,
8657 OrderSide::Sell => DeriveOrderSide::Buy,
8658 };
8659
8660 let mut complete = trade.clone();
8661 complete.order_id = "native-complete".into();
8662 complete.trade_id = "native-complete-trade".into();
8663
8664 complete.direction = match side {
8665 OrderSide::Buy => DeriveOrderSide::Buy,
8666 OrderSide::Sell => DeriveOrderSide::Sell,
8667 };
8668
8669 let (emitter, mut rx) = test_emitter(clock);
8670 let account_id = AccountId::from("DERIVE-001");
8671 dispatch_trades_payload(
8672 DeriveTradesSubscriptionData {
8673 trades: vec![trade.clone(), complete],
8674 },
8675 &emitter,
8676 account_id,
8677 clock,
8678 &state,
8679 );
8680
8681 dispatch_trades_payload(
8682 DeriveTradesSubscriptionData {
8683 trades: state.take_undelivered_trades(),
8684 },
8685 &emitter,
8686 account_id,
8687 clock,
8688 &state,
8689 );
8690
8691 let ExecutionEvent::Order(OrderEventAny::Accepted(accepted)) = rx.try_recv().unwrap()
8692 else {
8693 panic!("Expected acceptance before the completing fill");
8694 };
8695
8696 let ExecutionEvent::Order(OrderEventAny::Filled(fill)) = rx.try_recv().unwrap() else {
8697 panic!("Expected the native completing fill");
8698 };
8699
8700 assert_eq!(accepted.client_order_id, client_order_id);
8701 assert_eq!(
8702 accepted.venue_order_id,
8703 VenueOrderId::from("native-complete")
8704 );
8705 assert_eq!(fill.client_order_id, client_order_id);
8706 assert_eq!(fill.venue_order_id, VenueOrderId::from("native-complete"));
8707 assert_eq!(fill.order_side, side);
8708 assert_eq!(fill.last_qty.as_decimal(), dec!(0.5));
8709 assert_eq!(state.identity(&client_order_id), None);
8710 assert!(state.is_terminal(&client_order_id, identity.instrument_id));
8711 assert!(!state.contains_trade(&TradeId::from(trade.trade_id.as_str())));
8712 assert_eq!(
8713 serde_json::to_value(state.take_undelivered_trades()).unwrap(),
8714 serde_json::to_value(vec![trade]).unwrap()
8715 );
8716 assert!(rx.try_recv().is_err());
8717 }
8718
8719 #[rstest]
8720 fn test_trade_delivery_failure_preserves_replay() {
8721 let clock = get_atomic_clock_realtime();
8722 let state = WsDispatchState::new(42);
8723 let trade: DeriveTrade = serde_json::from_str(include_str!(
8724 "../test_data/perps/http_private_trade_eth.json"
8725 ))
8726 .unwrap();
8727 let trade_id = TradeId::new(&trade.trade_id);
8728 let (emitter, rx) = test_emitter(clock);
8729 drop(rx);
8730 dispatch_trades_payload(
8731 DeriveTradesSubscriptionData {
8732 trades: vec![trade],
8733 },
8734 &emitter,
8735 AccountId::from("DERIVE-001"),
8736 clock,
8737 &state,
8738 );
8739
8740 assert!(!state.contains_trade(&trade_id));
8741 }
8742
8743 fn sample_derive_instrument() -> DeriveInstrument {
8744 serde_json::from_str(include_str!("../test_data/perps/instrument_eth.json")).unwrap()
8745 }
8746
8747 fn test_client_with_instrument() -> DeriveExecutionClient {
8748 let mut client = DeriveExecutionClient::new(test_core(), test_config()).unwrap();
8749 let instrument =
8750 parse_derive_instrument_any(&sample_derive_instrument(), UnixNanos::default())
8751 .unwrap()
8752 .unwrap();
8753 client.on_instrument(instrument);
8754 client
8755 }
8756
8757 #[rstest]
8758 #[case(" ")]
8759 #[case("external-\u{03bb}")]
8760 fn test_order_result_invalid_id_preserves_identity_without_binding_or_events(
8761 #[case] invalid_id: &str,
8762 ) {
8763 let clock = get_atomic_clock_realtime();
8764 let account_id = AccountId::from("DERIVE-001");
8765 let client_order_id = ClientOrderId::from("STRAT-SUBMIT-BAD-ID");
8766 let instrument_id = InstrumentId::from("ETH-PERP.DERIVE");
8767 let strategy_id = StrategyId::from("S-1");
8768
8769 let identity = OrderIdentity {
8770 instrument_id,
8771 strategy_id,
8772 order_side: OrderSide::Buy,
8773 order_type: OrderType::Limit,
8774 };
8775
8776 let state = WsDispatchState::new(42);
8777 state.register_identity(client_order_id, identity);
8778 let (emitter, mut rx) = test_emitter(clock);
8779 let mut order: DeriveOrder = serde_json::from_str(include_str!(
8780 "../test_data/perps/http_order_eth_partially_filled.json"
8781 ))
8782 .unwrap();
8783 order.label = client_order_id.as_str().into();
8784 order.order_id = invalid_id.to_string();
8785 order.direction = crate::common::enums::DeriveOrderSide::Buy;
8786 order.order_status = DeriveOrderStatus::Open;
8787 order.filled_amount = Decimal::ZERO;
8788 dispatch_order_result(
8789 DeriveOrderResult {
8790 order: order.clone(),
8791 trades: vec![],
8792 },
8793 client_order_id,
8794 &emitter,
8795 account_id,
8796 clock,
8797 &state,
8798 );
8799
8800 assert!(
8801 rx.try_recv().is_err(),
8802 "invalid response must emit no lifecycle event"
8803 );
8804 assert_eq!(state.bound_venue_order_id(&client_order_id), None);
8805 let retained = state
8806 .identity(&client_order_id)
8807 .expect("identity retained for reconciliation");
8808 assert_eq!(retained.instrument_id, instrument_id);
8809 assert_eq!(retained.strategy_id, strategy_id);
8810 assert_eq!(retained.order_side, OrderSide::Buy);
8811 assert_eq!(retained.order_type, OrderType::Limit);
8812 let venue_order_id = VenueOrderId::from("ord-valid-submit-result");
8813 order.order_id = venue_order_id.to_string();
8814 dispatch_order_result(
8815 DeriveOrderResult {
8816 order,
8817 trades: vec![],
8818 },
8819 client_order_id,
8820 &emitter,
8821 account_id,
8822 clock,
8823 &state,
8824 );
8825
8826 let event = rx
8827 .try_recv()
8828 .expect("valid response accepts retained identity");
8829
8830 let ExecutionEvent::Order(OrderEventAny::Accepted(accepted)) = event else {
8831 panic!("expected tracked acceptance, was {event:?}");
8832 };
8833
8834 assert_eq!(accepted.client_order_id, client_order_id);
8835 assert_eq!(accepted.instrument_id, instrument_id);
8836 assert_eq!(accepted.strategy_id, strategy_id);
8837 assert_eq!(accepted.account_id, account_id);
8838 assert_eq!(accepted.venue_order_id, venue_order_id);
8839 assert_eq!(
8840 state.bound_venue_order_id(&client_order_id),
8841 Some(venue_order_id)
8842 );
8843 assert!(rx.try_recv().is_err(), "valid response must accept once");
8844 }
8845}