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