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