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