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