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