Skip to main content

nautilus_common/serialization/capnp/
trading.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//! Cap'n Proto serialization for trading commands.
17
18use nautilus_core::{Params, UUID4, UnixNanos};
19use nautilus_model::identifiers::{ClientId, InstrumentId, StrategyId, TraderId};
20use nautilus_serialization::{
21    base_capnp,
22    capnp::{ToCapnp, order_side_to_capnp},
23    trading_capnp,
24};
25
26use crate::messages::execution::{
27    BatchCancelOrders, CancelAllOrders, CancelOrder, ModifyOrder, QueryAccount, QueryOrder,
28    SubmitOrder, SubmitOrderList, TradingCommand,
29};
30
31/// Helper function to populate a `StringMap` builder from Params (`IndexMap<String, Value>`).
32fn populate_string_map(builder: base_capnp::string_map::Builder<'_>, params: &Params) {
33    let mut entries_builder = builder.init_entries(params.len() as u32);
34    for (i, (key, value)) in params.iter().enumerate() {
35        let mut entry_builder = entries_builder.reborrow().get(i as u32);
36        entry_builder.set_key(key.as_str());
37        let value_str = serde_json::to_string(value).unwrap_or_else(|_| value.to_string());
38        entry_builder.set_value(value_str.as_str());
39    }
40}
41
42/// Helper function to populate a `TradingCommandHeader` builder
43#[allow(clippy::too_many_arguments)]
44fn populate_trading_command_header(
45    mut builder: trading_capnp::trading_command_header::Builder<'_>,
46    trader_id: &TraderId,
47    client_id: Option<&ClientId>,
48    strategy_id: &StrategyId,
49    instrument_id: &InstrumentId,
50    command_id: &UUID4,
51    ts_init: UnixNanos,
52    correlation_id: Option<&UUID4>,
53    causation_id: Option<&UUID4>,
54) {
55    let trader_id_builder = builder.reborrow().init_trader_id();
56    trader_id.to_capnp(trader_id_builder);
57
58    if let Some(client_id) = client_id {
59        let client_id_builder = builder.reborrow().init_client_id();
60        client_id.to_capnp(client_id_builder);
61    }
62
63    let strategy_id_builder = builder.reborrow().init_strategy_id();
64    strategy_id.to_capnp(strategy_id_builder);
65
66    let instrument_id_builder = builder.reborrow().init_instrument_id();
67    instrument_id.to_capnp(instrument_id_builder);
68
69    let command_id_builder = builder.reborrow().init_command_id();
70    command_id.to_capnp(command_id_builder);
71
72    let mut ts_init_builder = builder.reborrow().init_ts_init();
73    ts_init_builder.set_value(*ts_init);
74
75    if let Some(correlation_id) = correlation_id {
76        let correlation_id_builder = builder.reborrow().init_correlation_id();
77        correlation_id.to_capnp(correlation_id_builder);
78    }
79
80    if let Some(causation_id) = causation_id {
81        let causation_id_builder = builder.reborrow().init_causation_id();
82        causation_id.to_capnp(causation_id_builder);
83    }
84}
85
86impl<'a> ToCapnp<'a> for CancelOrder {
87    type Builder = trading_capnp::cancel_order::Builder<'a>;
88
89    fn to_capnp(&self, mut builder: Self::Builder) {
90        let header_builder = builder.reborrow().init_header();
91        populate_trading_command_header(
92            header_builder,
93            &self.trader_id,
94            self.client_id.as_ref(),
95            &self.strategy_id,
96            &self.instrument_id,
97            &self.command_id,
98            self.ts_init,
99            self.correlation_id.as_ref(),
100            self.causation_id.as_ref(),
101        );
102
103        let client_order_id_builder = builder.reborrow().init_client_order_id();
104        self.client_order_id.to_capnp(client_order_id_builder);
105
106        if let Some(ref venue_order_id) = self.venue_order_id {
107            let venue_order_id_builder = builder.reborrow().init_venue_order_id();
108            venue_order_id.to_capnp(venue_order_id_builder);
109        }
110
111        if let Some(ref params) = self.params {
112            let params_builder = builder.reborrow().init_params();
113            populate_string_map(params_builder, params);
114        }
115    }
116}
117
118impl<'a> ToCapnp<'a> for CancelAllOrders {
119    type Builder = trading_capnp::cancel_all_orders::Builder<'a>;
120
121    fn to_capnp(&self, mut builder: Self::Builder) {
122        let header_builder = builder.reborrow().init_header();
123        populate_trading_command_header(
124            header_builder,
125            &self.trader_id,
126            self.client_id.as_ref(),
127            &self.strategy_id,
128            &self.instrument_id,
129            &self.command_id,
130            self.ts_init,
131            self.correlation_id.as_ref(),
132            self.causation_id.as_ref(),
133        );
134
135        builder.set_order_side(order_side_to_capnp(self.order_side));
136
137        if let Some(ref params) = self.params {
138            let params_builder = builder.reborrow().init_params();
139            populate_string_map(params_builder, params);
140        }
141    }
142}
143
144impl<'a> ToCapnp<'a> for BatchCancelOrders {
145    type Builder = trading_capnp::batch_cancel_orders::Builder<'a>;
146
147    fn to_capnp(&self, mut builder: Self::Builder) {
148        let header_builder = builder.reborrow().init_header();
149        populate_trading_command_header(
150            header_builder,
151            &self.trader_id,
152            self.client_id.as_ref(),
153            &self.strategy_id,
154            &self.instrument_id,
155            &self.command_id,
156            self.ts_init,
157            self.correlation_id.as_ref(),
158            self.causation_id.as_ref(),
159        );
160
161        let mut cancellations_builder = builder
162            .reborrow()
163            .init_cancellations(self.cancels.len() as u32);
164        for (i, cancel) in self.cancels.iter().enumerate() {
165            let cancel_builder = cancellations_builder.reborrow().get(i as u32);
166            cancel.to_capnp(cancel_builder);
167        }
168
169        if let Some(ref params) = self.params {
170            let params_builder = builder.reborrow().init_params();
171            populate_string_map(params_builder, params);
172        }
173    }
174}
175
176impl<'a> ToCapnp<'a> for ModifyOrder {
177    type Builder = trading_capnp::modify_order::Builder<'a>;
178
179    fn to_capnp(&self, mut builder: Self::Builder) {
180        let header_builder = builder.reborrow().init_header();
181        populate_trading_command_header(
182            header_builder,
183            &self.trader_id,
184            self.client_id.as_ref(),
185            &self.strategy_id,
186            &self.instrument_id,
187            &self.command_id,
188            self.ts_init,
189            self.correlation_id.as_ref(),
190            self.causation_id.as_ref(),
191        );
192
193        let client_order_id_builder = builder.reborrow().init_client_order_id();
194        self.client_order_id.to_capnp(client_order_id_builder);
195
196        if let Some(ref venue_order_id) = self.venue_order_id {
197            let venue_order_id_builder = builder.reborrow().init_venue_order_id();
198            venue_order_id.to_capnp(venue_order_id_builder);
199        }
200
201        if let Some(ref quantity) = self.quantity {
202            let quantity_builder = builder.reborrow().init_quantity();
203            quantity.to_capnp(quantity_builder);
204        }
205
206        if let Some(ref price) = self.price {
207            let price_builder = builder.reborrow().init_price();
208            price.to_capnp(price_builder);
209        }
210
211        if let Some(ref trigger_price) = self.trigger_price {
212            let trigger_price_builder = builder.reborrow().init_trigger_price();
213            trigger_price.to_capnp(trigger_price_builder);
214        }
215
216        if let Some(ref params) = self.params {
217            let params_builder = builder.reborrow().init_params();
218            populate_string_map(params_builder, params);
219        }
220    }
221}
222
223impl<'a> ToCapnp<'a> for QueryOrder {
224    type Builder = trading_capnp::query_order::Builder<'a>;
225
226    fn to_capnp(&self, mut builder: Self::Builder) {
227        let header_builder = builder.reborrow().init_header();
228        populate_trading_command_header(
229            header_builder,
230            &self.trader_id,
231            self.client_id.as_ref(),
232            &self.strategy_id,
233            &self.instrument_id,
234            &self.command_id,
235            self.ts_init,
236            self.correlation_id.as_ref(),
237            self.causation_id.as_ref(),
238        );
239
240        let client_order_id_builder = builder.reborrow().init_client_order_id();
241        self.client_order_id.to_capnp(client_order_id_builder);
242
243        if let Some(ref venue_order_id) = self.venue_order_id {
244            let venue_order_id_builder = builder.reborrow().init_venue_order_id();
245            venue_order_id.to_capnp(venue_order_id_builder);
246        }
247    }
248}
249
250impl<'a> ToCapnp<'a> for QueryAccount {
251    type Builder = trading_capnp::query_account::Builder<'a>;
252
253    fn to_capnp(&self, mut builder: Self::Builder) {
254        let trader_id_builder = builder.reborrow().init_trader_id();
255        self.trader_id.to_capnp(trader_id_builder);
256
257        let account_id_builder = builder.reborrow().init_account_id();
258        self.account_id.to_capnp(account_id_builder);
259
260        let command_id_builder = builder.reborrow().init_command_id();
261        self.command_id.to_capnp(command_id_builder);
262
263        let mut ts_init_builder = builder.reborrow().init_ts_init();
264        ts_init_builder.set_value(*self.ts_init);
265
266        if let Some(ref correlation_id) = self.correlation_id {
267            let correlation_id_builder = builder.reborrow().init_correlation_id();
268            correlation_id.to_capnp(correlation_id_builder);
269        }
270
271        if let Some(ref causation_id) = self.causation_id {
272            let causation_id_builder = builder.reborrow().init_causation_id();
273            causation_id.to_capnp(causation_id_builder);
274        }
275    }
276}
277
278impl<'a> ToCapnp<'a> for SubmitOrder {
279    type Builder = trading_capnp::submit_order::Builder<'a>;
280
281    fn to_capnp(&self, mut builder: Self::Builder) {
282        let header_builder = builder.reborrow().init_header();
283        populate_trading_command_header(
284            header_builder,
285            &self.trader_id,
286            self.client_id.as_ref(),
287            &self.strategy_id,
288            &self.instrument_id,
289            &self.command_id,
290            self.ts_init,
291            self.correlation_id.as_ref(),
292            self.causation_id.as_ref(),
293        );
294
295        let order_init_builder = builder.reborrow().init_order_init();
296        self.order_init.to_capnp(order_init_builder);
297
298        if let Some(ref position_id) = self.position_id {
299            let position_id_builder = builder.reborrow().init_position_id();
300            position_id.to_capnp(position_id_builder);
301        }
302
303        if let Some(ref params) = self.params {
304            let params_builder = builder.reborrow().init_params();
305            populate_string_map(params_builder, params);
306        }
307    }
308}
309
310impl<'a> ToCapnp<'a> for SubmitOrderList {
311    type Builder = trading_capnp::submit_order_list::Builder<'a>;
312
313    fn to_capnp(&self, mut builder: Self::Builder) {
314        let header_builder = builder.reborrow().init_header();
315        populate_trading_command_header(
316            header_builder,
317            &self.trader_id,
318            self.client_id.as_ref(),
319            &self.strategy_id,
320            &self.instrument_id,
321            &self.command_id,
322            self.ts_init,
323            self.correlation_id.as_ref(),
324            self.causation_id.as_ref(),
325        );
326
327        let mut order_inits_builder = builder
328            .reborrow()
329            .init_order_inits(self.order_inits.len() as u32);
330        for (i, order_init) in self.order_inits.iter().enumerate() {
331            let order_init_builder = order_inits_builder.reborrow().get(i as u32);
332            order_init.to_capnp(order_init_builder);
333        }
334
335        if let Some(ref position_id) = self.position_id {
336            let position_id_builder = builder.reborrow().init_position_id();
337            position_id.to_capnp(position_id_builder);
338        }
339
340        if let Some(ref params) = self.params {
341            let params_builder = builder.reborrow().init_params();
342            populate_string_map(params_builder, params);
343        }
344    }
345}
346
347impl<'a> ToCapnp<'a> for TradingCommand {
348    type Builder = trading_capnp::trading_command::Builder<'a>;
349
350    fn to_capnp(&self, builder: Self::Builder) {
351        match self {
352            Self::SubmitOrder(command) => {
353                let submit_builder = builder.init_submit_order();
354                command.to_capnp(submit_builder);
355            }
356            Self::SubmitOrderList(command) => {
357                let submit_list_builder = builder.init_submit_order_list();
358                command.to_capnp(submit_list_builder);
359            }
360            Self::ModifyOrder(command) => {
361                let modify_builder = builder.init_modify_order();
362                command.to_capnp(modify_builder);
363            }
364            Self::CancelOrder(command) => {
365                let cancel_builder = builder.init_cancel_order();
366                command.to_capnp(cancel_builder);
367            }
368            Self::CancelAllOrders(command) => {
369                let cancel_all_builder = builder.init_cancel_all_orders();
370                command.to_capnp(cancel_all_builder);
371            }
372            Self::BatchCancelOrders(command) => {
373                let batch_cancel_builder = builder.init_batch_cancel_orders();
374                command.to_capnp(batch_cancel_builder);
375            }
376            Self::QueryOrder(command) => {
377                let query_builder = builder.init_query_order();
378                command.to_capnp(query_builder);
379            }
380            Self::QueryAccount(command) => {
381                let query_builder = builder.init_query_account();
382                command.to_capnp(query_builder);
383            }
384        }
385    }
386}
387
388#[cfg(test)]
389mod tests {
390    use capnp::message::Builder;
391    use nautilus_core::UnixNanos;
392    use nautilus_model::{
393        enums::{OrderSide, OrderType},
394        identifiers::{
395            AccountId, ClientId, ClientOrderId, InstrumentId, OrderListId, StrategyId, TraderId,
396        },
397        orders::{Order, OrderList, OrderTestBuilder},
398        stubs::TestDefault,
399        types::{Price, Quantity},
400    };
401    use nautilus_serialization::capnp::FromCapnp;
402    use rstest::*;
403
404    use super::*;
405    use crate::messages::execution::{
406        cancel::{BatchCancelOrdersBuilder, CancelAllOrdersBuilder, CancelOrderBuilder},
407        modify::ModifyOrderBuilder,
408        query::{QueryAccountBuilder, QueryOrderBuilder},
409    };
410
411    #[fixture]
412    fn trader_id() -> TraderId {
413        TraderId::test_default()
414    }
415
416    #[fixture]
417    fn strategy_id() -> StrategyId {
418        StrategyId::test_default()
419    }
420
421    #[fixture]
422    fn instrument_id() -> InstrumentId {
423        InstrumentId::test_default()
424    }
425
426    #[fixture]
427    fn client_order_id() -> ClientOrderId {
428        ClientOrderId::test_default()
429    }
430
431    #[fixture]
432    fn command_id() -> UUID4 {
433        UUID4::new()
434    }
435
436    #[fixture]
437    fn ts_init() -> UnixNanos {
438        UnixNanos::default()
439    }
440
441    #[fixture]
442    fn client_id() -> ClientId {
443        ClientId::new("TEST")
444    }
445
446    #[rstest]
447    fn test_cancel_order_serialization(
448        trader_id: TraderId,
449        client_id: ClientId,
450        strategy_id: StrategyId,
451        instrument_id: InstrumentId,
452        client_order_id: ClientOrderId,
453        command_id: UUID4,
454        ts_init: UnixNanos,
455    ) {
456        let command = CancelOrderBuilder::default()
457            .trader_id(trader_id)
458            .client_id(Some(client_id))
459            .strategy_id(strategy_id)
460            .instrument_id(instrument_id)
461            .client_order_id(client_order_id)
462            .venue_order_id(None)
463            .command_id(command_id)
464            .ts_init(ts_init)
465            .params(None)
466            .build()
467            .unwrap();
468
469        let mut message = Builder::new_default();
470        {
471            let builder = message.init_root::<trading_capnp::cancel_order::Builder>();
472            command.to_capnp(builder);
473        }
474
475        let reader = message
476            .get_root_as_reader::<trading_capnp::cancel_order::Reader>()
477            .expect("Valid capnp message");
478
479        // Verify header is populated
480        assert!(reader.has_header());
481        let header = reader.get_header().unwrap();
482        assert!(header.has_trader_id());
483        assert!(header.has_client_id());
484        assert!(header.has_strategy_id());
485        assert!(header.has_instrument_id());
486        assert!(header.has_command_id());
487        assert!(header.has_ts_init());
488    }
489
490    #[rstest]
491    fn test_cancel_all_orders_serialization(
492        trader_id: TraderId,
493        strategy_id: StrategyId,
494        instrument_id: InstrumentId,
495        command_id: UUID4,
496        ts_init: UnixNanos,
497    ) {
498        let command = CancelAllOrdersBuilder::default()
499            .trader_id(trader_id)
500            .client_id(None)
501            .strategy_id(strategy_id)
502            .instrument_id(instrument_id)
503            .order_side(OrderSide::Buy)
504            .command_id(command_id)
505            .ts_init(ts_init)
506            .params(None)
507            .build()
508            .unwrap();
509
510        let mut message = Builder::new_default();
511        {
512            let builder = message.init_root::<trading_capnp::cancel_all_orders::Builder>();
513            command.to_capnp(builder);
514        }
515
516        let reader = message
517            .get_root_as_reader::<trading_capnp::cancel_all_orders::Reader>()
518            .expect("Valid capnp message");
519
520        assert!(reader.has_header());
521    }
522
523    #[rstest]
524    fn test_batch_cancel_orders_serialization(
525        trader_id: TraderId,
526        strategy_id: StrategyId,
527        instrument_id: InstrumentId,
528        command_id: UUID4,
529        ts_init: UnixNanos,
530    ) {
531        let cancel1 = CancelOrderBuilder::default()
532            .trader_id(trader_id)
533            .client_id(None)
534            .strategy_id(strategy_id)
535            .instrument_id(instrument_id)
536            .client_order_id(ClientOrderId::new("O-001"))
537            .venue_order_id(None)
538            .command_id(UUID4::new())
539            .ts_init(ts_init)
540            .params(None)
541            .build()
542            .unwrap();
543
544        let cancel2 = CancelOrderBuilder::default()
545            .trader_id(trader_id)
546            .client_id(None)
547            .strategy_id(strategy_id)
548            .instrument_id(instrument_id)
549            .client_order_id(ClientOrderId::new("O-002"))
550            .venue_order_id(None)
551            .command_id(UUID4::new())
552            .ts_init(ts_init)
553            .params(None)
554            .build()
555            .unwrap();
556
557        let command = BatchCancelOrdersBuilder::default()
558            .trader_id(trader_id)
559            .client_id(None)
560            .strategy_id(strategy_id)
561            .instrument_id(instrument_id)
562            .cancels(vec![cancel1, cancel2])
563            .command_id(command_id)
564            .ts_init(ts_init)
565            .params(None)
566            .build()
567            .unwrap();
568
569        let mut message = Builder::new_default();
570        {
571            let builder = message.init_root::<trading_capnp::batch_cancel_orders::Builder>();
572            command.to_capnp(builder);
573        }
574
575        let reader = message
576            .get_root_as_reader::<trading_capnp::batch_cancel_orders::Reader>()
577            .expect("Valid capnp message");
578
579        assert!(reader.has_header());
580        assert!(reader.has_cancellations());
581        assert_eq!(reader.get_cancellations().unwrap().len(), 2);
582    }
583
584    #[rstest]
585    fn test_modify_order_serialization(
586        trader_id: TraderId,
587        strategy_id: StrategyId,
588        instrument_id: InstrumentId,
589        client_order_id: ClientOrderId,
590        command_id: UUID4,
591        ts_init: UnixNanos,
592    ) {
593        let command = ModifyOrderBuilder::default()
594            .trader_id(trader_id)
595            .client_id(None)
596            .strategy_id(strategy_id)
597            .instrument_id(instrument_id)
598            .client_order_id(client_order_id)
599            .venue_order_id(None)
600            .quantity(Some(Quantity::new(100.0, 0)))
601            .price(Some(Price::new(50_000.0, 2)))
602            .trigger_price(Some(Price::new(49_000.0, 2)))
603            .command_id(command_id)
604            .ts_init(ts_init)
605            .params(None)
606            .build()
607            .unwrap();
608
609        let mut message = Builder::new_default();
610        {
611            let builder = message.init_root::<trading_capnp::modify_order::Builder>();
612            command.to_capnp(builder);
613        }
614
615        let reader = message
616            .get_root_as_reader::<trading_capnp::modify_order::Reader>()
617            .expect("Valid capnp message");
618
619        assert!(reader.has_header());
620        assert!(reader.has_quantity());
621        assert!(reader.has_price());
622        assert!(reader.has_trigger_price());
623    }
624
625    #[rstest]
626    fn test_query_order_serialization(
627        trader_id: TraderId,
628        strategy_id: StrategyId,
629        instrument_id: InstrumentId,
630        client_order_id: ClientOrderId,
631        command_id: UUID4,
632        ts_init: UnixNanos,
633    ) {
634        let command = QueryOrderBuilder::default()
635            .trader_id(trader_id)
636            .client_id(None)
637            .strategy_id(strategy_id)
638            .instrument_id(instrument_id)
639            .client_order_id(client_order_id)
640            .venue_order_id(None)
641            .command_id(command_id)
642            .ts_init(ts_init)
643            .params(None)
644            .build()
645            .unwrap();
646
647        let mut message = Builder::new_default();
648        {
649            let builder = message.init_root::<trading_capnp::query_order::Builder>();
650            command.to_capnp(builder);
651        }
652
653        let reader = message
654            .get_root_as_reader::<trading_capnp::query_order::Reader>()
655            .expect("Valid capnp message");
656
657        assert!(reader.has_header());
658    }
659
660    #[rstest]
661    fn test_query_account_serialization(
662        trader_id: TraderId,
663        command_id: UUID4,
664        ts_init: UnixNanos,
665    ) {
666        let command = QueryAccountBuilder::default()
667            .trader_id(trader_id)
668            .client_id(None)
669            .account_id(AccountId::new("ACC-001"))
670            .command_id(command_id)
671            .ts_init(ts_init)
672            .params(None)
673            .build()
674            .unwrap();
675
676        let mut message = Builder::new_default();
677        {
678            let builder = message.init_root::<trading_capnp::query_account::Builder>();
679            command.to_capnp(builder);
680        }
681
682        let reader = message
683            .get_root_as_reader::<trading_capnp::query_account::Reader>()
684            .expect("Valid capnp message");
685
686        assert!(reader.has_trader_id());
687        assert!(reader.has_account_id());
688    }
689
690    #[rstest]
691    #[case(None)]
692    #[case(Some(UUID4::new()))]
693    fn test_cancel_order_correlation_id_roundtrips_through_capnp_header(
694        trader_id: TraderId,
695        client_id: ClientId,
696        strategy_id: StrategyId,
697        instrument_id: InstrumentId,
698        client_order_id: ClientOrderId,
699        #[case] correlation_id: Option<UUID4>,
700    ) {
701        let command = CancelOrderBuilder::default()
702            .trader_id(trader_id)
703            .client_id(Some(client_id))
704            .strategy_id(strategy_id)
705            .instrument_id(instrument_id)
706            .client_order_id(client_order_id)
707            .venue_order_id(None)
708            .command_id(UUID4::new())
709            .ts_init(UnixNanos::default())
710            .params(None)
711            .correlation_id(correlation_id)
712            .build()
713            .unwrap();
714
715        let mut message = Builder::new_default();
716        {
717            let builder = message.init_root::<trading_capnp::cancel_order::Builder>();
718            command.to_capnp(builder);
719        }
720
721        let reader = message
722            .get_root_as_reader::<trading_capnp::cancel_order::Reader>()
723            .expect("Valid capnp message");
724        let header = reader.get_header().unwrap();
725
726        assert_eq!(header.has_correlation_id(), correlation_id.is_some());
727        if let Some(expected) = correlation_id {
728            let decoded = UUID4::from_capnp(header.get_correlation_id().unwrap())
729                .expect("correlation_id decodes");
730            assert_eq!(decoded, expected);
731        }
732    }
733
734    #[rstest]
735    #[case(None)]
736    #[case(Some(UUID4::new()))]
737    fn test_cancel_order_causation_id_roundtrips_through_capnp_header(
738        trader_id: TraderId,
739        client_id: ClientId,
740        strategy_id: StrategyId,
741        instrument_id: InstrumentId,
742        client_order_id: ClientOrderId,
743        #[case] causation_id: Option<UUID4>,
744    ) {
745        let command = CancelOrderBuilder::default()
746            .trader_id(trader_id)
747            .client_id(Some(client_id))
748            .strategy_id(strategy_id)
749            .instrument_id(instrument_id)
750            .client_order_id(client_order_id)
751            .venue_order_id(None)
752            .command_id(UUID4::new())
753            .ts_init(UnixNanos::default())
754            .params(None)
755            .causation_id(causation_id)
756            .build()
757            .unwrap();
758
759        let mut message = Builder::new_default();
760        {
761            let builder = message.init_root::<trading_capnp::cancel_order::Builder>();
762            command.to_capnp(builder);
763        }
764
765        let reader = message
766            .get_root_as_reader::<trading_capnp::cancel_order::Reader>()
767            .expect("Valid capnp message");
768        let header = reader.get_header().unwrap();
769
770        assert_eq!(header.has_causation_id(), causation_id.is_some());
771        if let Some(expected) = causation_id {
772            let decoded = UUID4::from_capnp(header.get_causation_id().unwrap())
773                .expect("causation_id decodes");
774            assert_eq!(decoded, expected);
775        }
776    }
777
778    #[rstest]
779    #[case(None)]
780    #[case(Some(UUID4::new()))]
781    fn test_query_account_correlation_id_roundtrips_through_capnp(
782        trader_id: TraderId,
783        command_id: UUID4,
784        ts_init: UnixNanos,
785        #[case] correlation_id: Option<UUID4>,
786    ) {
787        let command = QueryAccountBuilder::default()
788            .trader_id(trader_id)
789            .client_id(None)
790            .account_id(AccountId::new("ACC-001"))
791            .command_id(command_id)
792            .ts_init(ts_init)
793            .params(None)
794            .correlation_id(correlation_id)
795            .build()
796            .unwrap();
797
798        let mut message = Builder::new_default();
799        {
800            let builder = message.init_root::<trading_capnp::query_account::Builder>();
801            command.to_capnp(builder);
802        }
803
804        let reader = message
805            .get_root_as_reader::<trading_capnp::query_account::Reader>()
806            .expect("Valid capnp message");
807
808        assert_eq!(reader.has_correlation_id(), correlation_id.is_some());
809        if let Some(expected) = correlation_id {
810            let decoded = UUID4::from_capnp(reader.get_correlation_id().unwrap())
811                .expect("correlation_id decodes");
812            assert_eq!(decoded, expected);
813        }
814    }
815
816    #[rstest]
817    #[case(None)]
818    #[case(Some(UUID4::new()))]
819    fn test_query_account_causation_id_roundtrips_through_capnp(
820        trader_id: TraderId,
821        command_id: UUID4,
822        ts_init: UnixNanos,
823        #[case] causation_id: Option<UUID4>,
824    ) {
825        // QueryAccount has a flat capnp layout (not TradingCommandHeader); cover its
826        // causation_id field directly so the wire boundary preserves it the same way
827        // the shared header path does for the other trading commands.
828        let command = QueryAccountBuilder::default()
829            .trader_id(trader_id)
830            .client_id(None)
831            .account_id(AccountId::new("ACC-001"))
832            .command_id(command_id)
833            .ts_init(ts_init)
834            .params(None)
835            .causation_id(causation_id)
836            .build()
837            .unwrap();
838
839        let mut message = Builder::new_default();
840        {
841            let builder = message.init_root::<trading_capnp::query_account::Builder>();
842            command.to_capnp(builder);
843        }
844
845        let reader = message
846            .get_root_as_reader::<trading_capnp::query_account::Reader>()
847            .expect("Valid capnp message");
848
849        assert_eq!(reader.has_causation_id(), causation_id.is_some());
850        if let Some(expected) = causation_id {
851            let decoded = UUID4::from_capnp(reader.get_causation_id().unwrap())
852                .expect("causation_id decodes");
853            assert_eq!(decoded, expected);
854        }
855    }
856
857    #[rstest]
858    fn test_submit_order_serialization(command_id: UUID4, ts_init: UnixNanos, client_id: ClientId) {
859        let order = OrderTestBuilder::new(OrderType::Limit)
860            .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
861            .side(OrderSide::Buy)
862            .quantity(Quantity::new(1.0, 8))
863            .price(Price::new(50_000.0, 2))
864            .build();
865
866        let command = SubmitOrder::new(
867            order.trader_id(),
868            Some(client_id),
869            order.strategy_id(),
870            order.instrument_id(),
871            order.client_order_id(),
872            order.init_event().clone(),
873            None,
874            None,
875            None,
876            command_id,
877            ts_init,
878            None, // correlation_id
879        );
880
881        let mut message = Builder::new_default();
882        {
883            let builder = message.init_root::<trading_capnp::submit_order::Builder>();
884            command.to_capnp(builder);
885        }
886
887        let reader = message
888            .get_root_as_reader::<trading_capnp::submit_order::Reader>()
889            .expect("Valid capnp message");
890
891        assert!(reader.has_header());
892        assert!(reader.has_order_init());
893    }
894
895    #[rstest]
896    fn test_submit_order_list_serialization(
897        command_id: UUID4,
898        ts_init: UnixNanos,
899        client_id: ClientId,
900    ) {
901        let order1 = OrderTestBuilder::new(OrderType::Limit)
902            .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
903            .client_order_id(ClientOrderId::from("O-001"))
904            .side(OrderSide::Buy)
905            .quantity(Quantity::new(1.0, 8))
906            .price(Price::new(50_000.0, 2))
907            .build();
908
909        let order2 = OrderTestBuilder::new(OrderType::Limit)
910            .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
911            .client_order_id(ClientOrderId::from("O-002"))
912            .side(OrderSide::Sell)
913            .quantity(Quantity::new(1.0, 8))
914            .price(Price::new(51_000.0, 2))
915            .build();
916
917        let orders = [order1.clone(), order2];
918        let order_inits: Vec<_> = orders.iter().map(|o| o.init_event().clone()).collect();
919        let order_list = OrderList::new(
920            OrderListId::new("OL-001"),
921            InstrumentId::from("BTCUSDT.BINANCE"),
922            order1.strategy_id(),
923            vec![order1.client_order_id(), orders[1].client_order_id()],
924            ts_init,
925        );
926
927        let command = SubmitOrderList::new(
928            order1.trader_id(),
929            Some(client_id),
930            order1.strategy_id(),
931            order_list,
932            order_inits,
933            None,
934            None,
935            None,
936            command_id,
937            ts_init,
938            None, // correlation_id
939        );
940
941        let mut message = Builder::new_default();
942        {
943            let builder = message.init_root::<trading_capnp::submit_order_list::Builder>();
944            command.to_capnp(builder);
945        }
946
947        let reader = message
948            .get_root_as_reader::<trading_capnp::submit_order_list::Reader>()
949            .expect("Valid capnp message");
950
951        assert!(reader.has_header());
952        assert!(reader.has_order_inits());
953        assert_eq!(reader.get_order_inits().unwrap().len(), 2);
954    }
955
956    #[rstest]
957    fn test_trading_command_enum_serialization(
958        trader_id: TraderId,
959        strategy_id: StrategyId,
960        instrument_id: InstrumentId,
961        client_order_id: ClientOrderId,
962        command_id: UUID4,
963        ts_init: UnixNanos,
964    ) {
965        let cancel = CancelOrderBuilder::default()
966            .trader_id(trader_id)
967            .client_id(None)
968            .strategy_id(strategy_id)
969            .instrument_id(instrument_id)
970            .client_order_id(client_order_id)
971            .venue_order_id(None)
972            .command_id(command_id)
973            .ts_init(ts_init)
974            .params(None)
975            .build()
976            .unwrap();
977
978        let command = TradingCommand::CancelOrder(cancel);
979
980        let mut message = Builder::new_default();
981        {
982            let builder = message.init_root::<trading_capnp::trading_command::Builder>();
983            command.to_capnp(builder);
984        }
985
986        let reader = message
987            .get_root_as_reader::<trading_capnp::trading_command::Reader>()
988            .expect("Valid capnp message");
989
990        // Verify it's a cancel order variant
991        assert!(reader.has_cancel_order());
992    }
993}