Skip to main content

nautilus_event_store/capture/
builtins.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//! Representative encoders for the SPEC's allow-listed message surface.
17//!
18//! Phase 6 shipped a sample triple (`SubmitOrder` command, `OrderFilled` generated event,
19//! `OrderStatusReport` raw venue report) so the bus capture adapter had a working
20//! allow-list end-to-end. Phase 7 adds envelope-aware dispatchers for the
21//! wrapper enums production code actually pushes through `send_trading_command`,
22//! `publish_order_event`, `send_execution_report`, and `publish_position_event`
23//! ([`TradingCommand`], [`OrderEventAny`], [`ExecutionReport`], [`PositionEvent`]).
24//! The same pattern covers `send_data_command` and `send_data_response` ([`DataCommand`],
25//! [`DataResponse`]). These reach the bus tap as their wrapper [`std::any::TypeId`] and
26//! the bare-type registrations would miss them. Each dispatcher unwraps its variant,
27//! runs the inner-typed encode, and stamps the inner-variant's canonical `payload_type`
28//! tag so forensics scans see entries identical to the bare-type capture path.
29//!
30//! The payload serialization format is MessagePack via `rmp-serde`. The on-disk envelope
31//! uses the positional codec; MessagePack inside the payload handles the upstream Nautilus
32//! types that carry `#[serde(tag = "type")]` internal tagging, which a non-self-describing
33//! format cannot round-trip.
34
35use std::collections::HashSet;
36
37use bytes::Bytes;
38use nautilus_common::{
39    messages::{
40        data::{
41            BarsResponse, BookDeltasResponse, BookDepthResponse, BookResponse, CustomDataResponse,
42            DataCommand, DataResponse, DataResponseEnvelope, FundingRatesResponse,
43            InstrumentResponse, InstrumentsResponse, OptionChainReferencePriceResponse,
44            QuotesResponse, TradesResponse,
45        },
46        execution::{
47            BatchCancelOrders, BatchModifyOrders, CancelAllOrders, CancelOrder, ExecutionReport,
48            ModifyOrder, QueryAccount, QueryOrder, SubmitOrder, SubmitOrderList, TradingCommand,
49        },
50    },
51    timer::TimeEvent,
52};
53use nautilus_core::{Params, UUID4, UnixNanos};
54use nautilus_model::{
55    data::{DataType, InstrumentClose},
56    events::{
57        AccountState, OrderAccepted, OrderCancelRejected, OrderCanceled, OrderDenied,
58        OrderEmulated, OrderEventAny, OrderExpired, OrderFillVoided, OrderFilled, OrderInitialized,
59        OrderModifyRejected, OrderPendingCancel, OrderPendingUpdate, OrderRejected, OrderReleased,
60        OrderSubmitted, OrderTriggered, OrderUpdated, PositionAdjusted, PositionChanged,
61        PositionClosed, PositionEvent, PositionOpened,
62    },
63    identifiers::{ClientId, InstrumentId, Venue},
64    reports::{ExecutionMassStatus, FillReport, OrderStatusReport, PositionStatusReport},
65};
66use serde::Serialize;
67use ustr::Ustr;
68
69use crate::{
70    backend::{IndexKey, IndexKind},
71    capture::{
72        encoder::{EncodeError, EncodedPayload},
73        registry::EncoderRegistry,
74    },
75    entry::PayloadType,
76    headers::Headers,
77};
78
79/// The canonical `payload_type` tag for [`SubmitOrder`].
80pub const PAYLOAD_TYPE_SUBMIT_ORDER: &str = "SubmitOrder";
81/// The canonical `payload_type` tag for [`SubmitOrderList`].
82pub const PAYLOAD_TYPE_SUBMIT_ORDER_LIST: &str = "SubmitOrderList";
83/// The canonical `payload_type` tag for [`ModifyOrder`].
84pub const PAYLOAD_TYPE_MODIFY_ORDER: &str = "ModifyOrder";
85/// The canonical `payload_type` tag for [`BatchModifyOrders`].
86pub const PAYLOAD_TYPE_BATCH_MODIFY_ORDERS: &str = "BatchModifyOrders";
87/// The canonical `payload_type` tag for [`CancelOrder`].
88pub const PAYLOAD_TYPE_CANCEL_ORDER: &str = "CancelOrder";
89/// The canonical `payload_type` tag for [`CancelAllOrders`].
90pub const PAYLOAD_TYPE_CANCEL_ALL_ORDERS: &str = "CancelAllOrders";
91/// The canonical `payload_type` tag for [`BatchCancelOrders`].
92pub const PAYLOAD_TYPE_BATCH_CANCEL_ORDERS: &str = "BatchCancelOrders";
93/// The canonical `payload_type` tag for [`QueryOrder`].
94pub const PAYLOAD_TYPE_QUERY_ORDER: &str = "QueryOrder";
95/// The canonical `payload_type` tag for [`QueryAccount`].
96pub const PAYLOAD_TYPE_QUERY_ACCOUNT: &str = "QueryAccount";
97
98/// The canonical `payload_type` tag for [`OrderInitialized`].
99pub const PAYLOAD_TYPE_ORDER_INITIALIZED: &str = "OrderInitialized";
100/// The canonical `payload_type` tag for [`OrderDenied`].
101pub const PAYLOAD_TYPE_ORDER_DENIED: &str = "OrderDenied";
102/// The canonical `payload_type` tag for [`OrderEmulated`].
103pub const PAYLOAD_TYPE_ORDER_EMULATED: &str = "OrderEmulated";
104/// The canonical `payload_type` tag for [`OrderReleased`].
105pub const PAYLOAD_TYPE_ORDER_RELEASED: &str = "OrderReleased";
106/// The canonical `payload_type` tag for [`OrderSubmitted`].
107pub const PAYLOAD_TYPE_ORDER_SUBMITTED: &str = "OrderSubmitted";
108/// The canonical `payload_type` tag for [`OrderAccepted`].
109pub const PAYLOAD_TYPE_ORDER_ACCEPTED: &str = "OrderAccepted";
110/// The canonical `payload_type` tag for [`OrderRejected`].
111pub const PAYLOAD_TYPE_ORDER_REJECTED: &str = "OrderRejected";
112/// The canonical `payload_type` tag for [`OrderCanceled`].
113pub const PAYLOAD_TYPE_ORDER_CANCELED: &str = "OrderCanceled";
114/// The canonical `payload_type` tag for [`OrderExpired`].
115pub const PAYLOAD_TYPE_ORDER_EXPIRED: &str = "OrderExpired";
116/// The canonical `payload_type` tag for [`OrderTriggered`].
117pub const PAYLOAD_TYPE_ORDER_TRIGGERED: &str = "OrderTriggered";
118/// The canonical `payload_type` tag for [`OrderPendingUpdate`].
119pub const PAYLOAD_TYPE_ORDER_PENDING_UPDATE: &str = "OrderPendingUpdate";
120/// The canonical `payload_type` tag for [`OrderPendingCancel`].
121pub const PAYLOAD_TYPE_ORDER_PENDING_CANCEL: &str = "OrderPendingCancel";
122/// The canonical `payload_type` tag for [`OrderModifyRejected`].
123pub const PAYLOAD_TYPE_ORDER_MODIFY_REJECTED: &str = "OrderModifyRejected";
124/// The canonical `payload_type` tag for [`OrderCancelRejected`].
125pub const PAYLOAD_TYPE_ORDER_CANCEL_REJECTED: &str = "OrderCancelRejected";
126/// The canonical `payload_type` tag for [`OrderUpdated`].
127pub const PAYLOAD_TYPE_ORDER_UPDATED: &str = "OrderUpdated";
128/// The canonical `payload_type` tag for [`OrderFilled`].
129pub const PAYLOAD_TYPE_ORDER_FILLED: &str = "OrderFilled";
130/// The canonical `payload_type` tag for [`OrderFillVoided`].
131pub const PAYLOAD_TYPE_ORDER_FILL_VOIDED: &str = "OrderFillVoided";
132/// The canonical `payload_type` tag for [`OrderStatusReport`].
133pub const PAYLOAD_TYPE_ORDER_STATUS_REPORT: &str = "OrderStatusReport";
134/// The canonical `payload_type` tag for [`FillReport`].
135pub const PAYLOAD_TYPE_FILL_REPORT: &str = "FillReport";
136/// The canonical `payload_type` tag for the [`ExecutionReport::OrderWithFills`] bundle.
137pub const PAYLOAD_TYPE_ORDER_WITH_FILLS: &str = "OrderWithFills";
138/// The canonical `payload_type` tag for [`PositionStatusReport`].
139pub const PAYLOAD_TYPE_POSITION_STATUS_REPORT: &str = "PositionStatusReport";
140/// The canonical `payload_type` tag for [`ExecutionMassStatus`].
141pub const PAYLOAD_TYPE_EXECUTION_MASS_STATUS: &str = "ExecutionMassStatus";
142/// The canonical `payload_type` tag for [`PositionOpened`].
143pub const PAYLOAD_TYPE_POSITION_OPENED: &str = "PositionOpened";
144/// The canonical `payload_type` tag for [`PositionChanged`].
145pub const PAYLOAD_TYPE_POSITION_CHANGED: &str = "PositionChanged";
146/// The canonical `payload_type` tag for [`PositionClosed`].
147pub const PAYLOAD_TYPE_POSITION_CLOSED: &str = "PositionClosed";
148/// The canonical `payload_type` tag for [`PositionAdjusted`].
149pub const PAYLOAD_TYPE_POSITION_ADJUSTED: &str = "PositionAdjusted";
150/// The canonical `payload_type` tag for [`AccountState`].
151pub const PAYLOAD_TYPE_ACCOUNT_STATE: &str = "AccountState";
152/// The canonical `payload_type` tag for [`InstrumentClose`].
153pub const PAYLOAD_TYPE_INSTRUMENT_CLOSE: &str = "InstrumentClose";
154/// The canonical `payload_type` tag for [`TimeEvent`].
155pub const PAYLOAD_TYPE_TIME_EVENT: &str = "TimeEvent";
156
157/// The canonical `payload_type` tag for `RequestCommand`.
158pub const PAYLOAD_TYPE_REQUEST_COMMAND: &str = "RequestCommand";
159/// The canonical `payload_type` tag for `SubscribeCommand`.
160pub const PAYLOAD_TYPE_SUBSCRIBE_COMMAND: &str = "SubscribeCommand";
161/// The canonical `payload_type` tag for `UnsubscribeCommand`.
162pub const PAYLOAD_TYPE_UNSUBSCRIBE_COMMAND: &str = "UnsubscribeCommand";
163#[cfg(feature = "defi")]
164/// The canonical `payload_type` tag for `DefiRequestCommand`.
165pub const PAYLOAD_TYPE_DEFI_REQUEST_COMMAND: &str = "DefiRequestCommand";
166#[cfg(feature = "defi")]
167/// The canonical `payload_type` tag for `DefiSubscribeCommand`.
168pub const PAYLOAD_TYPE_DEFI_SUBSCRIBE_COMMAND: &str = "DefiSubscribeCommand";
169#[cfg(feature = "defi")]
170/// The canonical `payload_type` tag for `DefiUnsubscribeCommand`.
171pub const PAYLOAD_TYPE_DEFI_UNSUBSCRIBE_COMMAND: &str = "DefiUnsubscribeCommand";
172
173/// The canonical `payload_type` tag for [`CustomDataResponse`].
174pub const PAYLOAD_TYPE_CUSTOM_DATA_RESPONSE: &str = "CustomDataResponse";
175/// The canonical `payload_type` tag for [`InstrumentResponse`].
176pub const PAYLOAD_TYPE_INSTRUMENT_RESPONSE: &str = "InstrumentResponse";
177/// The canonical `payload_type` tag for [`InstrumentsResponse`].
178pub const PAYLOAD_TYPE_INSTRUMENTS_RESPONSE: &str = "InstrumentsResponse";
179/// The canonical `payload_type` tag for [`BookResponse`].
180pub const PAYLOAD_TYPE_BOOK_RESPONSE: &str = "BookResponse";
181/// The canonical `payload_type` tag for [`BookDeltasResponse`].
182pub const PAYLOAD_TYPE_BOOK_DELTAS_RESPONSE: &str = "BookDeltasResponse";
183/// The canonical `payload_type` tag for [`BookDepthResponse`].
184pub const PAYLOAD_TYPE_BOOK_DEPTH_RESPONSE: &str = "BookDepthResponse";
185/// The canonical `payload_type` tag for [`QuotesResponse`].
186pub const PAYLOAD_TYPE_QUOTES_RESPONSE: &str = "QuotesResponse";
187/// The canonical `payload_type` tag for [`TradesResponse`].
188pub const PAYLOAD_TYPE_TRADES_RESPONSE: &str = "TradesResponse";
189/// The canonical `payload_type` tag for [`FundingRatesResponse`].
190pub const PAYLOAD_TYPE_FUNDING_RATES_RESPONSE: &str = "FundingRatesResponse";
191/// The canonical `payload_type` tag for [`OptionChainReferencePriceResponse`].
192pub const PAYLOAD_TYPE_OPTION_CHAIN_REFERENCE_PRICE_RESPONSE: &str =
193    "OptionChainReferencePriceResponse";
194/// The canonical `payload_type` tag for [`BarsResponse`].
195pub const PAYLOAD_TYPE_BARS_RESPONSE: &str = "BarsResponse";
196/// The canonical `payload_type` tag for [`DataResponseEnvelope`].
197pub const PAYLOAD_TYPE_DATA_RESPONSE_ENVELOPE: &str = "DataResponseEnvelope";
198
199// Wrapper-level fallback tag reached only when a dispatcher returns an
200// `EncodedPayload` without an override. Every current variant stamps its own
201// inner tag, so this is a sentinel for a future variant that forgets the
202// override rather than a tag the writer is expected to commit.
203const PAYLOAD_TYPE_TRADING_COMMAND: &str = "TradingCommand";
204
205const PAYLOAD_TYPE_ORDER_EVENT_ANY: &str = "OrderEventAny";
206
207const PAYLOAD_TYPE_EXECUTION_REPORT: &str = "ExecutionReport";
208
209const PAYLOAD_TYPE_POSITION_EVENT: &str = "PositionEvent";
210
211const PAYLOAD_TYPE_DATA_COMMAND: &str = "DataCommand";
212
213const PAYLOAD_TYPE_DATA_RESPONSE: &str = "DataResponse";
214
215#[cfg(test)]
216pub(crate) const DEFAULT_CAPTURE_PAYLOAD_TYPES: &[&str] = &[
217    PAYLOAD_TYPE_SUBMIT_ORDER,
218    PAYLOAD_TYPE_SUBMIT_ORDER_LIST,
219    PAYLOAD_TYPE_MODIFY_ORDER,
220    PAYLOAD_TYPE_BATCH_MODIFY_ORDERS,
221    PAYLOAD_TYPE_CANCEL_ORDER,
222    PAYLOAD_TYPE_CANCEL_ALL_ORDERS,
223    PAYLOAD_TYPE_BATCH_CANCEL_ORDERS,
224    PAYLOAD_TYPE_QUERY_ORDER,
225    PAYLOAD_TYPE_QUERY_ACCOUNT,
226    PAYLOAD_TYPE_ORDER_INITIALIZED,
227    PAYLOAD_TYPE_ORDER_DENIED,
228    PAYLOAD_TYPE_ORDER_EMULATED,
229    PAYLOAD_TYPE_ORDER_RELEASED,
230    PAYLOAD_TYPE_ORDER_SUBMITTED,
231    PAYLOAD_TYPE_ORDER_ACCEPTED,
232    PAYLOAD_TYPE_ORDER_REJECTED,
233    PAYLOAD_TYPE_ORDER_CANCELED,
234    PAYLOAD_TYPE_ORDER_EXPIRED,
235    PAYLOAD_TYPE_ORDER_TRIGGERED,
236    PAYLOAD_TYPE_ORDER_PENDING_UPDATE,
237    PAYLOAD_TYPE_ORDER_PENDING_CANCEL,
238    PAYLOAD_TYPE_ORDER_MODIFY_REJECTED,
239    PAYLOAD_TYPE_ORDER_CANCEL_REJECTED,
240    PAYLOAD_TYPE_ORDER_UPDATED,
241    PAYLOAD_TYPE_ORDER_FILLED,
242    PAYLOAD_TYPE_ORDER_FILL_VOIDED,
243    PAYLOAD_TYPE_ORDER_STATUS_REPORT,
244    PAYLOAD_TYPE_FILL_REPORT,
245    PAYLOAD_TYPE_ORDER_WITH_FILLS,
246    PAYLOAD_TYPE_POSITION_STATUS_REPORT,
247    PAYLOAD_TYPE_EXECUTION_MASS_STATUS,
248    PAYLOAD_TYPE_POSITION_OPENED,
249    PAYLOAD_TYPE_POSITION_CHANGED,
250    PAYLOAD_TYPE_POSITION_CLOSED,
251    PAYLOAD_TYPE_POSITION_ADJUSTED,
252    PAYLOAD_TYPE_ACCOUNT_STATE,
253    PAYLOAD_TYPE_INSTRUMENT_CLOSE,
254    PAYLOAD_TYPE_TIME_EVENT,
255    PAYLOAD_TYPE_REQUEST_COMMAND,
256    PAYLOAD_TYPE_SUBSCRIBE_COMMAND,
257    PAYLOAD_TYPE_UNSUBSCRIBE_COMMAND,
258    #[cfg(feature = "defi")]
259    PAYLOAD_TYPE_DEFI_REQUEST_COMMAND,
260    #[cfg(feature = "defi")]
261    PAYLOAD_TYPE_DEFI_SUBSCRIBE_COMMAND,
262    #[cfg(feature = "defi")]
263    PAYLOAD_TYPE_DEFI_UNSUBSCRIBE_COMMAND,
264    PAYLOAD_TYPE_CUSTOM_DATA_RESPONSE,
265    PAYLOAD_TYPE_INSTRUMENT_RESPONSE,
266    PAYLOAD_TYPE_INSTRUMENTS_RESPONSE,
267    PAYLOAD_TYPE_BOOK_RESPONSE,
268    PAYLOAD_TYPE_BOOK_DELTAS_RESPONSE,
269    PAYLOAD_TYPE_BOOK_DEPTH_RESPONSE,
270    PAYLOAD_TYPE_QUOTES_RESPONSE,
271    PAYLOAD_TYPE_TRADES_RESPONSE,
272    PAYLOAD_TYPE_FUNDING_RATES_RESPONSE,
273    PAYLOAD_TYPE_OPTION_CHAIN_REFERENCE_PRICE_RESPONSE,
274    PAYLOAD_TYPE_BARS_RESPONSE,
275    PAYLOAD_TYPE_DATA_RESPONSE_ENVELOPE,
276];
277
278/// Returns an [`EncoderRegistry`] preloaded with the default encoders.
279///
280/// Callers can extend the returned registry with additional encoders before constructing
281/// the [`crate::capture::BusCaptureAdapter`].
282#[must_use]
283pub fn default_registry() -> EncoderRegistry {
284    let mut registry = EncoderRegistry::new();
285    register_default(&mut registry);
286    registry
287}
288
289/// Adds the default encoders to `registry`.
290///
291/// The bare-type registrations remain for capture sites that already submit the inner
292/// type directly (the kernel's `RunStarted` path and a few internal tests). The envelope
293/// registrations are what production bus traffic actually hits: `send_trading_command`
294/// reaches the tap as [`TradingCommand`], `publish_order_event` reaches it as
295/// [`OrderEventAny`], `send_execution_report` reaches it as [`ExecutionReport`],
296/// `publish_position_event` reaches it as [`PositionEvent`], and `send_data_response`
297/// reaches it as [`DataResponse`]. Without these wrapper-aware dispatchers the tap looks
298/// up the wrapper's [`std::any::TypeId`], finds no encoder, and silently drops the
299/// capture.
300///
301/// [`AccountState`] is registered as a bare type: `publish_account_state` and
302/// `send_account_state` both reach the tap as the same `AccountState` `TypeId`, so a
303/// single registration covers both dispatch paths.
304///
305/// [`OrderStatusReport`], [`FillReport`], and [`PositionStatusReport`] are registered
306/// as bare types because the execution engine publishes raw venue reports through
307/// `publish_any` on `reconciliation.raw.*` topics before any state mutation. The
308/// bare-type registration is what captures those raw inputs for forensic replay.
309pub fn register_default(registry: &mut EncoderRegistry) {
310    registry
311        .register::<SubmitOrder, _>(payload_type(PAYLOAD_TYPE_SUBMIT_ORDER), encode_submit_order);
312    registry
313        .register::<OrderFilled, _>(payload_type(PAYLOAD_TYPE_ORDER_FILLED), encode_order_filled);
314    registry.register::<OrderStatusReport, _>(
315        payload_type(PAYLOAD_TYPE_ORDER_STATUS_REPORT),
316        encode_order_status_report,
317    );
318    registry.register::<FillReport, _>(payload_type(PAYLOAD_TYPE_FILL_REPORT), encode_fill_report);
319    registry.register::<PositionStatusReport, _>(
320        payload_type(PAYLOAD_TYPE_POSITION_STATUS_REPORT),
321        encode_position_status_report,
322    );
323    registry.register::<TradingCommand, _>(
324        payload_type(PAYLOAD_TYPE_TRADING_COMMAND),
325        encode_trading_command,
326    );
327    registry.register::<OrderEventAny, _>(
328        payload_type(PAYLOAD_TYPE_ORDER_EVENT_ANY),
329        encode_order_event_any,
330    );
331    registry.register::<ExecutionReport, _>(
332        payload_type(PAYLOAD_TYPE_EXECUTION_REPORT),
333        encode_execution_report,
334    );
335    registry.register::<PositionEvent, _>(
336        payload_type(PAYLOAD_TYPE_POSITION_EVENT),
337        encode_position_event,
338    );
339    registry.register::<AccountState, _>(
340        payload_type(PAYLOAD_TYPE_ACCOUNT_STATE),
341        encode_account_state,
342    );
343    registry.register::<InstrumentClose, _>(
344        payload_type(PAYLOAD_TYPE_INSTRUMENT_CLOSE),
345        encode_instrument_close,
346    );
347    registry.register::<TimeEvent, _>(payload_type(PAYLOAD_TYPE_TIME_EVENT), encode_time_event);
348    registry
349        .register::<DataCommand, _>(payload_type(PAYLOAD_TYPE_DATA_COMMAND), encode_data_command);
350    registry.register::<DataResponse, _>(
351        payload_type(PAYLOAD_TYPE_DATA_RESPONSE),
352        encode_data_response,
353    );
354
355    registry.register::<DataResponseEnvelope, _>(
356        payload_type(PAYLOAD_TYPE_DATA_RESPONSE_ENVELOPE),
357        encode_response_envelope,
358    );
359    register_default_headers(registry);
360    register_default_identities(registry);
361}
362
363fn register_default_headers(registry: &mut EncoderRegistry) {
364    registry.register_headers::<SubmitOrder, _>(extract_submit_order_headers);
365    registry.register_headers::<SubmitOrderList, _>(extract_submit_order_list_headers);
366    registry.register_headers::<ModifyOrder, _>(extract_modify_order_headers);
367    registry.register_headers::<BatchModifyOrders, _>(extract_batch_modify_orders_headers);
368    registry.register_headers::<CancelOrder, _>(extract_cancel_order_headers);
369    registry.register_headers::<CancelAllOrders, _>(extract_cancel_all_orders_headers);
370    registry.register_headers::<BatchCancelOrders, _>(extract_batch_cancel_orders_headers);
371    registry.register_headers::<QueryOrder, _>(extract_query_order_headers);
372    registry.register_headers::<QueryAccount, _>(extract_query_account_headers);
373    registry.register_headers::<TradingCommand, _>(extract_trading_command_headers);
374    registry.register_headers::<DataCommand, _>(extract_data_command_headers);
375    registry.register_headers::<DataResponse, _>(extract_data_response_headers);
376    registry.register_headers::<DataResponseEnvelope, _>(|response| {
377        headers_from_fields(Some(*response.correlation_id()), None)
378    });
379}
380
381/// Attaches identity extractors for the types production dispatch pushes through more
382/// than one tap-visible boundary (portfolio endpoint send plus strategy topic publish,
383/// command hops through risk to execution, account states on both dispatch paths, data
384/// commands through the queue endpoint and the drained execute endpoint), so the
385/// adapter captures each logical message exactly once. The venue report types
386/// deliberately carry no extractor: the raw `reconciliation.raw.*` publish and the
387/// engine-bound dispatch are distinct capture boundaries.
388fn register_default_identities(registry: &mut EncoderRegistry) {
389    registry.register_identity::<SubmitOrder, _>(|command| Some(command.command_id));
390    registry.register_identity::<OrderFilled, _>(|event| Some(event.event_id));
391    registry.register_identity::<TradingCommand, _>(|c| Some(extract_trading_command_identity(c)));
392    registry.register_identity::<OrderEventAny, _>(|e| Some(extract_order_event_any_identity(e)));
393    registry.register_identity::<AccountState, _>(|state| Some(state.event_id));
394    registry.register_identity::<DataCommand, _>(extract_data_command_identity);
395}
396
397fn extract_data_command_identity(command: &DataCommand) -> Option<UUID4> {
398    match command {
399        DataCommand::Request(cmd) => Some(*cmd.request_id()),
400        DataCommand::Subscribe(cmd) => Some(cmd.command_id()),
401        DataCommand::Unsubscribe(cmd) => Some(cmd.command_id()),
402        #[cfg(feature = "defi")]
403        DataCommand::DefiRequest(cmd) => Some(*cmd.request_id()),
404        #[cfg(feature = "defi")]
405        DataCommand::DefiSubscribe(cmd) => Some(cmd.command_id()),
406        #[cfg(feature = "defi")]
407        DataCommand::DefiUnsubscribe(cmd) => Some(cmd.command_id()),
408        // `DataCommand` is `#[non_exhaustive]`; future variants capture per dispatch
409        _ => None,
410    }
411}
412
413fn extract_trading_command_identity(command: &TradingCommand) -> UUID4 {
414    match command {
415        TradingCommand::SubmitOrder(c) => c.command_id,
416        TradingCommand::SubmitOrderList(c) => c.command_id,
417        TradingCommand::ModifyOrder(c) => c.command_id,
418        TradingCommand::ModifyOrders(c) => c.command_id,
419        TradingCommand::CancelOrder(c) => c.command_id,
420        TradingCommand::CancelOrders(c) => c.command_id,
421        TradingCommand::CancelAllOrders(c) => c.command_id,
422        TradingCommand::QueryOrder(c) => c.command_id,
423        TradingCommand::QueryAccount(c) => c.command_id,
424    }
425}
426
427fn extract_order_event_any_identity(event: &OrderEventAny) -> UUID4 {
428    match event {
429        OrderEventAny::Initialized(e) => e.event_id,
430        OrderEventAny::Denied(e) => e.event_id,
431        OrderEventAny::Emulated(e) => e.event_id,
432        OrderEventAny::Released(e) => e.event_id,
433        OrderEventAny::Submitted(e) => e.event_id,
434        OrderEventAny::Accepted(e) => e.event_id,
435        OrderEventAny::Rejected(e) => e.event_id,
436        OrderEventAny::Canceled(e) => e.event_id,
437        OrderEventAny::Expired(e) => e.event_id,
438        OrderEventAny::Triggered(e) => e.event_id,
439        OrderEventAny::PendingUpdate(e) => e.event_id,
440        OrderEventAny::PendingCancel(e) => e.event_id,
441        OrderEventAny::ModifyRejected(e) => e.event_id,
442        OrderEventAny::CancelRejected(e) => e.event_id,
443        OrderEventAny::Updated(e) => e.event_id,
444        OrderEventAny::Filled(e) => e.event_id,
445        OrderEventAny::FillVoided(e) => e.event_id,
446    }
447}
448
449fn headers_from_fields(correlation_id: Option<UUID4>, causation_id: Option<UUID4>) -> Headers {
450    Headers {
451        correlation_id,
452        causation_id,
453    }
454}
455
456fn extract_submit_order_headers(cmd: &SubmitOrder) -> Headers {
457    headers_from_fields(cmd.correlation_id, cmd.causation_id)
458}
459
460fn extract_submit_order_list_headers(cmd: &SubmitOrderList) -> Headers {
461    headers_from_fields(cmd.correlation_id, cmd.causation_id)
462}
463
464fn extract_modify_order_headers(cmd: &ModifyOrder) -> Headers {
465    headers_from_fields(cmd.correlation_id, cmd.causation_id)
466}
467
468fn extract_batch_modify_orders_headers(cmd: &BatchModifyOrders) -> Headers {
469    headers_from_fields(cmd.correlation_id, cmd.causation_id)
470}
471
472fn extract_cancel_order_headers(cmd: &CancelOrder) -> Headers {
473    headers_from_fields(cmd.correlation_id, cmd.causation_id)
474}
475
476fn extract_cancel_all_orders_headers(cmd: &CancelAllOrders) -> Headers {
477    headers_from_fields(cmd.correlation_id, cmd.causation_id)
478}
479
480fn extract_batch_cancel_orders_headers(cmd: &BatchCancelOrders) -> Headers {
481    headers_from_fields(cmd.correlation_id, cmd.causation_id)
482}
483
484fn extract_query_order_headers(cmd: &QueryOrder) -> Headers {
485    headers_from_fields(cmd.correlation_id, cmd.causation_id)
486}
487
488fn extract_query_account_headers(cmd: &QueryAccount) -> Headers {
489    headers_from_fields(cmd.correlation_id, cmd.causation_id)
490}
491
492// `send_trading_command` reaches the bus tap with the wrapper's `TypeId`, so the
493// extractor must mirror the encoder's variant dispatch to surface the inner command's
494// correlation metadata on the captured entry.
495fn extract_trading_command_headers(command: &TradingCommand) -> Headers {
496    match command {
497        TradingCommand::SubmitOrder(cmd) => extract_submit_order_headers(cmd),
498        TradingCommand::SubmitOrderList(cmd) => extract_submit_order_list_headers(cmd),
499        TradingCommand::ModifyOrder(cmd) => extract_modify_order_headers(cmd),
500        TradingCommand::ModifyOrders(cmd) => extract_batch_modify_orders_headers(cmd),
501        TradingCommand::CancelOrder(cmd) => extract_cancel_order_headers(cmd),
502        TradingCommand::CancelOrders(cmd) => extract_batch_cancel_orders_headers(cmd),
503        TradingCommand::CancelAllOrders(cmd) => extract_cancel_all_orders_headers(cmd),
504        TradingCommand::QueryOrder(cmd) => extract_query_order_headers(cmd),
505        TradingCommand::QueryAccount(cmd) => extract_query_account_headers(cmd),
506    }
507}
508
509// `send_data_command` reaches the bus tap with the wrapper's `TypeId`. The data engine
510// keys RPC request/response pairs by the request's `request_id`: the response's
511// `correlation_id` echoes that uuid back. Surfacing `request_id` as the captured entry's
512// `correlation_id` therefore lines a request entry up with its eventual response entry
513// under the same chain key. Subscribe / Unsubscribe variants carry an explicit
514// `correlation_id` field, which we forward as-is. DeFi variants are not yet wired through
515// header propagation.
516fn extract_data_command_headers(command: &DataCommand) -> Headers {
517    match command {
518        DataCommand::Request(cmd) => headers_from_fields(Some(*cmd.request_id()), None),
519        DataCommand::Subscribe(cmd) => headers_from_fields(cmd.correlation_id(), None),
520        DataCommand::Unsubscribe(cmd) => headers_from_fields(cmd.correlation_id(), None),
521        // `DataCommand` is `#[non_exhaustive]` and the defi variants do not yet carry
522        // header propagation; future variants drop through this arm with empty headers
523        // until their correlation field shape lands.
524        _ => Headers::empty(),
525    }
526}
527
528// Every `DataResponse` variant carries a required `correlation_id` that pairs the
529// response with its originating request; the captured entry mirrors that value.
530fn extract_data_response_headers(response: &DataResponse) -> Headers {
531    headers_from_fields(Some(*response.correlation_id()), None)
532}
533
534fn payload_type(tag: &str) -> PayloadType {
535    Ustr::from(tag)
536}
537
538fn encode_serde<T: Serialize>(value: &T) -> Result<Bytes, EncodeError> {
539    rmp_serde::to_vec_named(value)
540        .map(Bytes::from)
541        .map_err(|e| EncodeError::Serialize(e.to_string()))
542}
543
544/// Encodes a [`SubmitOrder`] command into canonical bytes plus its `client_order_id` index.
545///
546/// # Errors
547///
548/// Returns an error if MessagePack rejects the payload (a malformed value the type system should
549/// make unrepresentable; surfaced rather than swallowed because the audit contract refuses to drop
550/// captured commands).
551pub fn encode_submit_order(message: &SubmitOrder) -> Result<EncodedPayload, EncodeError> {
552    let payload = encode_serde(message)?;
553    let index_keys = vec![IndexKey::new(
554        IndexKind::ClientOrderId,
555        message.client_order_id.to_string(),
556    )];
557    Ok(EncodedPayload::new(payload, index_keys))
558}
559
560/// Encodes an [`OrderFilled`] event into canonical bytes plus its `client_order_id` and
561/// `venue_order_id` indices.
562///
563/// # Errors
564///
565/// Returns an error if MessagePack rejects the payload.
566pub fn encode_order_filled(message: &OrderFilled) -> Result<EncodedPayload, EncodeError> {
567    let payload = encode_serde(message)?;
568    let index_keys = vec![
569        IndexKey::new(
570            IndexKind::ClientOrderId,
571            message.client_order_id.to_string(),
572        ),
573        IndexKey::new(IndexKind::VenueOrderId, message.venue_order_id.to_string()),
574    ];
575    Ok(EncodedPayload::new(payload, index_keys))
576}
577
578/// Encodes a [`TradingCommand`] envelope by dispatching on the variant.
579///
580/// The captured entry's `payload_type` matches the inner-variant tag (e.g. `SubmitOrder`
581/// rather than `TradingCommand`) so forensics scans pair with the same decoder as the
582/// bare-type capture path. The serialized payload is the inner variant; the wrapper enum
583/// is never written to disk.
584///
585/// # Errors
586///
587/// Returns an error if the inner encoder rejects the [`TradingCommand::SubmitOrder`] variant, or if
588/// MessagePack rejects the inner payload for another variant.
589pub fn encode_trading_command(command: &TradingCommand) -> Result<EncodedPayload, EncodeError> {
590    match command {
591        TradingCommand::SubmitOrder(cmd) => {
592            Ok(retag(encode_submit_order(cmd)?, PAYLOAD_TYPE_SUBMIT_ORDER))
593        }
594        TradingCommand::SubmitOrderList(cmd) => encode_submit_order_list(cmd),
595        TradingCommand::ModifyOrder(cmd) => encode_modify_order(cmd),
596        TradingCommand::ModifyOrders(cmd) => encode_batch_modify_orders(cmd),
597        TradingCommand::CancelOrder(cmd) => encode_cancel_order(cmd),
598        TradingCommand::CancelOrders(cmd) => encode_batch_cancel_orders(cmd),
599        TradingCommand::CancelAllOrders(cmd) => encode_cancel_all_orders(cmd),
600        TradingCommand::QueryOrder(cmd) => encode_query_order(cmd),
601        TradingCommand::QueryAccount(cmd) => encode_query_account(cmd),
602    }
603}
604
605/// Encodes an [`OrderEventAny`] envelope by dispatching on the inner variant.
606///
607/// The captured entry's `payload_type` matches the inner-variant tag (e.g. `OrderFilled`
608/// rather than `OrderEventAny`); the serialized payload is the inner variant.
609///
610/// # Errors
611///
612/// Returns an error if the inner encoder rejects the [`OrderEventAny::Filled`] variant, or if
613/// MessagePack rejects the inner payload for another variant.
614pub fn encode_order_event_any(event: &OrderEventAny) -> Result<EncodedPayload, EncodeError> {
615    match event {
616        OrderEventAny::Initialized(e) => encode_order_initialized(e),
617        OrderEventAny::Denied(e) => encode_order_denied(e),
618        OrderEventAny::Emulated(e) => encode_order_emulated(e),
619        OrderEventAny::Released(e) => encode_order_released(e),
620        OrderEventAny::Submitted(e) => encode_order_submitted(e),
621        OrderEventAny::Accepted(e) => encode_order_accepted(e),
622        OrderEventAny::Rejected(e) => encode_order_rejected(e),
623        OrderEventAny::Canceled(e) => encode_order_canceled(e),
624        OrderEventAny::Expired(e) => encode_order_expired(e),
625        OrderEventAny::Triggered(e) => encode_order_triggered(e),
626        OrderEventAny::PendingUpdate(e) => encode_order_pending_update(e),
627        OrderEventAny::PendingCancel(e) => encode_order_pending_cancel(e),
628        OrderEventAny::ModifyRejected(e) => encode_order_modify_rejected(e),
629        OrderEventAny::CancelRejected(e) => encode_order_cancel_rejected(e),
630        OrderEventAny::Updated(e) => encode_order_updated(e),
631        OrderEventAny::Filled(e) => Ok(retag(encode_order_filled(e)?, PAYLOAD_TYPE_ORDER_FILLED)),
632        OrderEventAny::FillVoided(e) => encode_order_fill_voided(e),
633    }
634}
635
636/// Encodes an [`ExecutionReport`] envelope by dispatching on the variant.
637///
638/// `send_execution_report` hands the bus tap an [`ExecutionReport`] wrapper, so the tap
639/// dispatches by the wrapper's [`std::any::TypeId`] and the inner variants never reach
640/// their bare-type encoders. The dispatcher unwraps each variant, encodes the inner type
641/// with its own index keys, and stamps the inner-variant tag so forensics scans see
642/// entries identical to a bare capture path.
643///
644/// The [`ExecutionReport::Order`] arm reuses [`encode_order_status_report`] because the
645/// bare-type encoder already exists; the remaining variants delegate to private inner
646/// encoders that index the report's identifiers individually.
647///
648/// # Errors
649///
650/// Returns an error if MessagePack rejects the inner payload.
651pub fn encode_execution_report(report: &ExecutionReport) -> Result<EncodedPayload, EncodeError> {
652    match report {
653        ExecutionReport::Order(r) => Ok(retag(
654            encode_order_status_report(r)?,
655            PAYLOAD_TYPE_ORDER_STATUS_REPORT,
656        )),
657        ExecutionReport::Fill(r) => encode_fill_report(r),
658        ExecutionReport::OrderWithFills(order, fills) => encode_order_with_fills(order, fills),
659        ExecutionReport::Position(r) => encode_position_status_report(r),
660        ExecutionReport::MassStatus(s) => encode_execution_mass_status(s),
661    }
662}
663
664/// Encodes a [`FillReport`] into canonical bytes plus its `venue_order_id` index and,
665/// when present, its `client_order_id` index.
666///
667/// # Errors
668///
669/// Returns an error if MessagePack rejects the payload.
670pub fn encode_fill_report(report: &FillReport) -> Result<EncodedPayload, EncodeError> {
671    let payload = encode_serde(report)?;
672    let mut index_keys = Vec::with_capacity(2);
673    index_keys.push(IndexKey::new(
674        IndexKind::VenueOrderId,
675        report.venue_order_id.to_string(),
676    ));
677
678    if let Some(client_order_id) = &report.client_order_id {
679        index_keys.push(IndexKey::new(
680            IndexKind::ClientOrderId,
681            client_order_id.to_string(),
682        ));
683    }
684
685    Ok(EncodedPayload::with_payload_type(
686        payload_type(PAYLOAD_TYPE_FILL_REPORT),
687        payload,
688        index_keys,
689    ))
690}
691
692/// Encodes a [`PositionStatusReport`] into canonical bytes with no sidecar indices.
693///
694/// `PositionStatusReport` carries only `AccountId`, `InstrumentId`, and `PositionId`;
695/// none of those have a matching [`IndexKind`] variant today. Capture with no sidecar
696/// indices so the entry is forensics-discoverable by sequential scan rather than
697/// synthesizing an index against an identifier the reader cannot query.
698///
699/// # Errors
700///
701/// Returns an error if MessagePack rejects the payload.
702pub fn encode_position_status_report(
703    report: &PositionStatusReport,
704) -> Result<EncodedPayload, EncodeError> {
705    let payload = encode_serde(report)?;
706    Ok(EncodedPayload::with_payload_type(
707        payload_type(PAYLOAD_TYPE_POSITION_STATUS_REPORT),
708        payload,
709        Vec::new(),
710    ))
711}
712
713fn encode_order_with_fills(
714    order: &OrderStatusReport,
715    fills: &[FillReport],
716) -> Result<EncodedPayload, EncodeError> {
717    #[derive(Serialize)]
718    struct OrderWithFillsRef<'a> {
719        order_report: &'a OrderStatusReport,
720        fill_reports: &'a [FillReport],
721    }
722
723    let payload = encode_serde(&OrderWithFillsRef {
724        order_report: order,
725        fill_reports: fills,
726    })?;
727    let mut index_keys = Vec::new();
728    let mut seen = HashSet::new();
729    push_unique_index_key(
730        &mut index_keys,
731        &mut seen,
732        IndexKind::VenueOrderId,
733        order.venue_order_id.to_string(),
734    );
735
736    if let Some(client_order_id) = &order.client_order_id {
737        push_unique_index_key(
738            &mut index_keys,
739            &mut seen,
740            IndexKind::ClientOrderId,
741            client_order_id.to_string(),
742        );
743    }
744
745    for fill in fills {
746        push_unique_index_key(
747            &mut index_keys,
748            &mut seen,
749            IndexKind::VenueOrderId,
750            fill.venue_order_id.to_string(),
751        );
752
753        if let Some(client_order_id) = &fill.client_order_id {
754            push_unique_index_key(
755                &mut index_keys,
756                &mut seen,
757                IndexKind::ClientOrderId,
758                client_order_id.to_string(),
759            );
760        }
761    }
762
763    Ok(EncodedPayload::with_payload_type(
764        payload_type(PAYLOAD_TYPE_ORDER_WITH_FILLS),
765        payload,
766        index_keys,
767    ))
768}
769
770fn encode_execution_mass_status(
771    status: &ExecutionMassStatus,
772) -> Result<EncodedPayload, EncodeError> {
773    let payload = encode_serde(status)?;
774    let mut index_keys = Vec::new();
775    let mut seen = HashSet::new();
776
777    let order_reports = status.order_reports();
778
779    for (venue_order_id, report) in &order_reports {
780        push_unique_index_key(
781            &mut index_keys,
782            &mut seen,
783            IndexKind::VenueOrderId,
784            venue_order_id.to_string(),
785        );
786
787        if let Some(client_order_id) = &report.client_order_id {
788            push_unique_index_key(
789                &mut index_keys,
790                &mut seen,
791                IndexKind::ClientOrderId,
792                client_order_id.to_string(),
793            );
794        }
795    }
796
797    let fill_reports = status.fill_reports();
798
799    for (venue_order_id, fills) in &fill_reports {
800        push_unique_index_key(
801            &mut index_keys,
802            &mut seen,
803            IndexKind::VenueOrderId,
804            venue_order_id.to_string(),
805        );
806
807        for fill in fills {
808            if let Some(client_order_id) = &fill.client_order_id {
809                push_unique_index_key(
810                    &mut index_keys,
811                    &mut seen,
812                    IndexKind::ClientOrderId,
813                    client_order_id.to_string(),
814                );
815            }
816        }
817    }
818    // PositionStatusReport identifiers are not indexable today, see
819    // `encode_position_status_report`.
820    Ok(EncodedPayload::with_payload_type(
821        payload_type(PAYLOAD_TYPE_EXECUTION_MASS_STATUS),
822        payload,
823        index_keys,
824    ))
825}
826
827fn push_unique_index_key(
828    index_keys: &mut Vec<IndexKey>,
829    seen: &mut HashSet<(IndexKind, String)>,
830    kind: IndexKind,
831    key: String,
832) {
833    if seen.insert((kind, key.clone())) {
834        index_keys.push(IndexKey::new(kind, key));
835    }
836}
837
838/// Encodes a [`PositionEvent`] envelope by dispatching on the variant.
839///
840/// `publish_position_event` hands the bus tap a [`PositionEvent`] wrapper, so the tap
841/// dispatches by the wrapper's [`std::any::TypeId`] and the inner variants never reach
842/// their bare-type encoders. The dispatcher unwraps each variant, encodes the inner
843/// struct, and stamps the inner-variant tag so forensics scans see entries identical
844/// to the bare-type capture path.
845///
846/// # Errors
847///
848/// Returns an error if MessagePack rejects the inner payload.
849pub fn encode_position_event(event: &PositionEvent) -> Result<EncodedPayload, EncodeError> {
850    match event {
851        PositionEvent::PositionOpened(e) => encode_position_opened(e),
852        PositionEvent::PositionChanged(e) => encode_position_changed(e),
853        PositionEvent::PositionClosed(e) => encode_position_closed(e),
854        PositionEvent::PositionAdjusted(e) => encode_position_adjusted(e),
855    }
856}
857
858fn encode_position_opened(event: &PositionOpened) -> Result<EncodedPayload, EncodeError> {
859    let payload = encode_serde(event)?;
860    let index_keys = vec![IndexKey::new(
861        IndexKind::ClientOrderId,
862        event.opening_order_id.to_string(),
863    )];
864    Ok(EncodedPayload::with_payload_type(
865        payload_type(PAYLOAD_TYPE_POSITION_OPENED),
866        payload,
867        index_keys,
868    ))
869}
870
871fn encode_position_changed(event: &PositionChanged) -> Result<EncodedPayload, EncodeError> {
872    let payload = encode_serde(event)?;
873    let index_keys = vec![IndexKey::new(
874        IndexKind::ClientOrderId,
875        event.opening_order_id.to_string(),
876    )];
877    Ok(EncodedPayload::with_payload_type(
878        payload_type(PAYLOAD_TYPE_POSITION_CHANGED),
879        payload,
880        index_keys,
881    ))
882}
883
884fn encode_position_closed(event: &PositionClosed) -> Result<EncodedPayload, EncodeError> {
885    let payload = encode_serde(event)?;
886    let mut index_keys = Vec::new();
887    let mut seen = HashSet::new();
888
889    push_unique_index_key(
890        &mut index_keys,
891        &mut seen,
892        IndexKind::ClientOrderId,
893        event.opening_order_id.to_string(),
894    );
895
896    // Opening and closing client_order_ids are distinct in normal operation; dedup
897    // guards the rare case where a single order both opens and closes the position.
898    if let Some(closing_order_id) = &event.closing_order_id {
899        push_unique_index_key(
900            &mut index_keys,
901            &mut seen,
902            IndexKind::ClientOrderId,
903            closing_order_id.to_string(),
904        );
905    }
906
907    Ok(EncodedPayload::with_payload_type(
908        payload_type(PAYLOAD_TYPE_POSITION_CLOSED),
909        payload,
910        index_keys,
911    ))
912}
913
914fn encode_position_adjusted(event: &PositionAdjusted) -> Result<EncodedPayload, EncodeError> {
915    // PositionAdjusted carries no client_order_id; identifiers are PositionId,
916    // AccountId, and InstrumentId, none of which have a matching IndexKind today.
917    let payload = encode_serde(event)?;
918    Ok(EncodedPayload::with_payload_type(
919        payload_type(PAYLOAD_TYPE_POSITION_ADJUSTED),
920        payload,
921        Vec::new(),
922    ))
923}
924
925fn encode_submit_order_list(cmd: &SubmitOrderList) -> Result<EncodedPayload, EncodeError> {
926    let payload = encode_serde(cmd)?;
927    let index_keys = cmd
928        .order_list
929        .client_order_ids
930        .iter()
931        .map(|cid| IndexKey::new(IndexKind::ClientOrderId, cid.to_string()))
932        .collect();
933    Ok(EncodedPayload::with_payload_type(
934        payload_type(PAYLOAD_TYPE_SUBMIT_ORDER_LIST),
935        payload,
936        index_keys,
937    ))
938}
939
940fn encode_modify_order(cmd: &ModifyOrder) -> Result<EncodedPayload, EncodeError> {
941    encode_with_order_ids(
942        cmd,
943        PAYLOAD_TYPE_MODIFY_ORDER,
944        cmd.client_order_id.to_string(),
945        cmd.venue_order_id.map(|v| v.to_string()),
946    )
947}
948
949fn encode_batch_modify_orders(cmd: &BatchModifyOrders) -> Result<EncodedPayload, EncodeError> {
950    let payload = encode_serde(cmd)?;
951    let mut index_keys = Vec::with_capacity(cmd.modifies.len() * 2);
952    for c in &cmd.modifies {
953        index_keys.push(IndexKey::new(
954            IndexKind::ClientOrderId,
955            c.client_order_id.to_string(),
956        ));
957
958        if let Some(venue) = c.venue_order_id {
959            index_keys.push(IndexKey::new(IndexKind::VenueOrderId, venue.to_string()));
960        }
961    }
962    Ok(EncodedPayload::with_payload_type(
963        payload_type(PAYLOAD_TYPE_BATCH_MODIFY_ORDERS),
964        payload,
965        index_keys,
966    ))
967}
968
969fn encode_cancel_order(cmd: &CancelOrder) -> Result<EncodedPayload, EncodeError> {
970    encode_with_order_ids(
971        cmd,
972        PAYLOAD_TYPE_CANCEL_ORDER,
973        cmd.client_order_id.to_string(),
974        cmd.venue_order_id.map(|v| v.to_string()),
975    )
976}
977
978fn encode_cancel_all_orders(cmd: &CancelAllOrders) -> Result<EncodedPayload, EncodeError> {
979    let payload = encode_serde(cmd)?;
980    Ok(EncodedPayload::with_payload_type(
981        payload_type(PAYLOAD_TYPE_CANCEL_ALL_ORDERS),
982        payload,
983        Vec::new(),
984    ))
985}
986
987fn encode_batch_cancel_orders(cmd: &BatchCancelOrders) -> Result<EncodedPayload, EncodeError> {
988    let payload = encode_serde(cmd)?;
989    let mut index_keys = Vec::with_capacity(cmd.cancels.len() * 2);
990    for c in &cmd.cancels {
991        index_keys.push(IndexKey::new(
992            IndexKind::ClientOrderId,
993            c.client_order_id.to_string(),
994        ));
995
996        if let Some(venue) = c.venue_order_id {
997            index_keys.push(IndexKey::new(IndexKind::VenueOrderId, venue.to_string()));
998        }
999    }
1000    Ok(EncodedPayload::with_payload_type(
1001        payload_type(PAYLOAD_TYPE_BATCH_CANCEL_ORDERS),
1002        payload,
1003        index_keys,
1004    ))
1005}
1006
1007fn encode_query_order(cmd: &QueryOrder) -> Result<EncodedPayload, EncodeError> {
1008    encode_with_order_ids(
1009        cmd,
1010        PAYLOAD_TYPE_QUERY_ORDER,
1011        cmd.client_order_id.to_string(),
1012        cmd.venue_order_id.map(|v| v.to_string()),
1013    )
1014}
1015
1016fn encode_query_account(cmd: &QueryAccount) -> Result<EncodedPayload, EncodeError> {
1017    let payload = encode_serde(cmd)?;
1018    Ok(EncodedPayload::with_payload_type(
1019        payload_type(PAYLOAD_TYPE_QUERY_ACCOUNT),
1020        payload,
1021        Vec::new(),
1022    ))
1023}
1024
1025fn encode_order_initialized(e: &OrderInitialized) -> Result<EncodedPayload, EncodeError> {
1026    encode_with_order_ids(
1027        e,
1028        PAYLOAD_TYPE_ORDER_INITIALIZED,
1029        e.client_order_id.to_string(),
1030        None,
1031    )
1032}
1033
1034fn encode_order_denied(e: &OrderDenied) -> Result<EncodedPayload, EncodeError> {
1035    encode_with_order_ids(
1036        e,
1037        PAYLOAD_TYPE_ORDER_DENIED,
1038        e.client_order_id.to_string(),
1039        None,
1040    )
1041}
1042
1043fn encode_order_emulated(e: &OrderEmulated) -> Result<EncodedPayload, EncodeError> {
1044    encode_with_order_ids(
1045        e,
1046        PAYLOAD_TYPE_ORDER_EMULATED,
1047        e.client_order_id.to_string(),
1048        None,
1049    )
1050}
1051
1052fn encode_order_released(e: &OrderReleased) -> Result<EncodedPayload, EncodeError> {
1053    encode_with_order_ids(
1054        e,
1055        PAYLOAD_TYPE_ORDER_RELEASED,
1056        e.client_order_id.to_string(),
1057        None,
1058    )
1059}
1060
1061fn encode_order_submitted(e: &OrderSubmitted) -> Result<EncodedPayload, EncodeError> {
1062    encode_with_order_ids(
1063        e,
1064        PAYLOAD_TYPE_ORDER_SUBMITTED,
1065        e.client_order_id.to_string(),
1066        None,
1067    )
1068}
1069
1070fn encode_order_accepted(e: &OrderAccepted) -> Result<EncodedPayload, EncodeError> {
1071    encode_with_order_ids(
1072        e,
1073        PAYLOAD_TYPE_ORDER_ACCEPTED,
1074        e.client_order_id.to_string(),
1075        Some(e.venue_order_id.to_string()),
1076    )
1077}
1078
1079fn encode_order_rejected(e: &OrderRejected) -> Result<EncodedPayload, EncodeError> {
1080    encode_with_order_ids(
1081        e,
1082        PAYLOAD_TYPE_ORDER_REJECTED,
1083        e.client_order_id.to_string(),
1084        None,
1085    )
1086}
1087
1088fn encode_order_canceled(e: &OrderCanceled) -> Result<EncodedPayload, EncodeError> {
1089    encode_with_order_ids(
1090        e,
1091        PAYLOAD_TYPE_ORDER_CANCELED,
1092        e.client_order_id.to_string(),
1093        e.venue_order_id.map(|v| v.to_string()),
1094    )
1095}
1096
1097fn encode_order_expired(e: &OrderExpired) -> Result<EncodedPayload, EncodeError> {
1098    encode_with_order_ids(
1099        e,
1100        PAYLOAD_TYPE_ORDER_EXPIRED,
1101        e.client_order_id.to_string(),
1102        e.venue_order_id.map(|v| v.to_string()),
1103    )
1104}
1105
1106fn encode_order_triggered(e: &OrderTriggered) -> Result<EncodedPayload, EncodeError> {
1107    encode_with_order_ids(
1108        e,
1109        PAYLOAD_TYPE_ORDER_TRIGGERED,
1110        e.client_order_id.to_string(),
1111        e.venue_order_id.map(|v| v.to_string()),
1112    )
1113}
1114
1115fn encode_order_pending_update(e: &OrderPendingUpdate) -> Result<EncodedPayload, EncodeError> {
1116    encode_with_order_ids(
1117        e,
1118        PAYLOAD_TYPE_ORDER_PENDING_UPDATE,
1119        e.client_order_id.to_string(),
1120        e.venue_order_id.map(|v| v.to_string()),
1121    )
1122}
1123
1124fn encode_order_pending_cancel(e: &OrderPendingCancel) -> Result<EncodedPayload, EncodeError> {
1125    encode_with_order_ids(
1126        e,
1127        PAYLOAD_TYPE_ORDER_PENDING_CANCEL,
1128        e.client_order_id.to_string(),
1129        e.venue_order_id.map(|v| v.to_string()),
1130    )
1131}
1132
1133fn encode_order_modify_rejected(e: &OrderModifyRejected) -> Result<EncodedPayload, EncodeError> {
1134    encode_with_order_ids(
1135        e,
1136        PAYLOAD_TYPE_ORDER_MODIFY_REJECTED,
1137        e.client_order_id.to_string(),
1138        e.venue_order_id.map(|v| v.to_string()),
1139    )
1140}
1141
1142fn encode_order_cancel_rejected(e: &OrderCancelRejected) -> Result<EncodedPayload, EncodeError> {
1143    encode_with_order_ids(
1144        e,
1145        PAYLOAD_TYPE_ORDER_CANCEL_REJECTED,
1146        e.client_order_id.to_string(),
1147        e.venue_order_id.map(|v| v.to_string()),
1148    )
1149}
1150
1151fn encode_order_updated(e: &OrderUpdated) -> Result<EncodedPayload, EncodeError> {
1152    encode_with_order_ids(
1153        e,
1154        PAYLOAD_TYPE_ORDER_UPDATED,
1155        e.client_order_id.to_string(),
1156        e.venue_order_id.map(|v| v.to_string()),
1157    )
1158}
1159
1160fn encode_order_fill_voided(e: &OrderFillVoided) -> Result<EncodedPayload, EncodeError> {
1161    encode_with_order_ids(
1162        e,
1163        PAYLOAD_TYPE_ORDER_FILL_VOIDED,
1164        e.client_order_id.to_string(),
1165        Some(e.venue_order_id.to_string()),
1166    )
1167}
1168
1169fn encode_with_order_ids<T: Serialize>(
1170    value: &T,
1171    tag: &str,
1172    client_order_id: String,
1173    venue_order_id: Option<String>,
1174) -> Result<EncodedPayload, EncodeError> {
1175    let payload = encode_serde(value)?;
1176    let mut index_keys = Vec::with_capacity(2);
1177    index_keys.push(IndexKey::new(IndexKind::ClientOrderId, client_order_id));
1178    if let Some(venue) = venue_order_id {
1179        index_keys.push(IndexKey::new(IndexKind::VenueOrderId, venue));
1180    }
1181    Ok(EncodedPayload::with_payload_type(
1182        payload_type(tag),
1183        payload,
1184        index_keys,
1185    ))
1186}
1187
1188fn retag(mut encoded: EncodedPayload, tag: &str) -> EncodedPayload {
1189    encoded.payload_type = Some(payload_type(tag));
1190    encoded
1191}
1192
1193/// Encodes an [`OrderStatusReport`] into canonical bytes plus its `venue_order_id` index
1194/// and, when present, its `client_order_id` index.
1195///
1196/// External orders observed only at the venue may not carry a `client_order_id`; the
1197/// index is omitted in that case so the secondary index never records an empty key.
1198///
1199/// # Errors
1200///
1201/// Returns an error if MessagePack rejects the payload.
1202pub fn encode_order_status_report(
1203    message: &OrderStatusReport,
1204) -> Result<EncodedPayload, EncodeError> {
1205    let payload = encode_serde(message)?;
1206    let mut index_keys = Vec::with_capacity(2);
1207    index_keys.push(IndexKey::new(
1208        IndexKind::VenueOrderId,
1209        message.venue_order_id.to_string(),
1210    ));
1211
1212    if let Some(client_order_id) = &message.client_order_id {
1213        index_keys.push(IndexKey::new(
1214            IndexKind::ClientOrderId,
1215            client_order_id.to_string(),
1216        ));
1217    }
1218    Ok(EncodedPayload::new(payload, index_keys))
1219}
1220
1221/// Encodes an [`AccountState`] into canonical bytes with no sidecar indices.
1222///
1223/// `AccountState` carries `AccountId` and `event_id` (UUID4); neither matches an
1224/// [`IndexKind`] variant today, so the encoder emits no sidecar keys and forensics
1225/// scans rely on sequential range over `seq`. This mirrors the [`PositionStatusReport`]
1226/// precedent.
1227///
1228/// # Errors
1229///
1230/// Returns an error if MessagePack rejects the payload.
1231pub fn encode_account_state(message: &AccountState) -> Result<EncodedPayload, EncodeError> {
1232    let payload = encode_serde(message)?;
1233    Ok(EncodedPayload::new(payload, Vec::new()))
1234}
1235
1236/// Encodes an [`InstrumentClose`] into canonical bytes with no sidecar indices.
1237///
1238/// Cache replay applies a captured contract expiration through the same settlement as the live
1239/// execution engine, so a restore keeps settled binary-option positions closed.
1240///
1241/// # Errors
1242///
1243/// Returns an error if MessagePack rejects the payload.
1244pub fn encode_instrument_close(message: &InstrumentClose) -> Result<EncodedPayload, EncodeError> {
1245    let payload = encode_serde(message)?;
1246    Ok(EncodedPayload::new(payload, Vec::new()))
1247}
1248
1249#[derive(Serialize)]
1250struct TimeEventPayload<'a> {
1251    name: &'a str,
1252    event_id: UUID4,
1253    ts_event: UnixNanos,
1254    ts_init: UnixNanos,
1255}
1256
1257/// Encodes a fired [`TimeEvent`] into canonical bytes with no sidecar indices.
1258///
1259/// Time events carry a callback boundary rather than a cache-state key. The event store
1260/// captures them for forensic ordering and deterministic replay inputs, while cache
1261/// replay leaves clock re-arming to the later clock lifecycle event workstream.
1262///
1263/// # Errors
1264///
1265/// Returns an error if MessagePack rejects the payload.
1266pub fn encode_time_event(event: &TimeEvent) -> Result<EncodedPayload, EncodeError> {
1267    let payload = TimeEventPayload {
1268        name: event.name.as_str(),
1269        event_id: event.event_id,
1270        ts_event: event.ts_event,
1271        ts_init: event.ts_init,
1272    };
1273    Ok(EncodedPayload::new(encode_serde(&payload)?, Vec::new()))
1274}
1275
1276/// Encodes a [`DataCommand`] envelope by dispatching on its command category.
1277///
1278/// `send_data_command` hands the bus tap a [`DataCommand`] wrapper, so the tap
1279/// dispatches by the wrapper's [`std::any::TypeId`] and the inner command category
1280/// never reaches a bare-type encoder. The dispatcher unwraps the category, encodes the
1281/// serializable inner enum (`RequestCommand`, `SubscribeCommand`, or
1282/// `UnsubscribeCommand`), and stamps that category's canonical `payload_type` tag.
1283///
1284/// Request IDs, command IDs, and correlation IDs do not have a matching [`IndexKind`]
1285/// today, so data commands emit no sidecar indices. Correlation is recovered from the
1286/// captured payload and, once header propagation lands, propagated headers.
1287///
1288/// # Errors
1289///
1290/// Returns an error if MessagePack rejects the inner payload, or when a future non-exhaustive
1291/// [`DataCommand`] variant has no encoder yet.
1292#[rustfmt::skip]
1293pub fn encode_data_command(command: &DataCommand) -> Result<EncodedPayload, EncodeError> {
1294    match command {
1295        DataCommand::Request(cmd) => encode_data_command_category(cmd, PAYLOAD_TYPE_REQUEST_COMMAND),
1296        DataCommand::Subscribe(cmd) => encode_data_command_category(cmd, PAYLOAD_TYPE_SUBSCRIBE_COMMAND),
1297        DataCommand::Unsubscribe(cmd) => encode_data_command_category(cmd, PAYLOAD_TYPE_UNSUBSCRIBE_COMMAND),
1298        #[cfg(feature = "defi")]
1299        DataCommand::DefiRequest(cmd) => encode_data_command_category(cmd, PAYLOAD_TYPE_DEFI_REQUEST_COMMAND),
1300        #[cfg(feature = "defi")]
1301        DataCommand::DefiSubscribe(cmd) => encode_data_command_category(cmd, PAYLOAD_TYPE_DEFI_SUBSCRIBE_COMMAND),
1302        #[cfg(feature = "defi")]
1303        DataCommand::DefiUnsubscribe(cmd) => encode_data_command_category(cmd, PAYLOAD_TYPE_DEFI_UNSUBSCRIBE_COMMAND),
1304        _ => Err(EncodeError::Serialize(
1305            "unsupported DataCommand variant".to_string(),
1306        )),
1307    }
1308}
1309
1310fn encode_data_command_category<T: Serialize>(
1311    command: &T,
1312    tag: &str,
1313) -> Result<EncodedPayload, EncodeError> {
1314    let payload = encode_serde(command)?;
1315    Ok(EncodedPayload::with_payload_type(
1316        payload_type(tag),
1317        payload,
1318        Vec::new(),
1319    ))
1320}
1321
1322/// Encodes a [`DataResponse`] envelope by dispatching on the variant.
1323///
1324/// `send_data_response` hands the bus tap a [`DataResponse`] wrapper, so the tap
1325/// dispatches by the wrapper's [`std::any::TypeId`] and the inner variants never reach
1326/// their bare-type encoders. The dispatcher unwraps each variant, encodes the inner
1327/// struct, and stamps the inner-variant tag so forensics scans see entries identical
1328/// to a bare-type capture path.
1329///
1330/// Each variant carries a `correlation_id` (UUID4) pairing the response with the
1331/// originating `RequestCommand::request_id`. [`IndexKind`] has no matching variant
1332/// today, so every variant emits zero sidecar indices, mirroring the
1333/// [`PositionStatusReport`] and [`AccountState`] precedents.
1334///
1335/// The [`DataResponse::Data`] and [`DataResponse::Book`] variants carry payloads that
1336/// are not directly serializable: [`CustomDataResponse`] holds an `Arc<dyn Any>` and
1337/// [`BookResponse`] holds a [`nautilus_model::orderbook::OrderBook`] without serde
1338/// derives. The dispatcher serializes the audit-relevant metadata for those two
1339/// variants via local borrowed wrapper structs (the `encode_order_with_fills`
1340/// precedent) and omits the opaque payload. `BookResponse` is state-affecting on
1341/// the data engine path (`handle_book_response` clones the book into the cache); a
1342/// follow-up that adds serde to `OrderBook`/`BookLadder` can replace the metadata
1343/// wrapper with full payload capture without changing the dispatcher contract.
1344///
1345/// # Errors
1346///
1347/// Returns an error if MessagePack rejects the inner payload.
1348pub fn encode_data_response(response: &DataResponse) -> Result<EncodedPayload, EncodeError> {
1349    match response {
1350        DataResponse::Data(resp) => encode_custom_data_response(resp),
1351        DataResponse::Instrument(resp) => encode_instrument_response(resp),
1352        DataResponse::Instruments(resp) => encode_instruments_response(resp),
1353        DataResponse::Book(resp) => encode_book_response(resp),
1354        DataResponse::BookDeltas(resp) => encode_book_deltas_response(resp),
1355        DataResponse::BookDepth(resp) => encode_book_depth_response(resp),
1356        DataResponse::Quotes(resp) => encode_quotes_response(resp),
1357        DataResponse::Trades(resp) => encode_trades_response(resp),
1358        DataResponse::FundingRates(resp) => encode_funding_rates_response(resp),
1359        DataResponse::OptionChainReferencePrice(resp) => {
1360            encode_option_chain_reference_price_response(resp)
1361        }
1362        DataResponse::Bars(resp) => encode_bars_response(resp),
1363    }
1364}
1365
1366#[derive(serde::Serialize, serde::Deserialize)]
1367pub(crate) struct ResponseEnvelopeCapture {
1368    pub(crate) correlation_id: UUID4,
1369    pub(crate) completed: bool,
1370    pub(crate) sub_correlation_id: Option<UUID4>,
1371    pub(crate) failure_reason: Option<String>,
1372    pub(crate) payload_type: Option<PayloadType>,
1373    pub(crate) payload: Option<Bytes>,
1374}
1375
1376fn encode_response_envelope(
1377    response: &DataResponseEnvelope,
1378) -> Result<EncodedPayload, EncodeError> {
1379    let payload = response.response().map(encode_data_response).transpose()?;
1380
1381    let capture = ResponseEnvelopeCapture {
1382        correlation_id: *response.correlation_id(),
1383        completed: response.completed(),
1384        sub_correlation_id: response.sub_correlation_id(),
1385        failure_reason: response.failure_reason().map(str::to_owned),
1386        payload_type: payload.as_ref().and_then(|value| value.payload_type),
1387        payload: payload.map(|value| value.payload),
1388    };
1389
1390    Ok(EncodedPayload::without_indices(encode_serde(&capture)?))
1391}
1392
1393fn encode_custom_data_response(
1394    response: &CustomDataResponse,
1395) -> Result<EncodedPayload, EncodeError> {
1396    // `data: Arc<dyn Any + Send + Sync>` is type-erased at the dispatcher; the
1397    // payload is captured via a metadata-only wrapper so the audit entry pairs
1398    // with the originating request without depending on per-registration
1399    // serializers for the inner Any payload.
1400    #[derive(Serialize)]
1401    struct CustomDataResponseRef<'a> {
1402        correlation_id: &'a UUID4,
1403        client_id: &'a ClientId,
1404        venue: &'a Option<Venue>,
1405        data_type: &'a DataType,
1406        start: &'a Option<UnixNanos>,
1407        end: &'a Option<UnixNanos>,
1408        ts_init: &'a UnixNanos,
1409        params: &'a Option<Params>,
1410    }
1411
1412    let payload = encode_serde(&CustomDataResponseRef {
1413        correlation_id: &response.correlation_id,
1414        client_id: &response.client_id,
1415        venue: &response.venue,
1416        data_type: &response.data_type,
1417        start: &response.start,
1418        end: &response.end,
1419        ts_init: &response.ts_init,
1420        params: &response.params,
1421    })?;
1422    Ok(EncodedPayload::with_payload_type(
1423        payload_type(PAYLOAD_TYPE_CUSTOM_DATA_RESPONSE),
1424        payload,
1425        Vec::new(),
1426    ))
1427}
1428
1429fn encode_instrument_response(
1430    response: &InstrumentResponse,
1431) -> Result<EncodedPayload, EncodeError> {
1432    let payload = encode_serde(response)?;
1433    Ok(EncodedPayload::with_payload_type(
1434        payload_type(PAYLOAD_TYPE_INSTRUMENT_RESPONSE),
1435        payload,
1436        Vec::new(),
1437    ))
1438}
1439
1440fn encode_instruments_response(
1441    response: &InstrumentsResponse,
1442) -> Result<EncodedPayload, EncodeError> {
1443    let payload = encode_serde(response)?;
1444    Ok(EncodedPayload::with_payload_type(
1445        payload_type(PAYLOAD_TYPE_INSTRUMENTS_RESPONSE),
1446        payload,
1447        Vec::new(),
1448    ))
1449}
1450
1451fn encode_book_response(response: &BookResponse) -> Result<EncodedPayload, EncodeError> {
1452    // `data: OrderBook` is not serde-derived today (BookLadder/BookLevel chain), and
1453    // the full book state is rarely the audit value at this level. Capture
1454    // response-level metadata via a borrowed wrapper so the entry pairs with the
1455    // originating request.
1456    #[derive(Serialize)]
1457    struct BookResponseRef<'a> {
1458        correlation_id: &'a UUID4,
1459        client_id: &'a ClientId,
1460        instrument_id: &'a InstrumentId,
1461        start: &'a Option<UnixNanos>,
1462        end: &'a Option<UnixNanos>,
1463        ts_init: &'a UnixNanos,
1464        params: &'a Option<Params>,
1465    }
1466
1467    let payload = encode_serde(&BookResponseRef {
1468        correlation_id: &response.correlation_id,
1469        client_id: &response.client_id,
1470        instrument_id: &response.instrument_id,
1471        start: &response.start,
1472        end: &response.end,
1473        ts_init: &response.ts_init,
1474        params: &response.params,
1475    })?;
1476    Ok(EncodedPayload::with_payload_type(
1477        payload_type(PAYLOAD_TYPE_BOOK_RESPONSE),
1478        payload,
1479        Vec::new(),
1480    ))
1481}
1482
1483fn encode_quotes_response(response: &QuotesResponse) -> Result<EncodedPayload, EncodeError> {
1484    let payload = encode_serde(response)?;
1485    Ok(EncodedPayload::with_payload_type(
1486        payload_type(PAYLOAD_TYPE_QUOTES_RESPONSE),
1487        payload,
1488        Vec::new(),
1489    ))
1490}
1491
1492fn encode_book_deltas_response(
1493    response: &BookDeltasResponse,
1494) -> Result<EncodedPayload, EncodeError> {
1495    let payload = encode_serde(response)?;
1496    Ok(EncodedPayload::with_payload_type(
1497        payload_type(PAYLOAD_TYPE_BOOK_DELTAS_RESPONSE),
1498        payload,
1499        Vec::new(),
1500    ))
1501}
1502
1503fn encode_book_depth_response(response: &BookDepthResponse) -> Result<EncodedPayload, EncodeError> {
1504    let payload = encode_serde(response)?;
1505    Ok(EncodedPayload::with_payload_type(
1506        payload_type(PAYLOAD_TYPE_BOOK_DEPTH_RESPONSE),
1507        payload,
1508        Vec::new(),
1509    ))
1510}
1511
1512fn encode_trades_response(response: &TradesResponse) -> Result<EncodedPayload, EncodeError> {
1513    let payload = encode_serde(response)?;
1514    Ok(EncodedPayload::with_payload_type(
1515        payload_type(PAYLOAD_TYPE_TRADES_RESPONSE),
1516        payload,
1517        Vec::new(),
1518    ))
1519}
1520
1521fn encode_funding_rates_response(
1522    response: &FundingRatesResponse,
1523) -> Result<EncodedPayload, EncodeError> {
1524    let payload = encode_serde(response)?;
1525    Ok(EncodedPayload::with_payload_type(
1526        payload_type(PAYLOAD_TYPE_FUNDING_RATES_RESPONSE),
1527        payload,
1528        Vec::new(),
1529    ))
1530}
1531
1532fn encode_option_chain_reference_price_response(
1533    response: &OptionChainReferencePriceResponse,
1534) -> Result<EncodedPayload, EncodeError> {
1535    let payload = encode_serde(response)?;
1536    Ok(EncodedPayload::with_payload_type(
1537        payload_type(PAYLOAD_TYPE_OPTION_CHAIN_REFERENCE_PRICE_RESPONSE),
1538        payload,
1539        Vec::new(),
1540    ))
1541}
1542
1543fn encode_bars_response(response: &BarsResponse) -> Result<EncodedPayload, EncodeError> {
1544    let payload = encode_serde(response)?;
1545    Ok(EncodedPayload::with_payload_type(
1546        payload_type(PAYLOAD_TYPE_BARS_RESPONSE),
1547        payload,
1548        Vec::new(),
1549    ))
1550}
1551
1552#[cfg(test)]
1553mod tests {
1554    use nautilus_common::messages::data::{
1555        RequestCommand, RequestQuotes, SubscribeCommand, SubscribeQuotes, UnsubscribeCommand,
1556        UnsubscribeQuotes,
1557    };
1558    #[cfg(feature = "defi")]
1559    use nautilus_common::messages::defi::{
1560        DefiRequestCommand, DefiSubscribeCommand, DefiUnsubscribeCommand, RequestPoolSnapshot,
1561        SubscribeBlocks, UnsubscribeBlocks,
1562    };
1563    use nautilus_core::{DurationNanos, UUID4, UnixNanos};
1564    #[cfg(feature = "defi")]
1565    use nautilus_model::defi::Blockchain;
1566    use nautilus_model::{
1567        data::{Bar, BarType, stubs::stub_depth10},
1568        enums::{
1569            AccountType, BookType, InstrumentCloseType, LiquiditySide, OrderSide, OrderStatus,
1570            OrderType, PositionAdjustmentType, PositionSide, TimeInForce,
1571        },
1572        events::{
1573            PositionAdjusted, PositionChanged, PositionClosed, PositionOpened,
1574            order::spec::{
1575                OrderFillVoidedSpec, OrderFilledSpec, OrderInitializedSpec, OrderSubmittedSpec,
1576            },
1577        },
1578        identifiers::{
1579            AccountId, ClientId, ClientOrderId, InstrumentId, OptionSeriesId, OrderListId,
1580            PositionId, StrategyId, TradeId, TraderId, Venue, VenueOrderId,
1581        },
1582        instruments::{InstrumentAny, stubs::currency_pair_ethusdt},
1583        orderbook::OrderBook,
1584        orders::OrderList,
1585        reports::{ExecutionMassStatus, FillReport, PositionStatusReport},
1586        types::{AccountBalance, Currency, Money, Price, Quantity},
1587    };
1588    use rstest::rstest;
1589    use serde::Deserialize;
1590    use ustr::Ustr;
1591
1592    use super::*;
1593
1594    fn trader_id() -> TraderId {
1595        TraderId::from("TRADER-001")
1596    }
1597
1598    fn strategy_id() -> StrategyId {
1599        StrategyId::from("S-001")
1600    }
1601
1602    fn instrument_id() -> InstrumentId {
1603        InstrumentId::from("ETHUSDT-PERP.BINANCE")
1604    }
1605
1606    fn client_order_id() -> ClientOrderId {
1607        ClientOrderId::from("O-20260510-000001")
1608    }
1609
1610    fn venue_order_id() -> VenueOrderId {
1611        VenueOrderId::from("V-12345")
1612    }
1613
1614    fn make_submit_order() -> SubmitOrder {
1615        let order_init = OrderInitializedSpec::builder()
1616            .instrument_id(instrument_id())
1617            .client_order_id(client_order_id())
1618            .quantity(Quantity::from("1"))
1619            .time_in_force(TimeInForce::Gtc)
1620            .ts_event(UnixNanos::from(1))
1621            .ts_init(UnixNanos::from(2))
1622            .build();
1623        SubmitOrder::new(
1624            trader_id(),
1625            Some(ClientId::from("BINANCE")),
1626            strategy_id(),
1627            instrument_id(),
1628            client_order_id(),
1629            order_init,
1630            None,
1631            None,
1632            None,
1633            UUID4::new(),
1634            UnixNanos::from(3),
1635            None, // correlation_id
1636        )
1637    }
1638
1639    fn make_order_filled() -> OrderFilled {
1640        OrderFilledSpec::builder()
1641            .instrument_id(instrument_id())
1642            .client_order_id(client_order_id())
1643            .venue_order_id(venue_order_id())
1644            .account_id(AccountId::from("BINANCE-001"))
1645            .trade_id(TradeId::from("T-9999"))
1646            .last_qty(Quantity::from("1"))
1647            .last_px(Price::from("100.00"))
1648            .currency(Currency::USDT())
1649            .ts_event(UnixNanos::from(10))
1650            .ts_init(UnixNanos::from(11))
1651            .commission(Money::new(0.10, Currency::USDT()))
1652            .build()
1653    }
1654
1655    fn make_order_status_report() -> OrderStatusReport {
1656        OrderStatusReport::new(
1657            AccountId::from("BINANCE-001"),
1658            instrument_id(),
1659            Some(client_order_id()),
1660            venue_order_id(),
1661            OrderSide::Buy.into(),
1662            OrderType::Market,
1663            TimeInForce::Gtc,
1664            OrderStatus::Filled,
1665            Quantity::from("1"),
1666            Quantity::from("1"),
1667            UnixNanos::from(20),
1668            UnixNanos::from(21),
1669            UnixNanos::from(22),
1670            Some(UUID4::new()),
1671        )
1672    }
1673
1674    #[rstest]
1675    fn submit_order_encoder_emits_client_order_id_index() {
1676        let cmd = make_submit_order();
1677        let encoded = encode_submit_order(&cmd).expect("encode");
1678
1679        assert!(!encoded.payload.is_empty());
1680        assert_eq!(encoded.index_keys.len(), 1);
1681        assert_eq!(encoded.index_keys[0].kind, IndexKind::ClientOrderId);
1682        assert_eq!(encoded.index_keys[0].key, cmd.client_order_id.to_string());
1683    }
1684
1685    #[rstest]
1686    fn order_filled_encoder_emits_client_and_venue_order_id_indices() {
1687        let event = make_order_filled();
1688        let encoded = encode_order_filled(&event).expect("encode");
1689
1690        assert!(!encoded.payload.is_empty());
1691        assert_eq!(encoded.index_keys.len(), 2);
1692        assert_eq!(encoded.index_keys[0].kind, IndexKind::ClientOrderId);
1693        assert_eq!(encoded.index_keys[0].key, event.client_order_id.to_string());
1694        assert_eq!(encoded.index_keys[1].kind, IndexKind::VenueOrderId);
1695        assert_eq!(encoded.index_keys[1].key, event.venue_order_id.to_string());
1696    }
1697
1698    #[rstest]
1699    fn order_status_report_encoder_includes_client_order_id_when_present() {
1700        let report = make_order_status_report();
1701        let encoded = encode_order_status_report(&report).expect("encode");
1702
1703        assert_eq!(encoded.index_keys.len(), 2);
1704        assert_eq!(encoded.index_keys[0].kind, IndexKind::VenueOrderId);
1705        assert_eq!(encoded.index_keys[1].kind, IndexKind::ClientOrderId);
1706    }
1707
1708    #[rstest]
1709    fn order_status_report_encoder_omits_client_order_id_when_absent() {
1710        let mut report = make_order_status_report();
1711        report.client_order_id = None;
1712        let encoded = encode_order_status_report(&report).expect("encode");
1713
1714        assert_eq!(encoded.index_keys.len(), 1);
1715        assert_eq!(encoded.index_keys[0].kind, IndexKind::VenueOrderId);
1716    }
1717
1718    #[rstest]
1719    fn default_registry_covers_published_state_affecting_surface() {
1720        let registry = default_registry();
1721        let expected = [
1722            (
1723                "send_any_value(SubmitOrder) / bare SubmitOrder",
1724                registry.contains::<SubmitOrder>(),
1725            ),
1726            (
1727                "publish_order_event(OrderFilled) / bare OrderFilled",
1728                registry.contains::<OrderFilled>(),
1729            ),
1730            (
1731                "reconciliation.raw.order_status / OrderStatusReport",
1732                registry.contains::<OrderStatusReport>(),
1733            ),
1734            (
1735                "reconciliation.raw.fill / FillReport",
1736                registry.contains::<FillReport>(),
1737            ),
1738            (
1739                "reconciliation.raw.position / PositionStatusReport",
1740                registry.contains::<PositionStatusReport>(),
1741            ),
1742            (
1743                "send_trading_command / TradingCommand",
1744                registry.contains::<TradingCommand>(),
1745            ),
1746            (
1747                "publish_order_event / OrderEventAny",
1748                registry.contains::<OrderEventAny>(),
1749            ),
1750            (
1751                "send_execution_report / ExecutionReport",
1752                registry.contains::<ExecutionReport>(),
1753            ),
1754            (
1755                "publish_position_event / PositionEvent",
1756                registry.contains::<PositionEvent>(),
1757            ),
1758            (
1759                "publish_account_state and send_account_state / AccountState",
1760                registry.contains::<AccountState>(),
1761            ),
1762            (
1763                "data.close.* publish_any / InstrumentClose",
1764                registry.contains::<InstrumentClose>(),
1765            ),
1766            (
1767                "time event handler firing / TimeEvent",
1768                registry.contains::<TimeEvent>(),
1769            ),
1770            (
1771                "send_data_command / DataCommand",
1772                registry.contains::<DataCommand>(),
1773            ),
1774            (
1775                "send_data_response / DataResponse",
1776                registry.contains::<DataResponse>(),
1777            ),
1778            (
1779                "send_response_envelope / DataResponseEnvelope",
1780                registry.contains::<DataResponseEnvelope>(),
1781            ),
1782        ];
1783        let missing: Vec<&str> = expected
1784            .iter()
1785            .filter_map(|(name, registered)| (!*registered).then_some(*name))
1786            .collect();
1787
1788        assert!(
1789            missing.is_empty(),
1790            "missing default event-store encoder registrations for {missing:?}",
1791        );
1792        assert_eq!(
1793            registry.len(),
1794            expected.len(),
1795            "default registry must match the audited state-affecting surface",
1796        );
1797    }
1798
1799    #[rstest]
1800    fn submit_order_payload_round_trips_through_msgpack() {
1801        let cmd = make_submit_order();
1802        let encoded = encode_submit_order(&cmd).expect("encode");
1803
1804        let decoded: SubmitOrder = rmp_serde::from_slice(&encoded.payload).expect("decode");
1805        assert_eq!(decoded, cmd);
1806    }
1807
1808    #[rstest]
1809    fn default_registry_data_command_identity_dedupes_dispatch_hops() {
1810        // Production pushes every queued data command through two tapped sends
1811        // (queue, then drained execute); the identity must key both hops to the
1812        // same command.
1813        let registry = default_registry();
1814
1815        let request = make_request_command();
1816        let expected_request = *request.request_id();
1817        let subscribe = make_subscribe_command();
1818        let expected_subscribe = subscribe.command_id();
1819        let unsubscribe = make_unsubscribe_command();
1820        let expected_unsubscribe = unsubscribe.command_id();
1821
1822        let cases = [
1823            (DataCommand::Request(request), expected_request),
1824            (DataCommand::Subscribe(subscribe), expected_subscribe),
1825            (DataCommand::Unsubscribe(unsubscribe), expected_unsubscribe),
1826        ];
1827
1828        for (command, expected) in cases {
1829            assert_eq!(registry.identity_for_any(&command), Some(expected));
1830        }
1831    }
1832
1833    #[cfg(feature = "defi")]
1834    #[rstest]
1835    fn default_registry_defi_data_command_identity_dedupes_dispatch_hops() {
1836        let registry = default_registry();
1837
1838        let request = make_defi_request_command();
1839        let expected_request = *request.request_id();
1840        let subscribe = make_defi_subscribe_command();
1841        let expected_subscribe = subscribe.command_id();
1842        let unsubscribe = make_defi_unsubscribe_command();
1843        let expected_unsubscribe = unsubscribe.command_id();
1844
1845        let cases = [
1846            (DataCommand::DefiRequest(request), expected_request),
1847            (DataCommand::DefiSubscribe(subscribe), expected_subscribe),
1848            (
1849                DataCommand::DefiUnsubscribe(unsubscribe),
1850                expected_unsubscribe,
1851            ),
1852        ];
1853
1854        for (command, expected) in cases {
1855            assert_eq!(registry.identity_for_any(&command), Some(expected));
1856        }
1857    }
1858
1859    #[rstest]
1860    fn order_filled_payload_round_trips_through_msgpack() {
1861        let event = make_order_filled();
1862        let encoded = encode_order_filled(&event).expect("encode");
1863
1864        let decoded: OrderFilled = rmp_serde::from_slice(&encoded.payload).expect("decode");
1865        assert_eq!(decoded, event);
1866    }
1867
1868    #[rstest]
1869    fn order_status_report_payload_round_trips_through_msgpack() {
1870        let report = make_order_status_report();
1871        let encoded = encode_order_status_report(&report).expect("encode");
1872
1873        let decoded: OrderStatusReport = rmp_serde::from_slice(&encoded.payload).expect("decode");
1874        assert_eq!(decoded, report);
1875    }
1876
1877    fn make_cancel_order() -> CancelOrder {
1878        CancelOrder::new(
1879            trader_id(),
1880            Some(ClientId::from("BINANCE")),
1881            strategy_id(),
1882            instrument_id(),
1883            client_order_id(),
1884            Some(venue_order_id()),
1885            UUID4::new(),
1886            UnixNanos::from(4),
1887            None,
1888            None, // correlation_id
1889        )
1890    }
1891
1892    fn make_query_account() -> QueryAccount {
1893        QueryAccount::new(
1894            trader_id(),
1895            Some(ClientId::from("BINANCE")),
1896            AccountId::from("BINANCE-001"),
1897            UUID4::new(),
1898            UnixNanos::from(5),
1899            None,
1900            None, // correlation_id
1901        )
1902    }
1903
1904    fn make_order_submitted() -> OrderSubmitted {
1905        OrderSubmittedSpec::builder()
1906            .instrument_id(instrument_id())
1907            .client_order_id(client_order_id())
1908            .account_id(AccountId::from("BINANCE-001"))
1909            .ts_event(UnixNanos::from(30))
1910            .ts_init(UnixNanos::from(31))
1911            .build()
1912    }
1913
1914    fn make_modify_order(venue: Option<VenueOrderId>) -> ModifyOrder {
1915        ModifyOrder::new(
1916            trader_id(),
1917            Some(ClientId::from("BINANCE")),
1918            strategy_id(),
1919            instrument_id(),
1920            client_order_id(),
1921            venue,
1922            Some(Quantity::from("2")),
1923            Some(Price::from("100.00")),
1924            None,
1925            UUID4::new(),
1926            UnixNanos::from(6),
1927            None,
1928            None, // correlation_id
1929        )
1930    }
1931
1932    fn make_batch_modify_orders(modifies: Vec<ModifyOrder>) -> BatchModifyOrders {
1933        BatchModifyOrders::new(
1934            trader_id(),
1935            Some(ClientId::from("BINANCE")),
1936            strategy_id(),
1937            instrument_id(),
1938            modifies,
1939            UUID4::new(),
1940            UnixNanos::from(7),
1941            None,
1942            None, // correlation_id
1943        )
1944    }
1945
1946    fn make_cancel_all_orders() -> CancelAllOrders {
1947        CancelAllOrders::new(
1948            trader_id(),
1949            Some(ClientId::from("BINANCE")),
1950            strategy_id(),
1951            instrument_id(),
1952            Some(OrderSide::Buy),
1953            UUID4::new(),
1954            UnixNanos::from(7),
1955            None,
1956            None, // correlation_id
1957        )
1958    }
1959
1960    fn make_query_order(venue: Option<VenueOrderId>) -> QueryOrder {
1961        QueryOrder::new(
1962            trader_id(),
1963            Some(ClientId::from("BINANCE")),
1964            strategy_id(),
1965            instrument_id(),
1966            client_order_id(),
1967            venue,
1968            UUID4::new(),
1969            UnixNanos::from(8),
1970            None,
1971            None, // correlation_id
1972        )
1973    }
1974
1975    fn make_batch_cancel_orders(cancels: Vec<CancelOrder>) -> BatchCancelOrders {
1976        BatchCancelOrders::new(
1977            trader_id(),
1978            Some(ClientId::from("BINANCE")),
1979            strategy_id(),
1980            instrument_id(),
1981            cancels,
1982            UUID4::new(),
1983            UnixNanos::from(9),
1984            None,
1985            None, // correlation_id
1986        )
1987    }
1988
1989    fn make_submit_order_list(client_order_ids: Vec<ClientOrderId>) -> SubmitOrderList {
1990        // OrderList::new asserts that order_inits' client_order_ids match the list's,
1991        // so we mint one OrderInitialized per id with the matching client_order_id.
1992        let order_inits: Vec<OrderInitialized> = client_order_ids
1993            .iter()
1994            .copied()
1995            .map(make_order_initialized_with_id)
1996            .collect();
1997        let order_list = OrderList::new(
1998            OrderListId::from("OL-1"),
1999            instrument_id(),
2000            strategy_id(),
2001            client_order_ids,
2002            UnixNanos::from(10),
2003        );
2004        SubmitOrderList::new(
2005            trader_id(),
2006            Some(ClientId::from("BINANCE")),
2007            strategy_id(),
2008            order_list,
2009            order_inits,
2010            None,
2011            None,
2012            None,
2013            UUID4::new(),
2014            UnixNanos::from(11),
2015            None, // correlation_id
2016        )
2017    }
2018
2019    fn make_order_initialized_with_id(client_order_id: ClientOrderId) -> OrderInitialized {
2020        OrderInitialized {
2021            client_order_id,
2022            ..OrderInitialized::default()
2023        }
2024    }
2025
2026    fn ev_initialized() -> OrderEventAny {
2027        OrderEventAny::Initialized(make_order_initialized_with_id(client_order_id()))
2028    }
2029
2030    fn ev_denied() -> OrderEventAny {
2031        OrderEventAny::Denied(OrderDenied {
2032            client_order_id: client_order_id(),
2033            ..Default::default()
2034        })
2035    }
2036
2037    fn ev_emulated() -> OrderEventAny {
2038        OrderEventAny::Emulated(OrderEmulated {
2039            client_order_id: client_order_id(),
2040            ..Default::default()
2041        })
2042    }
2043
2044    fn ev_released() -> OrderEventAny {
2045        OrderEventAny::Released(OrderReleased {
2046            client_order_id: client_order_id(),
2047            ..Default::default()
2048        })
2049    }
2050
2051    fn ev_submitted() -> OrderEventAny {
2052        OrderEventAny::Submitted(make_order_submitted())
2053    }
2054
2055    fn ev_accepted_with_venue(venue: VenueOrderId) -> OrderEventAny {
2056        OrderEventAny::Accepted(OrderAccepted {
2057            client_order_id: client_order_id(),
2058            venue_order_id: venue,
2059            ..Default::default()
2060        })
2061    }
2062
2063    fn ev_rejected() -> OrderEventAny {
2064        OrderEventAny::Rejected(OrderRejected {
2065            client_order_id: client_order_id(),
2066            ..Default::default()
2067        })
2068    }
2069
2070    fn ev_canceled(venue: Option<VenueOrderId>) -> OrderEventAny {
2071        OrderEventAny::Canceled(OrderCanceled {
2072            client_order_id: client_order_id(),
2073            venue_order_id: venue,
2074            ..Default::default()
2075        })
2076    }
2077
2078    fn ev_expired(venue: Option<VenueOrderId>) -> OrderEventAny {
2079        OrderEventAny::Expired(OrderExpired {
2080            client_order_id: client_order_id(),
2081            venue_order_id: venue,
2082            ..Default::default()
2083        })
2084    }
2085
2086    fn ev_triggered(venue: Option<VenueOrderId>) -> OrderEventAny {
2087        OrderEventAny::Triggered(OrderTriggered {
2088            client_order_id: client_order_id(),
2089            venue_order_id: venue,
2090            ..Default::default()
2091        })
2092    }
2093
2094    fn ev_pending_update(venue: Option<VenueOrderId>) -> OrderEventAny {
2095        OrderEventAny::PendingUpdate(OrderPendingUpdate {
2096            client_order_id: client_order_id(),
2097            venue_order_id: venue,
2098            ..Default::default()
2099        })
2100    }
2101
2102    fn ev_pending_cancel(venue: Option<VenueOrderId>) -> OrderEventAny {
2103        OrderEventAny::PendingCancel(OrderPendingCancel {
2104            client_order_id: client_order_id(),
2105            venue_order_id: venue,
2106            ..Default::default()
2107        })
2108    }
2109
2110    fn ev_modify_rejected(venue: Option<VenueOrderId>) -> OrderEventAny {
2111        OrderEventAny::ModifyRejected(OrderModifyRejected {
2112            client_order_id: client_order_id(),
2113            venue_order_id: venue,
2114            ..Default::default()
2115        })
2116    }
2117
2118    fn ev_cancel_rejected(venue: Option<VenueOrderId>) -> OrderEventAny {
2119        OrderEventAny::CancelRejected(OrderCancelRejected {
2120            client_order_id: client_order_id(),
2121            venue_order_id: venue,
2122            ..Default::default()
2123        })
2124    }
2125
2126    fn ev_updated(venue: Option<VenueOrderId>) -> OrderEventAny {
2127        OrderEventAny::Updated(OrderUpdated {
2128            client_order_id: client_order_id(),
2129            venue_order_id: venue,
2130            ..Default::default()
2131        })
2132    }
2133
2134    fn ev_filled() -> OrderEventAny {
2135        OrderEventAny::Filled(make_order_filled())
2136    }
2137
2138    fn ev_fill_voided() -> OrderEventAny {
2139        OrderEventAny::FillVoided(
2140            OrderFillVoidedSpec::builder()
2141                .client_order_id(client_order_id())
2142                .venue_order_id(venue_order_id())
2143                .build(),
2144        )
2145    }
2146
2147    #[rstest]
2148    fn trading_command_envelope_stamps_inner_submit_order_payload_type() {
2149        // TradingCommand reaches the bus tap as the wrapper TypeId; the dispatcher must
2150        // unwrap to SubmitOrder, produce the same bytes and indices as the bare-type
2151        // encoder, and stamp the inner payload_type so forensics scans pair the entry
2152        // with the SubmitOrder decoder.
2153        let cmd = make_submit_order();
2154        let bare = encode_submit_order(&cmd).expect("bare");
2155
2156        let envelope = TradingCommand::SubmitOrder(cmd);
2157        let wrapped = encode_trading_command(&envelope).expect("envelope");
2158
2159        assert_eq!(wrapped.payload, bare.payload);
2160        assert_eq!(wrapped.index_keys, bare.index_keys);
2161        assert_eq!(
2162            wrapped.payload_type.expect("override").as_str(),
2163            PAYLOAD_TYPE_SUBMIT_ORDER,
2164        );
2165    }
2166
2167    #[rstest]
2168    fn trading_command_cancel_order_envelope_emits_client_and_venue_indices() {
2169        let cancel = make_cancel_order();
2170        let envelope = TradingCommand::CancelOrder(cancel.clone());
2171        let wrapped = encode_trading_command(&envelope).expect("envelope");
2172
2173        assert_eq!(
2174            wrapped.payload_type.expect("override").as_str(),
2175            PAYLOAD_TYPE_CANCEL_ORDER,
2176        );
2177        assert_eq!(wrapped.index_keys.len(), 2);
2178        assert_eq!(wrapped.index_keys[0].kind, IndexKind::ClientOrderId);
2179        assert_eq!(
2180            wrapped.index_keys[0].key,
2181            cancel.client_order_id.to_string(),
2182        );
2183        assert_eq!(wrapped.index_keys[1].kind, IndexKind::VenueOrderId);
2184        assert_eq!(
2185            wrapped.index_keys[1].key,
2186            cancel.venue_order_id.expect("set").to_string(),
2187        );
2188
2189        let decoded: CancelOrder = rmp_serde::from_slice(&wrapped.payload).expect("decode");
2190        assert_eq!(decoded, cancel);
2191    }
2192
2193    #[rstest]
2194    fn trading_command_query_account_envelope_records_no_order_indices() {
2195        // QueryAccount carries no client_order_id or venue_order_id; the dispatcher
2196        // must not invent empty index keys.
2197        let envelope = TradingCommand::QueryAccount(make_query_account());
2198        let wrapped = encode_trading_command(&envelope).expect("envelope");
2199
2200        assert_eq!(
2201            wrapped.payload_type.expect("override").as_str(),
2202            PAYLOAD_TYPE_QUERY_ACCOUNT,
2203        );
2204        assert!(wrapped.index_keys.is_empty());
2205    }
2206
2207    #[rstest]
2208    fn order_event_any_envelope_stamps_inner_filled_payload_type() {
2209        let filled = make_order_filled();
2210        let bare = encode_order_filled(&filled).expect("bare");
2211
2212        let envelope = OrderEventAny::Filled(filled);
2213        let wrapped = encode_order_event_any(&envelope).expect("envelope");
2214
2215        assert_eq!(wrapped.payload, bare.payload);
2216        assert_eq!(wrapped.index_keys, bare.index_keys);
2217        assert_eq!(
2218            wrapped.payload_type.expect("override").as_str(),
2219            PAYLOAD_TYPE_ORDER_FILLED,
2220        );
2221    }
2222
2223    #[rstest]
2224    fn order_event_any_submitted_envelope_emits_client_order_id_index() {
2225        let submitted = make_order_submitted();
2226        let envelope = OrderEventAny::Submitted(submitted);
2227        let wrapped = encode_order_event_any(&envelope).expect("envelope");
2228
2229        assert_eq!(
2230            wrapped.payload_type.expect("override").as_str(),
2231            PAYLOAD_TYPE_ORDER_SUBMITTED,
2232        );
2233        assert_eq!(wrapped.index_keys.len(), 1);
2234        assert_eq!(wrapped.index_keys[0].kind, IndexKind::ClientOrderId);
2235        assert_eq!(
2236            wrapped.index_keys[0].key,
2237            submitted.client_order_id.to_string(),
2238        );
2239
2240        let decoded: OrderSubmitted = rmp_serde::from_slice(&wrapped.payload).expect("decode");
2241        assert_eq!(decoded, submitted);
2242    }
2243
2244    // Walks every TradingCommand variant and asserts the dispatcher stamps the inner-variant
2245    // payload_type tag and emits the expected number of index keys. Catches a swapped match
2246    // arm or a forgotten `with_payload_type` override that would otherwise fall back to the
2247    // wrapper sentinel tag.
2248    #[rstest]
2249    #[case::submit_order(
2250        TradingCommand::SubmitOrder(make_submit_order()),
2251        PAYLOAD_TYPE_SUBMIT_ORDER,
2252        1
2253    )]
2254    #[case::submit_order_list(
2255        TradingCommand::SubmitOrderList(make_submit_order_list(vec![
2256            ClientOrderId::from("O-A"),
2257            ClientOrderId::from("O-B"),
2258        ])),
2259        PAYLOAD_TYPE_SUBMIT_ORDER_LIST,
2260        2,
2261    )]
2262    #[case::modify_order(
2263        TradingCommand::ModifyOrder(make_modify_order(Some(venue_order_id()))),
2264        PAYLOAD_TYPE_MODIFY_ORDER,
2265        2
2266    )]
2267    #[case::batch_modify_orders(
2268        TradingCommand::ModifyOrders(make_batch_modify_orders(vec![
2269            make_modify_order(Some(venue_order_id())),
2270        ])),
2271        PAYLOAD_TYPE_BATCH_MODIFY_ORDERS,
2272        2,
2273    )]
2274    #[case::cancel_order(
2275        TradingCommand::CancelOrder(make_cancel_order()),
2276        PAYLOAD_TYPE_CANCEL_ORDER,
2277        2
2278    )]
2279    #[case::batch_cancel_orders(
2280        TradingCommand::CancelOrders(make_batch_cancel_orders(vec![make_cancel_order()])),
2281        PAYLOAD_TYPE_BATCH_CANCEL_ORDERS,
2282        2,
2283    )]
2284    #[case::cancel_all_orders(
2285        TradingCommand::CancelAllOrders(make_cancel_all_orders()),
2286        PAYLOAD_TYPE_CANCEL_ALL_ORDERS,
2287        0
2288    )]
2289    #[case::query_order(
2290        TradingCommand::QueryOrder(make_query_order(Some(venue_order_id()))),
2291        PAYLOAD_TYPE_QUERY_ORDER,
2292        2
2293    )]
2294    #[case::query_account(
2295        TradingCommand::QueryAccount(make_query_account()),
2296        PAYLOAD_TYPE_QUERY_ACCOUNT,
2297        0
2298    )]
2299    fn trading_command_envelope_stamps_inner_tag_for_every_variant(
2300        #[case] command: TradingCommand,
2301        #[case] expected_tag: &str,
2302        #[case] expected_index_count: usize,
2303    ) {
2304        let encoded = encode_trading_command(&command).expect("encode");
2305        let tag = encoded.payload_type.expect("override").as_str().to_string();
2306
2307        assert_eq!(tag, expected_tag);
2308        assert_ne!(
2309            tag, PAYLOAD_TYPE_TRADING_COMMAND,
2310            "wrapper fallback tag must never reach the writer",
2311        );
2312        assert_eq!(encoded.index_keys.len(), expected_index_count);
2313    }
2314
2315    // Walks every OrderEventAny variant. Builds each variant from `Default::default()`
2316    // (gated by the `test-support` feature on `nautilus-model`) with the test client_order_id
2317    // patched in, so the assertion can verify the index value alongside the tag.
2318    #[rstest]
2319    #[case::initialized(ev_initialized(), PAYLOAD_TYPE_ORDER_INITIALIZED, false)]
2320    #[case::denied(ev_denied(), PAYLOAD_TYPE_ORDER_DENIED, false)]
2321    #[case::emulated(ev_emulated(), PAYLOAD_TYPE_ORDER_EMULATED, false)]
2322    #[case::released(ev_released(), PAYLOAD_TYPE_ORDER_RELEASED, false)]
2323    #[case::submitted(ev_submitted(), PAYLOAD_TYPE_ORDER_SUBMITTED, false)]
2324    #[case::accepted(
2325        ev_accepted_with_venue(venue_order_id()),
2326        PAYLOAD_TYPE_ORDER_ACCEPTED,
2327        true
2328    )]
2329    #[case::rejected(ev_rejected(), PAYLOAD_TYPE_ORDER_REJECTED, false)]
2330    #[case::canceled(ev_canceled(Some(venue_order_id())), PAYLOAD_TYPE_ORDER_CANCELED, true)]
2331    #[case::expired(ev_expired(Some(venue_order_id())), PAYLOAD_TYPE_ORDER_EXPIRED, true)]
2332    #[case::triggered(
2333        ev_triggered(Some(venue_order_id())),
2334        PAYLOAD_TYPE_ORDER_TRIGGERED,
2335        true
2336    )]
2337    #[case::pending_update(
2338        ev_pending_update(Some(venue_order_id())),
2339        PAYLOAD_TYPE_ORDER_PENDING_UPDATE,
2340        true
2341    )]
2342    #[case::pending_cancel(
2343        ev_pending_cancel(Some(venue_order_id())),
2344        PAYLOAD_TYPE_ORDER_PENDING_CANCEL,
2345        true
2346    )]
2347    #[case::modify_rejected(
2348        ev_modify_rejected(Some(venue_order_id())),
2349        PAYLOAD_TYPE_ORDER_MODIFY_REJECTED,
2350        true
2351    )]
2352    #[case::cancel_rejected(
2353        ev_cancel_rejected(Some(venue_order_id())),
2354        PAYLOAD_TYPE_ORDER_CANCEL_REJECTED,
2355        true
2356    )]
2357    #[case::updated(ev_updated(Some(venue_order_id())), PAYLOAD_TYPE_ORDER_UPDATED, true)]
2358    #[case::filled(ev_filled(), PAYLOAD_TYPE_ORDER_FILLED, true)]
2359    #[case::fill_voided(ev_fill_voided(), PAYLOAD_TYPE_ORDER_FILL_VOIDED, true)]
2360    fn order_event_any_envelope_stamps_inner_tag_for_every_variant(
2361        #[case] event: OrderEventAny,
2362        #[case] expected_tag: &str,
2363        #[case] expects_venue_index: bool,
2364    ) {
2365        let encoded = encode_order_event_any(&event).expect("encode");
2366        let tag = encoded.payload_type.expect("override").as_str().to_string();
2367
2368        assert_eq!(tag, expected_tag);
2369        assert_ne!(
2370            tag, PAYLOAD_TYPE_ORDER_EVENT_ANY,
2371            "wrapper fallback tag must never reach the writer",
2372        );
2373        assert_eq!(
2374            encoded.index_keys[0].kind,
2375            IndexKind::ClientOrderId,
2376            "first index must always be ClientOrderId for every order event",
2377        );
2378        assert_eq!(encoded.index_keys[0].key, client_order_id().to_string(),);
2379        if expects_venue_index {
2380            assert_eq!(encoded.index_keys.len(), 2);
2381            assert_eq!(encoded.index_keys[1].kind, IndexKind::VenueOrderId);
2382        } else {
2383            assert_eq!(encoded.index_keys.len(), 1);
2384        }
2385    }
2386
2387    // Optional venue_order_id branch coverage for variants that carry one. None must skip
2388    // the VenueOrderId index entirely; Some must push it second.
2389    #[rstest]
2390    #[case::cancel_order_some(TradingCommand::CancelOrder(make_cancel_order()), 2)]
2391    #[case::cancel_order_none(
2392        TradingCommand::CancelOrder(CancelOrder {
2393            venue_order_id: None,
2394            ..make_cancel_order()
2395        }),
2396        1,
2397    )]
2398    #[case::modify_order_some(
2399        TradingCommand::ModifyOrder(make_modify_order(Some(venue_order_id()))),
2400        2
2401    )]
2402    #[case::modify_order_none(TradingCommand::ModifyOrder(make_modify_order(None)), 1)]
2403    #[case::batch_modify_orders(
2404        TradingCommand::ModifyOrders(make_batch_modify_orders(vec![
2405            make_modify_order(Some(venue_order_id())),
2406            make_modify_order(None),
2407        ])),
2408        3,
2409    )]
2410    #[case::query_order_some(
2411        TradingCommand::QueryOrder(make_query_order(Some(venue_order_id()))),
2412        2
2413    )]
2414    #[case::query_order_none(TradingCommand::QueryOrder(make_query_order(None)), 1)]
2415    fn trading_command_envelope_index_count_matches_venue_optionality(
2416        #[case] command: TradingCommand,
2417        #[case] expected_index_count: usize,
2418    ) {
2419        let encoded = encode_trading_command(&command).expect("encode");
2420        assert_eq!(encoded.index_keys.len(), expected_index_count);
2421        assert_eq!(encoded.index_keys[0].kind, IndexKind::ClientOrderId);
2422        if expected_index_count == 2 {
2423            assert_eq!(encoded.index_keys[1].kind, IndexKind::VenueOrderId);
2424        }
2425    }
2426
2427    #[rstest]
2428    #[case::canceled_some(ev_canceled(Some(venue_order_id())), 2)]
2429    #[case::canceled_none(ev_canceled(None), 1)]
2430    #[case::updated_some(ev_updated(Some(venue_order_id())), 2)]
2431    #[case::updated_none(ev_updated(None), 1)]
2432    #[case::pending_update_some(ev_pending_update(Some(venue_order_id())), 2)]
2433    #[case::pending_update_none(ev_pending_update(None), 1)]
2434    fn order_event_any_envelope_index_count_matches_venue_optionality(
2435        #[case] event: OrderEventAny,
2436        #[case] expected_index_count: usize,
2437    ) {
2438        let encoded = encode_order_event_any(&event).expect("encode");
2439        assert_eq!(encoded.index_keys.len(), expected_index_count);
2440    }
2441
2442    #[rstest]
2443    fn batch_cancel_orders_envelope_indexes_each_child_with_optional_venue() {
2444        // Two cancels: one with a venue_order_id (contributes 2 indices), one without
2445        // (contributes 1). The dispatcher must preserve both children's identifiers in
2446        // the same order they appear in the batch.
2447        let with_venue = make_cancel_order();
2448        let mut without_venue = make_cancel_order();
2449        without_venue.venue_order_id = None;
2450        without_venue.client_order_id = ClientOrderId::from("O-NOVENUE");
2451        let batch = make_batch_cancel_orders(vec![with_venue.clone(), without_venue.clone()]);
2452
2453        let encoded = encode_trading_command(&TradingCommand::CancelOrders(batch)).expect("encode");
2454
2455        assert_eq!(
2456            encoded.payload_type.expect("override").as_str(),
2457            PAYLOAD_TYPE_BATCH_CANCEL_ORDERS,
2458        );
2459        assert_eq!(encoded.index_keys.len(), 3);
2460        assert_eq!(encoded.index_keys[0].kind, IndexKind::ClientOrderId);
2461        assert_eq!(
2462            encoded.index_keys[0].key,
2463            with_venue.client_order_id.to_string(),
2464        );
2465        assert_eq!(encoded.index_keys[1].kind, IndexKind::VenueOrderId);
2466        assert_eq!(
2467            encoded.index_keys[1].key,
2468            with_venue.venue_order_id.expect("set").to_string(),
2469        );
2470        assert_eq!(encoded.index_keys[2].kind, IndexKind::ClientOrderId);
2471        assert_eq!(
2472            encoded.index_keys[2].key,
2473            without_venue.client_order_id.to_string(),
2474        );
2475    }
2476
2477    #[rstest]
2478    fn batch_modify_orders_envelope_indexes_each_child_with_optional_venue() {
2479        let with_venue = make_modify_order(Some(venue_order_id()));
2480        let mut without_venue = make_modify_order(None);
2481        without_venue.client_order_id = ClientOrderId::from("O-NOVENUE");
2482        let batch = make_batch_modify_orders(vec![with_venue.clone(), without_venue.clone()]);
2483
2484        let encoded = encode_trading_command(&TradingCommand::ModifyOrders(batch)).expect("encode");
2485
2486        assert_eq!(
2487            encoded.payload_type.expect("override").as_str(),
2488            PAYLOAD_TYPE_BATCH_MODIFY_ORDERS,
2489        );
2490        assert_eq!(encoded.index_keys.len(), 3);
2491        assert_eq!(encoded.index_keys[0].kind, IndexKind::ClientOrderId);
2492        assert_eq!(
2493            encoded.index_keys[0].key,
2494            with_venue.client_order_id.to_string(),
2495        );
2496        assert_eq!(encoded.index_keys[1].kind, IndexKind::VenueOrderId);
2497        assert_eq!(
2498            encoded.index_keys[1].key,
2499            with_venue.venue_order_id.expect("set").to_string(),
2500        );
2501        assert_eq!(encoded.index_keys[2].kind, IndexKind::ClientOrderId);
2502        assert_eq!(
2503            encoded.index_keys[2].key,
2504            without_venue.client_order_id.to_string(),
2505        );
2506    }
2507
2508    #[rstest]
2509    fn submit_order_list_envelope_indexes_each_client_order_id() {
2510        // SubmitOrderList carries N orders; the dispatcher must emit a ClientOrderId
2511        // index per child so forensics can resolve any of the list's intents to the
2512        // captured seq.
2513        let ids = vec![
2514            ClientOrderId::from("O-LIST-1"),
2515            ClientOrderId::from("O-LIST-2"),
2516            ClientOrderId::from("O-LIST-3"),
2517        ];
2518        let cmd = make_submit_order_list(ids.clone());
2519        let encoded =
2520            encode_trading_command(&TradingCommand::SubmitOrderList(cmd)).expect("encode");
2521
2522        assert_eq!(
2523            encoded.payload_type.expect("override").as_str(),
2524            PAYLOAD_TYPE_SUBMIT_ORDER_LIST,
2525        );
2526        assert_eq!(encoded.index_keys.len(), ids.len());
2527        for (idx, expected_id) in ids.iter().enumerate() {
2528            assert_eq!(encoded.index_keys[idx].kind, IndexKind::ClientOrderId);
2529            assert_eq!(encoded.index_keys[idx].key, expected_id.to_string());
2530        }
2531    }
2532
2533    fn make_fill_report() -> FillReport {
2534        FillReport::new(
2535            AccountId::from("BINANCE-001"),
2536            instrument_id(),
2537            venue_order_id(),
2538            TradeId::from("T-1111"),
2539            OrderSide::Buy,
2540            Quantity::from("1"),
2541            Price::from("100.00"),
2542            Money::new(0.10, Currency::USDT()),
2543            LiquiditySide::Taker,
2544            Some(client_order_id()),
2545            None,
2546            UnixNanos::from(40),
2547            UnixNanos::from(41),
2548            None,
2549        )
2550    }
2551
2552    fn make_position_status_report() -> PositionStatusReport {
2553        PositionStatusReport::new(
2554            AccountId::from("BINANCE-001"),
2555            instrument_id(),
2556            PositionSide::Long,
2557            Quantity::from("1"),
2558            UnixNanos::from(50),
2559            UnixNanos::from(51),
2560            None,
2561            Some(PositionId::from("P-001")),
2562            None,
2563        )
2564    }
2565
2566    fn make_execution_mass_status_with_reports() -> ExecutionMassStatus {
2567        let mut status = ExecutionMassStatus::new(
2568            ClientId::from("BINANCE"),
2569            AccountId::from("BINANCE-001"),
2570            Venue::from("BINANCE"),
2571            UnixNanos::from(60),
2572            None,
2573        );
2574        status.add_order_reports(vec![make_order_status_report()]);
2575        status.add_fill_reports(vec![make_fill_report()]);
2576        status.add_position_reports(vec![make_position_status_report()]);
2577        status
2578    }
2579
2580    #[rstest]
2581    fn execution_report_order_envelope_reuses_bare_status_encoder() {
2582        // ExecutionReport::Order maps onto the existing OrderStatusReport bare-type
2583        // encoder; the dispatcher must produce identical bytes and indices and stamp
2584        // the OrderStatusReport tag so forensics scans pair the entry with the same
2585        // decoder as the bare-type capture path.
2586        let report = make_order_status_report();
2587        let bare = encode_order_status_report(&report).expect("bare");
2588
2589        let envelope = ExecutionReport::Order(Box::new(report));
2590        let wrapped = encode_execution_report(&envelope).expect("envelope");
2591
2592        assert_eq!(wrapped.payload, bare.payload);
2593        assert_eq!(wrapped.index_keys, bare.index_keys);
2594        assert_eq!(
2595            wrapped.payload_type.expect("override").as_str(),
2596            PAYLOAD_TYPE_ORDER_STATUS_REPORT,
2597        );
2598    }
2599
2600    #[rstest]
2601    fn execution_report_fill_envelope_emits_venue_and_client_order_id_indices() {
2602        let fill = make_fill_report();
2603        let envelope = ExecutionReport::Fill(Box::new(fill.clone()));
2604        let encoded = encode_execution_report(&envelope).expect("encode");
2605
2606        assert_eq!(
2607            encoded.payload_type.expect("override").as_str(),
2608            PAYLOAD_TYPE_FILL_REPORT,
2609        );
2610        assert_eq!(encoded.index_keys.len(), 2);
2611        assert_eq!(encoded.index_keys[0].kind, IndexKind::VenueOrderId);
2612        assert_eq!(encoded.index_keys[0].key, fill.venue_order_id.to_string());
2613        assert_eq!(encoded.index_keys[1].kind, IndexKind::ClientOrderId);
2614        assert_eq!(
2615            encoded.index_keys[1].key,
2616            fill.client_order_id.expect("set").to_string(),
2617        );
2618
2619        let decoded: FillReport = rmp_serde::from_slice(&encoded.payload).expect("decode");
2620        assert_eq!(decoded, fill);
2621    }
2622
2623    #[rstest]
2624    fn execution_report_fill_envelope_omits_client_order_id_when_absent() {
2625        let mut fill = make_fill_report();
2626        fill.client_order_id = None;
2627        let envelope = ExecutionReport::Fill(Box::new(fill));
2628        let encoded = encode_execution_report(&envelope).expect("encode");
2629
2630        assert_eq!(encoded.index_keys.len(), 1);
2631        assert_eq!(encoded.index_keys[0].kind, IndexKind::VenueOrderId);
2632    }
2633
2634    #[rstest]
2635    fn execution_report_position_envelope_records_no_indices() {
2636        // PositionStatusReport identifiers (AccountId, InstrumentId, PositionId) have
2637        // no matching IndexKind today; the dispatcher must not invent sidecar indices
2638        // pointing at an identifier the reader cannot query.
2639        let position = make_position_status_report();
2640        let envelope = ExecutionReport::Position(Box::new(position.clone()));
2641        let encoded = encode_execution_report(&envelope).expect("encode");
2642
2643        assert_eq!(
2644            encoded.payload_type.expect("override").as_str(),
2645            PAYLOAD_TYPE_POSITION_STATUS_REPORT,
2646        );
2647        assert!(encoded.index_keys.is_empty());
2648
2649        let decoded: PositionStatusReport =
2650            rmp_serde::from_slice(&encoded.payload).expect("decode");
2651        assert_eq!(decoded, position);
2652    }
2653
2654    #[rstest]
2655    fn execution_report_order_with_fills_envelope_dedupes_shared_order_ids() {
2656        // The order and its fills semantically carry the same venue_order_id and
2657        // client_order_id, so the dispatcher must dedupe rather than emit a duplicate
2658        // (kind, key) pair the backend would silently drop.
2659        let order = make_order_status_report();
2660        let fills = vec![make_fill_report()];
2661        let envelope = ExecutionReport::OrderWithFills(Box::new(order.clone()), fills);
2662        let encoded = encode_execution_report(&envelope).expect("encode");
2663
2664        assert_eq!(
2665            encoded.payload_type.expect("override").as_str(),
2666            PAYLOAD_TYPE_ORDER_WITH_FILLS,
2667        );
2668        assert_eq!(encoded.index_keys.len(), 2);
2669        assert_eq!(encoded.index_keys[0].kind, IndexKind::VenueOrderId);
2670        assert_eq!(encoded.index_keys[0].key, order.venue_order_id.to_string());
2671        assert_eq!(encoded.index_keys[1].kind, IndexKind::ClientOrderId);
2672        assert_eq!(
2673            encoded.index_keys[1].key,
2674            order.client_order_id.expect("set").to_string(),
2675        );
2676    }
2677
2678    #[rstest]
2679    fn execution_report_order_with_fills_envelope_indexes_distinct_fill_ids() {
2680        // A bundled OrderWithFills can carry a fill whose client_order_id differs
2681        // from the order's (rare, but real: external orders observed via a fill
2682        // before the venue confirms the canonical id). The dispatcher must index
2683        // both client_order_ids so forensics can resolve either to the same seq.
2684        let order = make_order_status_report();
2685        let mut fill = make_fill_report();
2686        fill.client_order_id = Some(ClientOrderId::from("O-EXTRA-001"));
2687        let envelope = ExecutionReport::OrderWithFills(Box::new(order), vec![fill.clone()]);
2688        let encoded = encode_execution_report(&envelope).expect("encode");
2689
2690        assert_eq!(encoded.index_keys.len(), 3);
2691        assert_eq!(encoded.index_keys[2].kind, IndexKind::ClientOrderId);
2692        assert_eq!(
2693            encoded.index_keys[2].key,
2694            fill.client_order_id.expect("set").to_string(),
2695        );
2696    }
2697
2698    #[rstest]
2699    fn execution_report_order_with_fills_payload_round_trips() {
2700        #[derive(serde::Deserialize)]
2701        struct OrderWithFillsOwned {
2702            order_report: OrderStatusReport,
2703            fill_reports: Vec<FillReport>,
2704        }
2705
2706        let order = make_order_status_report();
2707        let fills = vec![make_fill_report()];
2708        let envelope = ExecutionReport::OrderWithFills(Box::new(order.clone()), fills.clone());
2709        let encoded = encode_execution_report(&envelope).expect("encode");
2710
2711        let decoded: OrderWithFillsOwned = rmp_serde::from_slice(&encoded.payload).expect("decode");
2712        assert_eq!(decoded.order_report, order);
2713        assert_eq!(decoded.fill_reports, fills);
2714    }
2715
2716    #[rstest]
2717    fn execution_report_mass_status_envelope_indexes_orders_and_fills() {
2718        let status = make_execution_mass_status_with_reports();
2719        let envelope = ExecutionReport::MassStatus(Box::new(status.clone()));
2720        let encoded = encode_execution_report(&envelope).expect("encode");
2721
2722        assert_eq!(
2723            encoded.payload_type.expect("override").as_str(),
2724            PAYLOAD_TYPE_EXECUTION_MASS_STATUS,
2725        );
2726        // One order with venue+client ids, one fill sharing both ids, one position
2727        // (unindexable). The dispatcher must dedupe the shared ids.
2728        assert_eq!(encoded.index_keys.len(), 2);
2729        assert_eq!(encoded.index_keys[0].kind, IndexKind::VenueOrderId);
2730        assert_eq!(encoded.index_keys[0].key, venue_order_id().to_string());
2731        assert_eq!(encoded.index_keys[1].kind, IndexKind::ClientOrderId);
2732        assert_eq!(encoded.index_keys[1].key, client_order_id().to_string());
2733
2734        let decoded: ExecutionMassStatus = rmp_serde::from_slice(&encoded.payload).expect("decode");
2735        assert_eq!(decoded, status);
2736    }
2737
2738    #[rstest]
2739    fn execution_report_mass_status_envelope_indexes_distinct_children() {
2740        // Two distinct orders + a fill for a third venue_order_id with its own
2741        // client_order_id. The dispatcher must record an index for each unique id
2742        // so forensics can resolve any child of the status report.
2743        let mut status = ExecutionMassStatus::new(
2744            ClientId::from("BINANCE"),
2745            AccountId::from("BINANCE-001"),
2746            Venue::from("BINANCE"),
2747            UnixNanos::from(60),
2748            None,
2749        );
2750        let order_a = make_order_status_report();
2751        let order_b = OrderStatusReport {
2752            client_order_id: Some(ClientOrderId::from("O-B")),
2753            venue_order_id: VenueOrderId::from("V-B"),
2754            ..make_order_status_report()
2755        };
2756        let fill_c = FillReport {
2757            venue_order_id: VenueOrderId::from("V-C"),
2758            client_order_id: Some(ClientOrderId::from("O-C")),
2759            ..make_fill_report()
2760        };
2761        status.add_order_reports(vec![order_a, order_b]);
2762        status.add_fill_reports(vec![fill_c]);
2763
2764        let encoded = encode_execution_report(&ExecutionReport::MassStatus(Box::new(status)))
2765            .expect("encode");
2766
2767        // Three venue_order_ids + three client_order_ids = 6 distinct keys
2768        assert_eq!(encoded.index_keys.len(), 6);
2769        let venue_keys: Vec<&str> = encoded
2770            .index_keys
2771            .iter()
2772            .filter(|k| k.kind == IndexKind::VenueOrderId)
2773            .map(|k| k.key.as_str())
2774            .collect();
2775        let client_keys: Vec<&str> = encoded
2776            .index_keys
2777            .iter()
2778            .filter(|k| k.kind == IndexKind::ClientOrderId)
2779            .map(|k| k.key.as_str())
2780            .collect();
2781        assert!(venue_keys.contains(&venue_order_id().to_string().as_str()));
2782        assert!(venue_keys.contains(&"V-B"));
2783        assert!(venue_keys.contains(&"V-C"));
2784        assert!(client_keys.contains(&client_order_id().to_string().as_str()));
2785        assert!(client_keys.contains(&"O-B"));
2786        assert!(client_keys.contains(&"O-C"));
2787    }
2788
2789    // Walks every ExecutionReport variant and asserts the dispatcher stamps the
2790    // inner-variant tag. Catches a swapped match arm or a forgotten override that
2791    // would otherwise fall back to the wrapper sentinel tag.
2792    #[rstest]
2793    #[case::order(
2794        ExecutionReport::Order(Box::new(make_order_status_report())),
2795        PAYLOAD_TYPE_ORDER_STATUS_REPORT
2796    )]
2797    #[case::fill(
2798        ExecutionReport::Fill(Box::new(make_fill_report())),
2799        PAYLOAD_TYPE_FILL_REPORT
2800    )]
2801    #[case::order_with_fills(
2802        ExecutionReport::OrderWithFills(
2803            Box::new(make_order_status_report()),
2804            vec![make_fill_report()],
2805        ),
2806        PAYLOAD_TYPE_ORDER_WITH_FILLS,
2807    )]
2808    #[case::position(
2809        ExecutionReport::Position(Box::new(make_position_status_report())),
2810        PAYLOAD_TYPE_POSITION_STATUS_REPORT
2811    )]
2812    #[case::mass_status(
2813        ExecutionReport::MassStatus(Box::new(make_execution_mass_status_with_reports())),
2814        PAYLOAD_TYPE_EXECUTION_MASS_STATUS
2815    )]
2816    fn execution_report_envelope_stamps_inner_tag_for_every_variant(
2817        #[case] report: ExecutionReport,
2818        #[case] expected_tag: &str,
2819    ) {
2820        let encoded = encode_execution_report(&report).expect("encode");
2821        let tag = encoded.payload_type.expect("override").as_str().to_string();
2822
2823        assert_eq!(tag, expected_tag);
2824        assert_ne!(
2825            tag, PAYLOAD_TYPE_EXECUTION_REPORT,
2826            "wrapper fallback tag must never reach the writer",
2827        );
2828    }
2829
2830    fn opening_order_id() -> ClientOrderId {
2831        ClientOrderId::from("O-OPEN-001")
2832    }
2833
2834    fn closing_order_id() -> ClientOrderId {
2835        ClientOrderId::from("O-CLOSE-001")
2836    }
2837
2838    fn position_id() -> PositionId {
2839        PositionId::from("P-001")
2840    }
2841
2842    fn make_position_opened() -> PositionOpened {
2843        PositionOpened {
2844            trader_id: trader_id(),
2845            strategy_id: strategy_id(),
2846            instrument_id: instrument_id(),
2847            position_id: position_id(),
2848            account_id: AccountId::from("BINANCE-001"),
2849            opening_order_id: opening_order_id(),
2850            entry: OrderSide::Buy,
2851            side: PositionSide::Long,
2852            signed_qty: 1.into(),
2853            quantity: Quantity::from("1"),
2854            last_qty: Quantity::from("1"),
2855            last_px: Price::from("100.00"),
2856            currency: Currency::USDT(),
2857            avg_px_open: 100.into(),
2858            realized_pnl: Some(Money::new(-0.1, Currency::USDT())),
2859            event_id: UUID4::new(),
2860            ts_event: UnixNanos::from(70),
2861            ts_init: UnixNanos::from(71),
2862        }
2863    }
2864
2865    fn make_position_changed() -> PositionChanged {
2866        PositionChanged {
2867            trader_id: trader_id(),
2868            strategy_id: strategy_id(),
2869            instrument_id: instrument_id(),
2870            position_id: position_id(),
2871            account_id: AccountId::from("BINANCE-001"),
2872            opening_order_id: opening_order_id(),
2873            entry: OrderSide::Buy,
2874            side: PositionSide::Long,
2875            signed_qty: 2.into(),
2876            quantity: Quantity::from("2"),
2877            peak_quantity: Quantity::from("2"),
2878            last_qty: Quantity::from("1"),
2879            last_px: Price::from("101.00"),
2880            currency: Currency::USDT(),
2881            avg_px_open: "100.5".parse().unwrap(),
2882            avg_px_close: None,
2883            realized_return: 0.0,
2884            realized_pnl: None,
2885            unrealized_pnl: Money::new(1.0, Currency::USDT()),
2886            event_id: UUID4::new(),
2887            ts_opened: UnixNanos::from(70),
2888            ts_event: UnixNanos::from(80),
2889            ts_init: UnixNanos::from(81),
2890        }
2891    }
2892
2893    fn make_position_closed() -> PositionClosed {
2894        PositionClosed {
2895            trader_id: trader_id(),
2896            strategy_id: strategy_id(),
2897            instrument_id: instrument_id(),
2898            position_id: position_id(),
2899            account_id: AccountId::from("BINANCE-001"),
2900            opening_order_id: opening_order_id(),
2901            closing_order_id: Some(closing_order_id()),
2902            entry: OrderSide::Buy,
2903            side: PositionSide::Flat,
2904            signed_qty: 0.into(),
2905            quantity: Quantity::from("0"),
2906            peak_quantity: Quantity::from("2"),
2907            last_qty: Quantity::from("2"),
2908            last_px: Price::from("102.00"),
2909            currency: Currency::USDT(),
2910            avg_px_open: "100.5".parse().unwrap(),
2911            avg_px_close: Some(102.into()),
2912            realized_return: 0.015,
2913            realized_pnl: Some(Money::new(3.0, Currency::USDT())),
2914            unrealized_pnl: Money::new(0.0, Currency::USDT()),
2915            duration: DurationNanos::from_hours(1),
2916            event_id: UUID4::new(),
2917            ts_opened: UnixNanos::from(70),
2918            ts_closed: Some(UnixNanos::from(90)),
2919            ts_event: UnixNanos::from(90),
2920            ts_init: UnixNanos::from(91),
2921        }
2922    }
2923
2924    fn make_position_adjusted() -> PositionAdjusted {
2925        PositionAdjusted::new(
2926            trader_id(),
2927            strategy_id(),
2928            instrument_id(),
2929            position_id(),
2930            AccountId::from("BINANCE-001"),
2931            PositionAdjustmentType::Commission,
2932            None,
2933            None,
2934            None,
2935            UUID4::new(),
2936            UnixNanos::from(100),
2937            UnixNanos::from(101),
2938        )
2939    }
2940
2941    #[rstest]
2942    fn position_event_opened_envelope_emits_opening_order_id_index() {
2943        let opened = make_position_opened();
2944        let envelope = PositionEvent::PositionOpened(opened.clone());
2945        let encoded = encode_position_event(&envelope).expect("encode");
2946
2947        assert_eq!(
2948            encoded.payload_type.expect("override").as_str(),
2949            PAYLOAD_TYPE_POSITION_OPENED,
2950        );
2951        assert_eq!(encoded.index_keys.len(), 1);
2952        assert_eq!(encoded.index_keys[0].kind, IndexKind::ClientOrderId);
2953        assert_eq!(
2954            encoded.index_keys[0].key,
2955            opened.opening_order_id.to_string(),
2956        );
2957
2958        let decoded: PositionOpened = rmp_serde::from_slice(&encoded.payload).expect("decode");
2959        assert_eq!(decoded, opened);
2960    }
2961
2962    #[rstest]
2963    fn position_event_changed_envelope_emits_opening_order_id_index() {
2964        let changed = make_position_changed();
2965        let envelope = PositionEvent::PositionChanged(changed.clone());
2966        let encoded = encode_position_event(&envelope).expect("encode");
2967
2968        assert_eq!(
2969            encoded.payload_type.expect("override").as_str(),
2970            PAYLOAD_TYPE_POSITION_CHANGED,
2971        );
2972        assert_eq!(encoded.index_keys.len(), 1);
2973        assert_eq!(encoded.index_keys[0].kind, IndexKind::ClientOrderId);
2974        assert_eq!(
2975            encoded.index_keys[0].key,
2976            changed.opening_order_id.to_string(),
2977        );
2978
2979        let decoded: PositionChanged = rmp_serde::from_slice(&encoded.payload).expect("decode");
2980        assert_eq!(decoded, changed);
2981    }
2982
2983    #[rstest]
2984    fn position_event_closed_envelope_indexes_both_opening_and_closing_order_ids() {
2985        let closed = make_position_closed();
2986        let envelope = PositionEvent::PositionClosed(closed.clone());
2987        let encoded = encode_position_event(&envelope).expect("encode");
2988
2989        assert_eq!(
2990            encoded.payload_type.expect("override").as_str(),
2991            PAYLOAD_TYPE_POSITION_CLOSED,
2992        );
2993        assert_eq!(encoded.index_keys.len(), 2);
2994        assert_eq!(encoded.index_keys[0].kind, IndexKind::ClientOrderId);
2995        assert_eq!(
2996            encoded.index_keys[0].key,
2997            closed.opening_order_id.to_string(),
2998        );
2999        assert_eq!(encoded.index_keys[1].kind, IndexKind::ClientOrderId);
3000        assert_eq!(
3001            encoded.index_keys[1].key,
3002            closed.closing_order_id.expect("set").to_string(),
3003        );
3004
3005        let decoded: PositionClosed = rmp_serde::from_slice(&encoded.payload).expect("decode");
3006        assert_eq!(decoded, closed);
3007    }
3008
3009    #[rstest]
3010    fn position_event_closed_envelope_omits_closing_order_id_when_absent() {
3011        let mut closed = make_position_closed();
3012        closed.closing_order_id = None;
3013        let envelope = PositionEvent::PositionClosed(closed);
3014        let encoded = encode_position_event(&envelope).expect("encode");
3015
3016        assert_eq!(encoded.index_keys.len(), 1);
3017        assert_eq!(encoded.index_keys[0].kind, IndexKind::ClientOrderId);
3018        assert_eq!(encoded.index_keys[0].key, opening_order_id().to_string());
3019    }
3020
3021    #[rstest]
3022    fn position_event_closed_envelope_dedupes_when_open_and_close_match() {
3023        // Rare but real: a single order both opens and closes the position (e.g.,
3024        // reduce-only fills against a stale position). The dispatcher must dedupe
3025        // rather than insert the same (kind, key) twice.
3026        let mut closed = make_position_closed();
3027        closed.closing_order_id = Some(closed.opening_order_id);
3028        let envelope = PositionEvent::PositionClosed(closed);
3029        let encoded = encode_position_event(&envelope).expect("encode");
3030
3031        assert_eq!(encoded.index_keys.len(), 1);
3032        assert_eq!(encoded.index_keys[0].key, opening_order_id().to_string());
3033    }
3034
3035    #[rstest]
3036    fn position_event_adjusted_envelope_records_no_indices() {
3037        // PositionAdjusted has no ClientOrderId field; PositionId/AccountId/
3038        // InstrumentId have no matching IndexKind today, so the dispatcher must
3039        // not invent sidecar indices pointing at an identifier the reader cannot
3040        // query.
3041        let adjusted = make_position_adjusted();
3042        let envelope = PositionEvent::PositionAdjusted(adjusted);
3043        let encoded = encode_position_event(&envelope).expect("encode");
3044
3045        assert_eq!(
3046            encoded.payload_type.expect("override").as_str(),
3047            PAYLOAD_TYPE_POSITION_ADJUSTED,
3048        );
3049        assert!(encoded.index_keys.is_empty());
3050
3051        let decoded: PositionAdjusted = rmp_serde::from_slice(&encoded.payload).expect("decode");
3052        assert_eq!(decoded, adjusted);
3053    }
3054
3055    #[rstest]
3056    #[case::opened(
3057        PositionEvent::PositionOpened(make_position_opened()),
3058        PAYLOAD_TYPE_POSITION_OPENED
3059    )]
3060    #[case::changed(
3061        PositionEvent::PositionChanged(make_position_changed()),
3062        PAYLOAD_TYPE_POSITION_CHANGED
3063    )]
3064    #[case::closed(
3065        PositionEvent::PositionClosed(make_position_closed()),
3066        PAYLOAD_TYPE_POSITION_CLOSED
3067    )]
3068    #[case::adjusted(
3069        PositionEvent::PositionAdjusted(make_position_adjusted()),
3070        PAYLOAD_TYPE_POSITION_ADJUSTED
3071    )]
3072    fn position_event_envelope_stamps_inner_tag_for_every_variant(
3073        #[case] event: PositionEvent,
3074        #[case] expected_tag: &str,
3075    ) {
3076        let encoded = encode_position_event(&event).expect("encode");
3077        let tag = encoded.payload_type.expect("override").as_str().to_string();
3078
3079        assert_eq!(tag, expected_tag);
3080        assert_ne!(
3081            tag, PAYLOAD_TYPE_POSITION_EVENT,
3082            "wrapper fallback tag must never reach the writer",
3083        );
3084    }
3085
3086    fn make_account_state() -> AccountState {
3087        AccountState::new(
3088            AccountId::from("BINANCE-001"),
3089            AccountType::Cash,
3090            vec![AccountBalance::new(
3091                Money::from("1000000 USD"),
3092                Money::from("0 USD"),
3093                Money::from("1000000 USD"),
3094            )],
3095            vec![],
3096            true,
3097            UUID4::new(),
3098            UnixNanos::from(110),
3099            UnixNanos::from(111),
3100            Some(Currency::USD()),
3101        )
3102    }
3103
3104    #[rstest]
3105    fn account_state_encoder_records_no_indices() {
3106        // AccountState carries AccountId and event_id (UUID4); neither matches an
3107        // IndexKind variant today. The encoder must capture the payload without
3108        // synthesizing sidecar indices pointing at identifiers the reader cannot
3109        // query, mirroring the PositionStatusReport precedent.
3110        let state = make_account_state();
3111        let encoded = encode_account_state(&state).expect("encode");
3112
3113        assert!(!encoded.payload.is_empty());
3114        assert!(encoded.index_keys.is_empty());
3115        assert!(
3116            encoded.payload_type.is_none(),
3117            "bare-type encoders inherit the registry's registered tag",
3118        );
3119    }
3120
3121    #[rstest]
3122    fn account_state_payload_round_trips_through_msgpack() {
3123        let state = make_account_state();
3124        let encoded = encode_account_state(&state).expect("encode");
3125
3126        let decoded: AccountState = rmp_serde::from_slice(&encoded.payload).expect("decode");
3127        assert_eq!(decoded, state);
3128    }
3129
3130    #[rstest]
3131    fn account_state_registered_under_canonical_payload_type() {
3132        // The default registry must dispatch AccountState through encode_account_state
3133        // and stamp PAYLOAD_TYPE_ACCOUNT_STATE so both `publish_account_state` and
3134        // `send_account_state` capture under the same canonical tag.
3135        let registry = default_registry();
3136        let state = make_account_state();
3137        let (tag, encoded) = registry
3138            .encode(&state)
3139            .expect("encode")
3140            .expect("registered");
3141
3142        assert_eq!(tag.as_str(), PAYLOAD_TYPE_ACCOUNT_STATE);
3143        assert!(encoded.index_keys.is_empty());
3144    }
3145
3146    #[rstest]
3147    fn instrument_close_registered_under_canonical_payload_type() {
3148        let registry = default_registry();
3149
3150        let close = InstrumentClose::new(
3151            InstrumentId::from("TOKEN-YES.POLYMARKET"),
3152            Price::from("1.000"),
3153            InstrumentCloseType::ContractExpired,
3154            UnixNanos::from(300),
3155            UnixNanos::from(301),
3156        );
3157
3158        let (tag, encoded) = registry
3159            .encode(&close)
3160            .expect("encode")
3161            .expect("registered");
3162        let decoded: InstrumentClose = rmp_serde::from_slice(&encoded.payload).expect("decode");
3163
3164        assert_eq!(tag.as_str(), PAYLOAD_TYPE_INSTRUMENT_CLOSE);
3165        assert!(encoded.index_keys.is_empty());
3166        assert_eq!(decoded, close);
3167    }
3168
3169    #[derive(Debug, Deserialize, PartialEq, Eq)]
3170    struct DecodedTimeEventPayload {
3171        name: String,
3172        event_id: UUID4,
3173        ts_event: UnixNanos,
3174        ts_init: UnixNanos,
3175    }
3176
3177    #[rstest]
3178    fn time_event_payload_round_trips_through_msgpack() {
3179        let event = TimeEvent::new(
3180            Ustr::from("heartbeat"),
3181            UUID4::new(),
3182            UnixNanos::from(100),
3183            UnixNanos::from(99),
3184        );
3185        let encoded = encode_time_event(&event).expect("encode");
3186
3187        let decoded: DecodedTimeEventPayload =
3188            rmp_serde::from_slice(&encoded.payload).expect("decode");
3189        assert_eq!(
3190            decoded,
3191            DecodedTimeEventPayload {
3192                name: event.name.to_string(),
3193                event_id: event.event_id,
3194                ts_event: event.ts_event,
3195                ts_init: event.ts_init,
3196            },
3197        );
3198        assert!(encoded.index_keys.is_empty());
3199    }
3200
3201    #[rstest]
3202    fn time_event_registered_under_canonical_payload_type() {
3203        let registry = default_registry();
3204        let event = TimeEvent::new(
3205            Ustr::from("heartbeat"),
3206            UUID4::new(),
3207            UnixNanos::from(100),
3208            UnixNanos::from(99),
3209        );
3210        let (tag, encoded) = registry
3211            .encode(&event)
3212            .expect("encode")
3213            .expect("registered");
3214
3215        assert_eq!(tag.as_str(), PAYLOAD_TYPE_TIME_EVENT);
3216        assert!(encoded.index_keys.is_empty());
3217    }
3218
3219    #[rstest]
3220    fn data_command_request_envelope_stamps_request_command_payload_type() {
3221        // DataCommand reaches the bus tap as the wrapper TypeId. The dispatcher must
3222        // unwrap one level, encode RequestCommand, and stamp the category tag so the
3223        // reader pairs the bytes with the RequestCommand decoder.
3224        let request = make_request_command();
3225        let envelope = DataCommand::Request(request.clone());
3226        let encoded = encode_data_command(&envelope).expect("encode");
3227
3228        assert_eq!(
3229            encoded.payload_type.expect("override").as_str(),
3230            PAYLOAD_TYPE_REQUEST_COMMAND,
3231        );
3232        assert!(encoded.index_keys.is_empty());
3233
3234        let decoded: RequestCommand = rmp_serde::from_slice(&encoded.payload).expect("decode");
3235        match (decoded, request) {
3236            (RequestCommand::Quotes(decoded), RequestCommand::Quotes(expected)) => {
3237                assert_eq!(decoded.request_id, expected.request_id);
3238                assert_eq!(decoded.instrument_ids, expected.instrument_ids);
3239            }
3240            other => panic!("expected RequestCommand::Quotes round trip, was {other:?}"),
3241        }
3242    }
3243
3244    #[rstest]
3245    fn data_command_subscribe_envelope_stamps_subscribe_command_payload_type() {
3246        let subscribe = make_subscribe_command();
3247        let envelope = DataCommand::Subscribe(subscribe.clone());
3248        let encoded = encode_data_command(&envelope).expect("encode");
3249
3250        assert_eq!(
3251            encoded.payload_type.expect("override").as_str(),
3252            PAYLOAD_TYPE_SUBSCRIBE_COMMAND,
3253        );
3254        assert!(encoded.index_keys.is_empty());
3255
3256        let decoded: SubscribeCommand = rmp_serde::from_slice(&encoded.payload).expect("decode");
3257        match (decoded, subscribe) {
3258            (SubscribeCommand::Quotes(decoded), SubscribeCommand::Quotes(expected)) => {
3259                assert_eq!(decoded.command_id, expected.command_id);
3260                assert_eq!(decoded.instrument_ids, expected.instrument_ids);
3261            }
3262            other => panic!("expected SubscribeCommand::Quotes round trip, was {other:?}"),
3263        }
3264    }
3265
3266    #[rstest]
3267    fn data_command_unsubscribe_envelope_stamps_unsubscribe_command_payload_type() {
3268        let unsubscribe = make_unsubscribe_command();
3269        let envelope = DataCommand::Unsubscribe(unsubscribe.clone());
3270        let encoded = encode_data_command(&envelope).expect("encode");
3271
3272        assert_eq!(
3273            encoded.payload_type.expect("override").as_str(),
3274            PAYLOAD_TYPE_UNSUBSCRIBE_COMMAND,
3275        );
3276        assert!(encoded.index_keys.is_empty());
3277
3278        let decoded: UnsubscribeCommand = rmp_serde::from_slice(&encoded.payload).expect("decode");
3279        match (decoded, unsubscribe) {
3280            (UnsubscribeCommand::Quotes(decoded), UnsubscribeCommand::Quotes(expected)) => {
3281                assert_eq!(decoded.command_id, expected.command_id);
3282                assert_eq!(decoded.instrument_ids, expected.instrument_ids);
3283            }
3284            other => panic!("expected UnsubscribeCommand::Quotes round trip, was {other:?}"),
3285        }
3286    }
3287
3288    #[rstest]
3289    fn data_command_registered_under_category_payload_type() {
3290        // The default registry must dispatch DataCommand through encode_data_command and
3291        // stamp the inner category tag, not the wrapper sentinel tag.
3292        let registry = default_registry();
3293        let envelope = DataCommand::Subscribe(make_subscribe_command());
3294        let (tag, encoded) = registry
3295            .encode(&envelope)
3296            .expect("encode")
3297            .expect("registered");
3298
3299        assert_eq!(tag.as_str(), PAYLOAD_TYPE_SUBSCRIBE_COMMAND);
3300        assert!(encoded.index_keys.is_empty());
3301    }
3302
3303    #[cfg(feature = "defi")]
3304    #[rstest]
3305    fn data_command_defi_request_envelope_stamps_defi_request_command_payload_type() {
3306        let request = make_defi_request_command();
3307        let envelope = DataCommand::DefiRequest(request.clone());
3308        let encoded = encode_data_command(&envelope).expect("encode");
3309
3310        assert_eq!(
3311            encoded.payload_type.expect("override").as_str(),
3312            PAYLOAD_TYPE_DEFI_REQUEST_COMMAND,
3313        );
3314        assert!(encoded.index_keys.is_empty());
3315
3316        let decoded: DefiRequestCommand = rmp_serde::from_slice(&encoded.payload).expect("decode");
3317        match (decoded, request) {
3318            (
3319                DefiRequestCommand::PoolSnapshot(decoded),
3320                DefiRequestCommand::PoolSnapshot(expected),
3321            ) => {
3322                assert_eq!(decoded.request_id, expected.request_id);
3323                assert_eq!(decoded.instrument_id, expected.instrument_id);
3324                assert_eq!(decoded.client_id, expected.client_id);
3325                assert_eq!(decoded.ts_init, expected.ts_init);
3326            }
3327        }
3328    }
3329
3330    #[cfg(feature = "defi")]
3331    #[rstest]
3332    fn data_command_defi_subscribe_envelope_stamps_defi_subscribe_command_payload_type() {
3333        let subscribe = make_defi_subscribe_command();
3334        let envelope = DataCommand::DefiSubscribe(subscribe.clone());
3335        let encoded = encode_data_command(&envelope).expect("encode");
3336
3337        assert_eq!(
3338            encoded.payload_type.expect("override").as_str(),
3339            PAYLOAD_TYPE_DEFI_SUBSCRIBE_COMMAND,
3340        );
3341        assert!(encoded.index_keys.is_empty());
3342
3343        let decoded: DefiSubscribeCommand =
3344            rmp_serde::from_slice(&encoded.payload).expect("decode");
3345
3346        match (decoded, subscribe) {
3347            (DefiSubscribeCommand::Blocks(decoded), DefiSubscribeCommand::Blocks(expected)) => {
3348                assert_eq!(decoded.command_id, expected.command_id);
3349                assert_eq!(decoded.chain, expected.chain);
3350                assert_eq!(decoded.client_id, expected.client_id);
3351                assert_eq!(decoded.ts_init, expected.ts_init);
3352            }
3353            other => panic!("expected DefiSubscribeCommand::Blocks round trip, was {other:?}"),
3354        }
3355    }
3356
3357    #[cfg(feature = "defi")]
3358    #[rstest]
3359    fn data_command_defi_unsubscribe_envelope_stamps_defi_unsubscribe_command_payload_type() {
3360        let unsubscribe = make_defi_unsubscribe_command();
3361        let envelope = DataCommand::DefiUnsubscribe(unsubscribe.clone());
3362        let encoded = encode_data_command(&envelope).expect("encode");
3363
3364        assert_eq!(
3365            encoded.payload_type.expect("override").as_str(),
3366            PAYLOAD_TYPE_DEFI_UNSUBSCRIBE_COMMAND,
3367        );
3368        assert!(encoded.index_keys.is_empty());
3369
3370        let decoded: DefiUnsubscribeCommand =
3371            rmp_serde::from_slice(&encoded.payload).expect("decode");
3372
3373        match (decoded, unsubscribe) {
3374            (DefiUnsubscribeCommand::Blocks(decoded), DefiUnsubscribeCommand::Blocks(expected)) => {
3375                assert_eq!(decoded.command_id, expected.command_id);
3376                assert_eq!(decoded.chain, expected.chain);
3377                assert_eq!(decoded.client_id, expected.client_id);
3378                assert_eq!(decoded.ts_init, expected.ts_init);
3379            }
3380            other => panic!("expected DefiUnsubscribeCommand::Blocks round trip, was {other:?}"),
3381        }
3382    }
3383
3384    fn client_id() -> ClientId {
3385        ClientId::from("BINANCE")
3386    }
3387
3388    fn venue() -> Venue {
3389        Venue::from("BINANCE")
3390    }
3391
3392    fn correlation_id() -> UUID4 {
3393        UUID4::new()
3394    }
3395
3396    fn bar_type() -> BarType {
3397        BarType::from("ETHUSDT-PERP.BINANCE-1-MINUTE-LAST-EXTERNAL")
3398    }
3399
3400    fn data_type() -> DataType {
3401        DataType::new("Bar", None, None)
3402    }
3403
3404    fn make_request_command() -> RequestCommand {
3405        RequestCommand::Quotes(RequestQuotes::new(
3406            instrument_id(),
3407            None,
3408            None,
3409            None,
3410            Some(client_id()),
3411            correlation_id(),
3412            UnixNanos::from(197),
3413            None,
3414        ))
3415    }
3416
3417    fn make_subscribe_command() -> SubscribeCommand {
3418        SubscribeCommand::Quotes(SubscribeQuotes::new(
3419            instrument_id(),
3420            Some(client_id()),
3421            Some(venue()),
3422            correlation_id(),
3423            UnixNanos::from(198),
3424            Some(correlation_id()),
3425            None,
3426        ))
3427    }
3428
3429    fn make_unsubscribe_command() -> UnsubscribeCommand {
3430        UnsubscribeCommand::Quotes(UnsubscribeQuotes::new(
3431            instrument_id(),
3432            Some(client_id()),
3433            Some(venue()),
3434            correlation_id(),
3435            UnixNanos::from(199),
3436            Some(correlation_id()),
3437            None,
3438        ))
3439    }
3440
3441    #[cfg(feature = "defi")]
3442    fn make_defi_request_command() -> DefiRequestCommand {
3443        DefiRequestCommand::PoolSnapshot(RequestPoolSnapshot::new(
3444            instrument_id(),
3445            Some(client_id()),
3446            correlation_id(),
3447            UnixNanos::from(196),
3448            None,
3449        ))
3450    }
3451
3452    #[cfg(feature = "defi")]
3453    fn make_defi_subscribe_command() -> DefiSubscribeCommand {
3454        DefiSubscribeCommand::Blocks(SubscribeBlocks::new(
3455            Blockchain::Ethereum,
3456            Some(client_id()),
3457            correlation_id(),
3458            UnixNanos::from(195),
3459            None,
3460        ))
3461    }
3462
3463    #[cfg(feature = "defi")]
3464    fn make_defi_unsubscribe_command() -> DefiUnsubscribeCommand {
3465        DefiUnsubscribeCommand::Blocks(UnsubscribeBlocks::new(
3466            Blockchain::Ethereum,
3467            Some(client_id()),
3468            correlation_id(),
3469            UnixNanos::from(194),
3470            None,
3471        ))
3472    }
3473
3474    fn make_custom_data_response() -> CustomDataResponse {
3475        CustomDataResponse::new(
3476            correlation_id(),
3477            client_id(),
3478            Some(venue()),
3479            data_type(),
3480            (),
3481            None,
3482            None,
3483            UnixNanos::from(200),
3484            None,
3485        )
3486    }
3487
3488    fn make_instrument_response() -> InstrumentResponse {
3489        InstrumentResponse::new(
3490            correlation_id(),
3491            client_id(),
3492            instrument_id(),
3493            InstrumentAny::CurrencyPair(currency_pair_ethusdt()),
3494            None,
3495            None,
3496            UnixNanos::from(201),
3497            None,
3498        )
3499    }
3500
3501    fn make_instruments_response() -> InstrumentsResponse {
3502        InstrumentsResponse::new(
3503            correlation_id(),
3504            client_id(),
3505            venue(),
3506            vec![InstrumentAny::CurrencyPair(currency_pair_ethusdt())],
3507            None,
3508            None,
3509            UnixNanos::from(202),
3510            None,
3511        )
3512    }
3513
3514    fn make_book_response() -> BookResponse {
3515        BookResponse::new(
3516            correlation_id(),
3517            client_id(),
3518            instrument_id(),
3519            OrderBook::new(instrument_id(), BookType::L2_MBP),
3520            None,
3521            None,
3522            UnixNanos::from(203),
3523            None,
3524        )
3525    }
3526
3527    fn make_book_deltas_response() -> BookDeltasResponse {
3528        BookDeltasResponse::new(
3529            correlation_id(),
3530            client_id(),
3531            instrument_id(),
3532            Vec::new(),
3533            None,
3534            None,
3535            UnixNanos::from(204),
3536            None,
3537        )
3538    }
3539
3540    fn make_book_depth_response() -> BookDepthResponse {
3541        let mut depth = stub_depth10();
3542        depth.instrument_id = instrument_id();
3543        BookDepthResponse::new(
3544            correlation_id(),
3545            client_id(),
3546            instrument_id(),
3547            vec![depth],
3548            None,
3549            None,
3550            UnixNanos::from(205),
3551            None,
3552        )
3553    }
3554
3555    fn make_quotes_response() -> QuotesResponse {
3556        QuotesResponse::new(
3557            correlation_id(),
3558            client_id(),
3559            instrument_id(),
3560            Vec::new(),
3561            None,
3562            None,
3563            UnixNanos::from(206),
3564            None,
3565        )
3566    }
3567
3568    fn make_trades_response() -> TradesResponse {
3569        TradesResponse::new(
3570            correlation_id(),
3571            client_id(),
3572            instrument_id(),
3573            Vec::new(),
3574            None,
3575            None,
3576            UnixNanos::from(207),
3577            None,
3578        )
3579    }
3580
3581    fn make_funding_rates_response() -> FundingRatesResponse {
3582        FundingRatesResponse::new(
3583            correlation_id(),
3584            client_id(),
3585            instrument_id(),
3586            Vec::new(),
3587            None,
3588            None,
3589            UnixNanos::from(208),
3590            None,
3591        )
3592    }
3593
3594    fn make_option_chain_reference_price_response() -> OptionChainReferencePriceResponse {
3595        OptionChainReferencePriceResponse::new(
3596            correlation_id(),
3597            client_id(),
3598            OptionSeriesId::new_derived(
3599                venue(),
3600                Ustr::from("BTC"),
3601                Ustr::from("BTC"),
3602                UnixNanos::from(100),
3603            ),
3604            Some(Price::from("50123.45")),
3605            UnixNanos::from(209),
3606            None,
3607        )
3608    }
3609
3610    fn make_bars_response() -> BarsResponse {
3611        BarsResponse::new(
3612            correlation_id(),
3613            client_id(),
3614            bar_type(),
3615            Vec::<Bar>::new(),
3616            None,
3617            None,
3618            UnixNanos::from(210),
3619            None,
3620        )
3621    }
3622
3623    #[rstest]
3624    fn data_response_custom_data_envelope_stamps_inner_tag_with_no_indices() {
3625        // CustomDataResponse holds `data: Arc<dyn Any>`; the dispatcher captures
3626        // metadata via a borrowed wrapper. Correlation_id has no matching IndexKind
3627        // today, so the encoder emits no sidecar keys.
3628        let response = make_custom_data_response();
3629        let envelope = DataResponse::Data(response);
3630        let encoded = encode_data_response(&envelope).expect("encode");
3631
3632        assert_eq!(
3633            encoded.payload_type.expect("override").as_str(),
3634            PAYLOAD_TYPE_CUSTOM_DATA_RESPONSE,
3635        );
3636        assert!(encoded.index_keys.is_empty());
3637        assert!(!encoded.payload.is_empty());
3638    }
3639
3640    #[rstest]
3641    fn data_response_custom_data_payload_round_trips_metadata() {
3642        // The CustomDataResponseRef wrapper omits `data` (Arc<dyn Any>); the audit
3643        // entry captures correlation_id, client_id, venue, data_type, timing, and
3644        // params so forensics can pair the response with its request.
3645        #[derive(serde::Deserialize)]
3646        struct CustomDataResponseOwned {
3647            correlation_id: UUID4,
3648            client_id: ClientId,
3649            venue: Option<Venue>,
3650            ts_init: UnixNanos,
3651        }
3652
3653        let response = make_custom_data_response();
3654        let envelope = DataResponse::Data(response.clone());
3655        let encoded = encode_data_response(&envelope).expect("encode");
3656
3657        let decoded: CustomDataResponseOwned =
3658            rmp_serde::from_slice(&encoded.payload).expect("decode");
3659        assert_eq!(decoded.correlation_id, response.correlation_id);
3660        assert_eq!(decoded.client_id, response.client_id);
3661        assert_eq!(decoded.venue, response.venue);
3662        assert_eq!(decoded.ts_init, response.ts_init);
3663    }
3664
3665    #[rstest]
3666    fn data_response_book_envelope_stamps_inner_tag_with_no_indices() {
3667        let response = make_book_response();
3668        let envelope = DataResponse::Book(response);
3669        let encoded = encode_data_response(&envelope).expect("encode");
3670
3671        assert_eq!(
3672            encoded.payload_type.expect("override").as_str(),
3673            PAYLOAD_TYPE_BOOK_RESPONSE,
3674        );
3675        assert!(encoded.index_keys.is_empty());
3676        assert!(!encoded.payload.is_empty());
3677    }
3678
3679    #[rstest]
3680    fn data_response_book_payload_round_trips_metadata() {
3681        // BookResponseRef omits `data: OrderBook` (not serde-derived). The audit
3682        // entry captures the correlation and addressing metadata.
3683        #[derive(serde::Deserialize)]
3684        struct BookResponseOwned {
3685            correlation_id: UUID4,
3686            instrument_id: InstrumentId,
3687        }
3688
3689        let response = make_book_response();
3690        let envelope = DataResponse::Book(response.clone());
3691        let encoded = encode_data_response(&envelope).expect("encode");
3692
3693        let decoded: BookResponseOwned = rmp_serde::from_slice(&encoded.payload).expect("decode");
3694        assert_eq!(decoded.correlation_id, response.correlation_id);
3695        assert_eq!(decoded.instrument_id, response.instrument_id);
3696    }
3697
3698    #[rstest]
3699    fn data_response_book_depth_payload_round_trips() {
3700        let response = make_book_depth_response();
3701        let envelope = DataResponse::BookDepth(response.clone());
3702        let encoded = encode_data_response(&envelope).expect("encode");
3703
3704        assert_eq!(
3705            encoded.payload_type.expect("override").as_str(),
3706            PAYLOAD_TYPE_BOOK_DEPTH_RESPONSE,
3707        );
3708        assert!(encoded.index_keys.is_empty());
3709
3710        let decoded: BookDepthResponse = rmp_serde::from_slice(&encoded.payload).expect("decode");
3711        assert_eq!(decoded.correlation_id, response.correlation_id);
3712        assert_eq!(decoded.instrument_id, response.instrument_id);
3713        assert_eq!(decoded.data.as_ref(), response.data.as_ref());
3714    }
3715
3716    #[rstest]
3717    fn data_response_quotes_payload_round_trips() {
3718        let response = make_quotes_response();
3719        let envelope = DataResponse::Quotes(response.clone());
3720        let encoded = encode_data_response(&envelope).expect("encode");
3721
3722        assert_eq!(
3723            encoded.payload_type.expect("override").as_str(),
3724            PAYLOAD_TYPE_QUOTES_RESPONSE,
3725        );
3726        assert!(encoded.index_keys.is_empty());
3727
3728        let decoded: QuotesResponse = rmp_serde::from_slice(&encoded.payload).expect("decode");
3729        assert_eq!(decoded.correlation_id, response.correlation_id);
3730        assert_eq!(decoded.instrument_ids, response.instrument_ids);
3731        assert_eq!(decoded.data.as_ref(), response.data.as_ref());
3732    }
3733
3734    #[rstest]
3735    fn data_response_trades_payload_round_trips() {
3736        let response = make_trades_response();
3737        let envelope = DataResponse::Trades(response.clone());
3738        let encoded = encode_data_response(&envelope).expect("encode");
3739
3740        assert_eq!(
3741            encoded.payload_type.expect("override").as_str(),
3742            PAYLOAD_TYPE_TRADES_RESPONSE,
3743        );
3744
3745        let decoded: TradesResponse = rmp_serde::from_slice(&encoded.payload).expect("decode");
3746        assert_eq!(decoded.correlation_id, response.correlation_id);
3747    }
3748
3749    #[rstest]
3750    fn data_response_bars_payload_round_trips() {
3751        let response = make_bars_response();
3752        let envelope = DataResponse::Bars(response.clone());
3753        let encoded = encode_data_response(&envelope).expect("encode");
3754
3755        assert_eq!(
3756            encoded.payload_type.expect("override").as_str(),
3757            PAYLOAD_TYPE_BARS_RESPONSE,
3758        );
3759
3760        let decoded: BarsResponse = rmp_serde::from_slice(&encoded.payload).expect("decode");
3761        assert_eq!(decoded.correlation_id, response.correlation_id);
3762        assert_eq!(decoded.bar_type, response.bar_type);
3763    }
3764
3765    #[rstest]
3766    fn data_response_instrument_payload_round_trips() {
3767        let response = make_instrument_response();
3768        let envelope = DataResponse::Instrument(Box::new(response.clone()));
3769        let encoded = encode_data_response(&envelope).expect("encode");
3770
3771        assert_eq!(
3772            encoded.payload_type.expect("override").as_str(),
3773            PAYLOAD_TYPE_INSTRUMENT_RESPONSE,
3774        );
3775
3776        let decoded: InstrumentResponse = rmp_serde::from_slice(&encoded.payload).expect("decode");
3777        assert_eq!(decoded.correlation_id, response.correlation_id);
3778        assert_eq!(decoded.instrument_id, response.instrument_id);
3779    }
3780
3781    // Walks every DataResponse variant and asserts the dispatcher stamps the
3782    // inner-variant tag and emits zero sidecar indices. Catches a swapped match
3783    // arm or a forgotten `with_payload_type` override that would otherwise fall
3784    // back to the wrapper sentinel tag.
3785    #[rstest]
3786    #[case::data(
3787        DataResponse::Data(make_custom_data_response()),
3788        PAYLOAD_TYPE_CUSTOM_DATA_RESPONSE
3789    )]
3790    #[case::instrument(
3791        DataResponse::Instrument(Box::new(make_instrument_response())),
3792        PAYLOAD_TYPE_INSTRUMENT_RESPONSE
3793    )]
3794    #[case::instruments(
3795        DataResponse::Instruments(make_instruments_response()),
3796        PAYLOAD_TYPE_INSTRUMENTS_RESPONSE
3797    )]
3798    #[case::book(DataResponse::Book(make_book_response()), PAYLOAD_TYPE_BOOK_RESPONSE)]
3799    #[case::book_deltas(
3800        DataResponse::BookDeltas(make_book_deltas_response()),
3801        PAYLOAD_TYPE_BOOK_DELTAS_RESPONSE
3802    )]
3803    #[case::book_depth(
3804        DataResponse::BookDepth(make_book_depth_response()),
3805        PAYLOAD_TYPE_BOOK_DEPTH_RESPONSE
3806    )]
3807    #[case::quotes(
3808        DataResponse::Quotes(make_quotes_response()),
3809        PAYLOAD_TYPE_QUOTES_RESPONSE
3810    )]
3811    #[case::trades(
3812        DataResponse::Trades(make_trades_response()),
3813        PAYLOAD_TYPE_TRADES_RESPONSE
3814    )]
3815    #[case::funding_rates(
3816        DataResponse::FundingRates(make_funding_rates_response()),
3817        PAYLOAD_TYPE_FUNDING_RATES_RESPONSE
3818    )]
3819    #[case::option_chain_reference_price(
3820        DataResponse::OptionChainReferencePrice(make_option_chain_reference_price_response()),
3821        PAYLOAD_TYPE_OPTION_CHAIN_REFERENCE_PRICE_RESPONSE
3822    )]
3823    #[case::bars(DataResponse::Bars(make_bars_response()), PAYLOAD_TYPE_BARS_RESPONSE)]
3824    fn data_response_envelope_stamps_inner_tag_for_every_variant(
3825        #[case] response: DataResponse,
3826        #[case] expected_tag: &str,
3827    ) {
3828        let encoded = encode_data_response(&response).expect("encode");
3829        let tag = encoded.payload_type.expect("override").as_str().to_string();
3830
3831        assert_eq!(tag, expected_tag);
3832        assert_ne!(
3833            tag, PAYLOAD_TYPE_DATA_RESPONSE,
3834            "wrapper fallback tag must never reach the writer",
3835        );
3836        assert!(
3837            encoded.index_keys.is_empty(),
3838            "DataResponse correlation_id has no matching IndexKind today",
3839        );
3840    }
3841
3842    #[rstest]
3843    fn data_response_registered_under_canonical_payload_type() {
3844        // The default registry must dispatch DataResponse through encode_data_response
3845        // and stamp the inner-variant tag, so `send_data_response` captures under the
3846        // same canonical tag as a bare-type capture path.
3847        let registry = default_registry();
3848        let envelope = DataResponse::Quotes(make_quotes_response());
3849        let (tag, encoded) = registry
3850            .encode(&envelope)
3851            .expect("encode")
3852            .expect("registered");
3853
3854        assert_eq!(tag.as_str(), PAYLOAD_TYPE_QUOTES_RESPONSE);
3855        assert!(encoded.index_keys.is_empty());
3856    }
3857
3858    #[rstest]
3859    fn data_response_headers_extractor_surfaces_correlation_id() {
3860        // The data engine pairs RPC requests and responses by correlation_id (see
3861        // `crates/data/src/engine/mod.rs` `send_response`). Captured DataResponse entries
3862        // must carry that uuid in `Headers::correlation_id` so forensics can join a
3863        // captured response to its request.
3864        let registry = default_registry();
3865        let response = make_quotes_response();
3866        let expected = response.correlation_id;
3867        let envelope = DataResponse::Quotes(response);
3868
3869        let headers = registry
3870            .headers_for_any(&envelope as &dyn std::any::Any)
3871            .expect("registered");
3872        assert_eq!(headers.correlation_id, Some(expected));
3873        assert_eq!(headers.causation_id, None);
3874    }
3875
3876    #[rstest]
3877    fn data_command_request_headers_use_request_id_as_correlation() {
3878        // The request_id of an outbound RequestCommand IS the chain root: the eventual
3879        // DataResponse echoes the same uuid back as its correlation_id. Surfacing
3880        // request_id in `Headers::correlation_id` lines the request entry up with its
3881        // response entry under one chain key.
3882        let registry = default_registry();
3883        let request = make_quotes_request();
3884        let expected = request.request_id;
3885        let envelope = DataCommand::Request(RequestCommand::Quotes(request));
3886
3887        let headers = registry
3888            .headers_for_any(&envelope as &dyn std::any::Any)
3889            .expect("registered");
3890        assert_eq!(headers.correlation_id, Some(expected));
3891    }
3892
3893    #[rstest]
3894    fn data_command_subscribe_headers_surface_correlation_id() {
3895        // Subscribe variants carry an optional correlation_id field; the extractor
3896        // must forward whatever the inner command reports so captured subscribe
3897        // traffic joins its acknowledgements under one chain key.
3898        let registry = default_registry();
3899        let subscribe = make_subscribe_command();
3900        let expected = subscribe.correlation_id();
3901        let envelope = DataCommand::Subscribe(subscribe);
3902
3903        let headers = registry
3904            .headers_for_any(&envelope as &dyn std::any::Any)
3905            .expect("registered");
3906        assert_eq!(headers.correlation_id, expected);
3907    }
3908
3909    #[rstest]
3910    fn data_command_unsubscribe_headers_surface_correlation_id() {
3911        let registry = default_registry();
3912        let unsubscribe = make_unsubscribe_command();
3913        let expected = unsubscribe.correlation_id();
3914        let envelope = DataCommand::Unsubscribe(unsubscribe);
3915
3916        let headers = registry
3917            .headers_for_any(&envelope as &dyn std::any::Any)
3918            .expect("registered");
3919        assert_eq!(headers.correlation_id, expected);
3920    }
3921
3922    #[rstest]
3923    #[case::submit_order(trading_command_submit_order)]
3924    #[case::submit_order_list(trading_command_submit_order_list)]
3925    #[case::modify_order(trading_command_modify_order)]
3926    #[case::batch_modify_orders(trading_command_batch_modify_orders)]
3927    #[case::cancel_order(trading_command_cancel_order)]
3928    #[case::cancel_all_orders(trading_command_cancel_all_orders)]
3929    #[case::batch_cancel_orders(trading_command_batch_cancel_orders)]
3930    #[case::query_order(trading_command_query_order)]
3931    #[case::query_account(trading_command_query_account)]
3932    fn trading_command_extractor_surfaces_both_headers(
3933        #[case] builder: fn() -> (TradingCommand, UUID4, UUID4),
3934    ) {
3935        // The TradingCommand envelope dispatch must route every variant to the matching
3936        // per-type extractor and forward both correlation_id and causation_id intact.
3937        // A swap of args inside any extract_*_headers function or a misrouted wrapper arm
3938        // is caught by exercising each variant with distinct populated values.
3939        let (envelope, corr, caus) = builder();
3940        let registry = default_registry();
3941
3942        let headers = registry
3943            .headers_for_any(&envelope as &dyn std::any::Any)
3944            .expect("registered");
3945        assert_eq!(headers.correlation_id, Some(corr));
3946        assert_eq!(headers.causation_id, Some(caus));
3947    }
3948
3949    fn trading_command_submit_order() -> (TradingCommand, UUID4, UUID4) {
3950        let corr = UUID4::new();
3951        let caus = UUID4::new();
3952        let mut cmd = make_submit_order();
3953        cmd.correlation_id = Some(corr);
3954        cmd.causation_id = Some(caus);
3955        (TradingCommand::SubmitOrder(cmd), corr, caus)
3956    }
3957
3958    fn trading_command_submit_order_list() -> (TradingCommand, UUID4, UUID4) {
3959        let corr = UUID4::new();
3960        let caus = UUID4::new();
3961        let mut cmd = make_submit_order_list(vec![client_order_id()]);
3962        cmd.correlation_id = Some(corr);
3963        cmd.causation_id = Some(caus);
3964        (TradingCommand::SubmitOrderList(cmd), corr, caus)
3965    }
3966
3967    fn trading_command_modify_order() -> (TradingCommand, UUID4, UUID4) {
3968        let corr = UUID4::new();
3969        let caus = UUID4::new();
3970        let mut cmd = make_modify_order(Some(venue_order_id()));
3971        cmd.correlation_id = Some(corr);
3972        cmd.causation_id = Some(caus);
3973        (TradingCommand::ModifyOrder(cmd), corr, caus)
3974    }
3975
3976    fn trading_command_batch_modify_orders() -> (TradingCommand, UUID4, UUID4) {
3977        let corr = UUID4::new();
3978        let caus = UUID4::new();
3979        let mut cmd = make_batch_modify_orders(vec![make_modify_order(Some(venue_order_id()))]);
3980        cmd.correlation_id = Some(corr);
3981        cmd.causation_id = Some(caus);
3982        (TradingCommand::ModifyOrders(cmd), corr, caus)
3983    }
3984
3985    fn trading_command_cancel_order() -> (TradingCommand, UUID4, UUID4) {
3986        let corr = UUID4::new();
3987        let caus = UUID4::new();
3988        let mut cmd = make_cancel_order();
3989        cmd.correlation_id = Some(corr);
3990        cmd.causation_id = Some(caus);
3991        (TradingCommand::CancelOrder(cmd), corr, caus)
3992    }
3993
3994    fn trading_command_batch_cancel_orders() -> (TradingCommand, UUID4, UUID4) {
3995        let corr = UUID4::new();
3996        let caus = UUID4::new();
3997        let mut cmd = make_batch_cancel_orders(vec![make_cancel_order()]);
3998        cmd.correlation_id = Some(corr);
3999        cmd.causation_id = Some(caus);
4000        (TradingCommand::CancelOrders(cmd), corr, caus)
4001    }
4002
4003    fn trading_command_cancel_all_orders() -> (TradingCommand, UUID4, UUID4) {
4004        let corr = UUID4::new();
4005        let caus = UUID4::new();
4006        let mut cmd = make_cancel_all_orders();
4007        cmd.correlation_id = Some(corr);
4008        cmd.causation_id = Some(caus);
4009        (TradingCommand::CancelAllOrders(cmd), corr, caus)
4010    }
4011
4012    fn trading_command_query_order() -> (TradingCommand, UUID4, UUID4) {
4013        let corr = UUID4::new();
4014        let caus = UUID4::new();
4015        let mut cmd = make_query_order(Some(venue_order_id()));
4016        cmd.correlation_id = Some(corr);
4017        cmd.causation_id = Some(caus);
4018        (TradingCommand::QueryOrder(cmd), corr, caus)
4019    }
4020
4021    fn trading_command_query_account() -> (TradingCommand, UUID4, UUID4) {
4022        let corr = UUID4::new();
4023        let caus = UUID4::new();
4024        let mut cmd = make_query_account();
4025        cmd.correlation_id = Some(corr);
4026        cmd.causation_id = Some(caus);
4027        (TradingCommand::QueryAccount(cmd), corr, caus)
4028    }
4029
4030    #[rstest]
4031    #[case::data(data_response_data())]
4032    #[case::instrument(data_response_instrument())]
4033    #[case::instruments(data_response_instruments())]
4034    #[case::book(data_response_book())]
4035    #[case::book_deltas(data_response_book_deltas())]
4036    #[case::book_depth(data_response_book_depth())]
4037    #[case::quotes(data_response_quotes())]
4038    #[case::trades(data_response_trades())]
4039    #[case::funding_rates(data_response_funding_rates())]
4040    #[case::option_chain_reference_price(data_response_option_chain_reference_price())]
4041    #[case::bars(data_response_bars())]
4042    fn data_response_extractor_surfaces_correlation_id_for_every_variant(
4043        #[case] envelope_with_expected: (DataResponse, UUID4),
4044    ) {
4045        // Every DataResponse variant carries a required correlation_id paired with its
4046        // originating request; the extractor must forward that uuid intact regardless
4047        // of which variant is captured.
4048        let (envelope, expected) = envelope_with_expected;
4049        let registry = default_registry();
4050
4051        let headers = registry
4052            .headers_for_any(&envelope as &dyn std::any::Any)
4053            .expect("registered");
4054        assert_eq!(headers.correlation_id, Some(expected));
4055        assert_eq!(headers.causation_id, None);
4056    }
4057
4058    fn data_response_data() -> (DataResponse, UUID4) {
4059        let resp = make_custom_data_response();
4060        let expected = resp.correlation_id;
4061        (DataResponse::Data(resp), expected)
4062    }
4063
4064    fn data_response_instrument() -> (DataResponse, UUID4) {
4065        let resp = make_instrument_response();
4066        let expected = resp.correlation_id;
4067        (DataResponse::Instrument(Box::new(resp)), expected)
4068    }
4069
4070    fn data_response_instruments() -> (DataResponse, UUID4) {
4071        let resp = make_instruments_response();
4072        let expected = resp.correlation_id;
4073        (DataResponse::Instruments(resp), expected)
4074    }
4075
4076    fn data_response_book() -> (DataResponse, UUID4) {
4077        let resp = make_book_response();
4078        let expected = resp.correlation_id;
4079        (DataResponse::Book(resp), expected)
4080    }
4081
4082    fn data_response_book_deltas() -> (DataResponse, UUID4) {
4083        let resp = make_book_deltas_response();
4084        let expected = resp.correlation_id;
4085        (DataResponse::BookDeltas(resp), expected)
4086    }
4087
4088    fn data_response_book_depth() -> (DataResponse, UUID4) {
4089        let resp = make_book_depth_response();
4090        let expected = resp.correlation_id;
4091        (DataResponse::BookDepth(resp), expected)
4092    }
4093
4094    fn data_response_quotes() -> (DataResponse, UUID4) {
4095        let resp = make_quotes_response();
4096        let expected = resp.correlation_id;
4097        (DataResponse::Quotes(resp), expected)
4098    }
4099
4100    fn data_response_trades() -> (DataResponse, UUID4) {
4101        let resp = make_trades_response();
4102        let expected = resp.correlation_id;
4103        (DataResponse::Trades(resp), expected)
4104    }
4105
4106    fn data_response_funding_rates() -> (DataResponse, UUID4) {
4107        let resp = make_funding_rates_response();
4108        let expected = resp.correlation_id;
4109        (DataResponse::FundingRates(resp), expected)
4110    }
4111
4112    fn data_response_option_chain_reference_price() -> (DataResponse, UUID4) {
4113        let resp = make_option_chain_reference_price_response();
4114        let expected = resp.correlation_id;
4115        (DataResponse::OptionChainReferencePrice(resp), expected)
4116    }
4117
4118    fn data_response_bars() -> (DataResponse, UUID4) {
4119        let resp = make_bars_response();
4120        let expected = resp.correlation_id;
4121        (DataResponse::Bars(resp), expected)
4122    }
4123
4124    fn make_quotes_request() -> RequestQuotes {
4125        RequestQuotes {
4126            instrument_ids: vec![InstrumentId::from("EUR/USD.SIM")],
4127            start: None,
4128            end: None,
4129            limit: None,
4130            client_id: None,
4131            request_id: UUID4::new(),
4132            ts_init: UnixNanos::default(),
4133            params: None,
4134        }
4135    }
4136}