Skip to main content

nautilus_derive/
execution.rs

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