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<'a>(builder: base_capnp::string_map::Builder<'a>, 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
43fn populate_trading_command_header<'a>(
44    mut builder: trading_capnp::trading_command_header::Builder<'a>,
45    trader_id: &TraderId,
46    client_id: Option<&ClientId>,
47    strategy_id: &StrategyId,
48    instrument_id: &InstrumentId,
49    command_id: &UUID4,
50    ts_init: UnixNanos,
51) {
52    let trader_id_builder = builder.reborrow().init_trader_id();
53    trader_id.to_capnp(trader_id_builder);
54
55    if let Some(client_id) = client_id {
56        let client_id_builder = builder.reborrow().init_client_id();
57        client_id.to_capnp(client_id_builder);
58    }
59
60    let strategy_id_builder = builder.reborrow().init_strategy_id();
61    strategy_id.to_capnp(strategy_id_builder);
62
63    let instrument_id_builder = builder.reborrow().init_instrument_id();
64    instrument_id.to_capnp(instrument_id_builder);
65
66    let command_id_builder = builder.reborrow().init_command_id();
67    command_id.to_capnp(command_id_builder);
68
69    let mut ts_init_builder = builder.reborrow().init_ts_init();
70    ts_init_builder.set_value(*ts_init);
71}
72
73impl<'a> ToCapnp<'a> for CancelOrder {
74    type Builder = trading_capnp::cancel_order::Builder<'a>;
75
76    fn to_capnp(&self, mut builder: Self::Builder) {
77        let header_builder = builder.reborrow().init_header();
78        populate_trading_command_header(
79            header_builder,
80            &self.trader_id,
81            self.client_id.as_ref(),
82            &self.strategy_id,
83            &self.instrument_id,
84            &self.command_id,
85            self.ts_init,
86        );
87
88        let client_order_id_builder = builder.reborrow().init_client_order_id();
89        self.client_order_id.to_capnp(client_order_id_builder);
90
91        if let Some(ref venue_order_id) = self.venue_order_id {
92            let venue_order_id_builder = builder.reborrow().init_venue_order_id();
93            venue_order_id.to_capnp(venue_order_id_builder);
94        }
95
96        if let Some(ref params) = self.params {
97            let params_builder = builder.reborrow().init_params();
98            populate_string_map(params_builder, params);
99        }
100    }
101}
102
103impl<'a> ToCapnp<'a> for CancelAllOrders {
104    type Builder = trading_capnp::cancel_all_orders::Builder<'a>;
105
106    fn to_capnp(&self, mut builder: Self::Builder) {
107        let header_builder = builder.reborrow().init_header();
108        populate_trading_command_header(
109            header_builder,
110            &self.trader_id,
111            self.client_id.as_ref(),
112            &self.strategy_id,
113            &self.instrument_id,
114            &self.command_id,
115            self.ts_init,
116        );
117
118        builder.set_order_side(order_side_to_capnp(self.order_side));
119
120        if let Some(ref params) = self.params {
121            let params_builder = builder.reborrow().init_params();
122            populate_string_map(params_builder, params);
123        }
124    }
125}
126
127impl<'a> ToCapnp<'a> for BatchCancelOrders {
128    type Builder = trading_capnp::batch_cancel_orders::Builder<'a>;
129
130    fn to_capnp(&self, mut builder: Self::Builder) {
131        let header_builder = builder.reborrow().init_header();
132        populate_trading_command_header(
133            header_builder,
134            &self.trader_id,
135            self.client_id.as_ref(),
136            &self.strategy_id,
137            &self.instrument_id,
138            &self.command_id,
139            self.ts_init,
140        );
141
142        let mut cancellations_builder = builder
143            .reborrow()
144            .init_cancellations(self.cancels.len() as u32);
145        for (i, cancel) in self.cancels.iter().enumerate() {
146            let cancel_builder = cancellations_builder.reborrow().get(i as u32);
147            cancel.to_capnp(cancel_builder);
148        }
149
150        if let Some(ref params) = self.params {
151            let params_builder = builder.reborrow().init_params();
152            populate_string_map(params_builder, params);
153        }
154    }
155}
156
157impl<'a> ToCapnp<'a> for ModifyOrder {
158    type Builder = trading_capnp::modify_order::Builder<'a>;
159
160    fn to_capnp(&self, mut builder: Self::Builder) {
161        let header_builder = builder.reborrow().init_header();
162        populate_trading_command_header(
163            header_builder,
164            &self.trader_id,
165            self.client_id.as_ref(),
166            &self.strategy_id,
167            &self.instrument_id,
168            &self.command_id,
169            self.ts_init,
170        );
171
172        let client_order_id_builder = builder.reborrow().init_client_order_id();
173        self.client_order_id.to_capnp(client_order_id_builder);
174
175        if let Some(ref venue_order_id) = self.venue_order_id {
176            let venue_order_id_builder = builder.reborrow().init_venue_order_id();
177            venue_order_id.to_capnp(venue_order_id_builder);
178        }
179
180        if let Some(ref quantity) = self.quantity {
181            let quantity_builder = builder.reborrow().init_quantity();
182            quantity.to_capnp(quantity_builder);
183        }
184
185        if let Some(ref price) = self.price {
186            let price_builder = builder.reborrow().init_price();
187            price.to_capnp(price_builder);
188        }
189
190        if let Some(ref trigger_price) = self.trigger_price {
191            let trigger_price_builder = builder.reborrow().init_trigger_price();
192            trigger_price.to_capnp(trigger_price_builder);
193        }
194
195        if let Some(ref params) = self.params {
196            let params_builder = builder.reborrow().init_params();
197            populate_string_map(params_builder, params);
198        }
199    }
200}
201
202impl<'a> ToCapnp<'a> for QueryOrder {
203    type Builder = trading_capnp::query_order::Builder<'a>;
204
205    fn to_capnp(&self, mut builder: Self::Builder) {
206        let header_builder = builder.reborrow().init_header();
207        populate_trading_command_header(
208            header_builder,
209            &self.trader_id,
210            self.client_id.as_ref(),
211            &self.strategy_id,
212            &self.instrument_id,
213            &self.command_id,
214            self.ts_init,
215        );
216
217        let client_order_id_builder = builder.reborrow().init_client_order_id();
218        self.client_order_id.to_capnp(client_order_id_builder);
219
220        if let Some(ref venue_order_id) = self.venue_order_id {
221            let venue_order_id_builder = builder.reborrow().init_venue_order_id();
222            venue_order_id.to_capnp(venue_order_id_builder);
223        }
224    }
225}
226
227impl<'a> ToCapnp<'a> for QueryAccount {
228    type Builder = trading_capnp::query_account::Builder<'a>;
229
230    fn to_capnp(&self, mut builder: Self::Builder) {
231        let trader_id_builder = builder.reborrow().init_trader_id();
232        self.trader_id.to_capnp(trader_id_builder);
233
234        let account_id_builder = builder.reborrow().init_account_id();
235        self.account_id.to_capnp(account_id_builder);
236
237        let command_id_builder = builder.reborrow().init_command_id();
238        self.command_id.to_capnp(command_id_builder);
239
240        let mut ts_init_builder = builder.reborrow().init_ts_init();
241        ts_init_builder.set_value(*self.ts_init);
242    }
243}
244
245impl<'a> ToCapnp<'a> for SubmitOrder {
246    type Builder = trading_capnp::submit_order::Builder<'a>;
247
248    fn to_capnp(&self, mut builder: Self::Builder) {
249        let header_builder = builder.reborrow().init_header();
250        populate_trading_command_header(
251            header_builder,
252            &self.trader_id,
253            self.client_id.as_ref(),
254            &self.strategy_id,
255            &self.instrument_id,
256            &self.command_id,
257            self.ts_init,
258        );
259
260        let order_init_builder = builder.reborrow().init_order_init();
261        self.order_init.to_capnp(order_init_builder);
262
263        if let Some(ref position_id) = self.position_id {
264            let position_id_builder = builder.reborrow().init_position_id();
265            position_id.to_capnp(position_id_builder);
266        }
267
268        if let Some(ref params) = self.params {
269            let params_builder = builder.reborrow().init_params();
270            populate_string_map(params_builder, params);
271        }
272    }
273}
274
275impl<'a> ToCapnp<'a> for SubmitOrderList {
276    type Builder = trading_capnp::submit_order_list::Builder<'a>;
277
278    fn to_capnp(&self, mut builder: Self::Builder) {
279        let header_builder = builder.reborrow().init_header();
280        populate_trading_command_header(
281            header_builder,
282            &self.trader_id,
283            self.client_id.as_ref(),
284            &self.strategy_id,
285            &self.instrument_id,
286            &self.command_id,
287            self.ts_init,
288        );
289
290        let mut order_inits_builder = builder
291            .reborrow()
292            .init_order_inits(self.order_inits.len() as u32);
293        for (i, order_init) in self.order_inits.iter().enumerate() {
294            let order_init_builder = order_inits_builder.reborrow().get(i as u32);
295            order_init.to_capnp(order_init_builder);
296        }
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 TradingCommand {
311    type Builder = trading_capnp::trading_command::Builder<'a>;
312
313    fn to_capnp(&self, builder: Self::Builder) {
314        match self {
315            Self::SubmitOrder(command) => {
316                let submit_builder = builder.init_submit_order();
317                command.to_capnp(submit_builder);
318            }
319            Self::SubmitOrderList(command) => {
320                let submit_list_builder = builder.init_submit_order_list();
321                command.to_capnp(submit_list_builder);
322            }
323            Self::ModifyOrder(command) => {
324                let modify_builder = builder.init_modify_order();
325                command.to_capnp(modify_builder);
326            }
327            Self::CancelOrder(command) => {
328                let cancel_builder = builder.init_cancel_order();
329                command.to_capnp(cancel_builder);
330            }
331            Self::CancelAllOrders(command) => {
332                let cancel_all_builder = builder.init_cancel_all_orders();
333                command.to_capnp(cancel_all_builder);
334            }
335            Self::BatchCancelOrders(command) => {
336                let batch_cancel_builder = builder.init_batch_cancel_orders();
337                command.to_capnp(batch_cancel_builder);
338            }
339            Self::QueryOrder(command) => {
340                let query_builder = builder.init_query_order();
341                command.to_capnp(query_builder);
342            }
343            Self::QueryAccount(command) => {
344                let query_builder = builder.init_query_account();
345                command.to_capnp(query_builder);
346            }
347        }
348    }
349}
350
351#[cfg(test)]
352mod tests {
353    use capnp::message::Builder;
354    use nautilus_core::UnixNanos;
355    use nautilus_model::{
356        enums::{OrderSide, OrderType},
357        identifiers::{
358            AccountId, ClientId, ClientOrderId, InstrumentId, OrderListId, StrategyId, TraderId,
359        },
360        orders::{Order, OrderList, OrderTestBuilder},
361        stubs::TestDefault,
362        types::{Price, Quantity},
363    };
364    use rstest::*;
365
366    use super::*;
367    use crate::messages::execution::{
368        cancel::{BatchCancelOrdersBuilder, CancelAllOrdersBuilder, CancelOrderBuilder},
369        modify::ModifyOrderBuilder,
370        query::{QueryAccountBuilder, QueryOrderBuilder},
371    };
372
373    #[fixture]
374    fn trader_id() -> TraderId {
375        TraderId::test_default()
376    }
377
378    #[fixture]
379    fn strategy_id() -> StrategyId {
380        StrategyId::test_default()
381    }
382
383    #[fixture]
384    fn instrument_id() -> InstrumentId {
385        InstrumentId::test_default()
386    }
387
388    #[fixture]
389    fn client_order_id() -> ClientOrderId {
390        ClientOrderId::test_default()
391    }
392
393    #[fixture]
394    fn command_id() -> UUID4 {
395        UUID4::new()
396    }
397
398    #[fixture]
399    fn ts_init() -> UnixNanos {
400        UnixNanos::default()
401    }
402
403    #[fixture]
404    fn client_id() -> ClientId {
405        ClientId::new("TEST")
406    }
407
408    #[rstest]
409    fn test_cancel_order_serialization(
410        trader_id: TraderId,
411        client_id: ClientId,
412        strategy_id: StrategyId,
413        instrument_id: InstrumentId,
414        client_order_id: ClientOrderId,
415        command_id: UUID4,
416        ts_init: UnixNanos,
417    ) {
418        let command = CancelOrderBuilder::default()
419            .trader_id(trader_id)
420            .client_id(Some(client_id))
421            .strategy_id(strategy_id)
422            .instrument_id(instrument_id)
423            .client_order_id(client_order_id)
424            .venue_order_id(None)
425            .command_id(command_id)
426            .ts_init(ts_init)
427            .params(None)
428            .build()
429            .unwrap();
430
431        let mut message = Builder::new_default();
432        {
433            let builder = message.init_root::<trading_capnp::cancel_order::Builder>();
434            command.to_capnp(builder);
435        }
436
437        let reader = message
438            .get_root_as_reader::<trading_capnp::cancel_order::Reader>()
439            .expect("Valid capnp message");
440
441        // Verify header is populated
442        assert!(reader.has_header());
443        let header = reader.get_header().unwrap();
444        assert!(header.has_trader_id());
445        assert!(header.has_client_id());
446        assert!(header.has_strategy_id());
447        assert!(header.has_instrument_id());
448        assert!(header.has_command_id());
449        assert!(header.has_ts_init());
450    }
451
452    #[rstest]
453    fn test_cancel_all_orders_serialization(
454        trader_id: TraderId,
455        strategy_id: StrategyId,
456        instrument_id: InstrumentId,
457        command_id: UUID4,
458        ts_init: UnixNanos,
459    ) {
460        let command = CancelAllOrdersBuilder::default()
461            .trader_id(trader_id)
462            .client_id(None)
463            .strategy_id(strategy_id)
464            .instrument_id(instrument_id)
465            .order_side(OrderSide::Buy)
466            .command_id(command_id)
467            .ts_init(ts_init)
468            .params(None)
469            .build()
470            .unwrap();
471
472        let mut message = Builder::new_default();
473        {
474            let builder = message.init_root::<trading_capnp::cancel_all_orders::Builder>();
475            command.to_capnp(builder);
476        }
477
478        let reader = message
479            .get_root_as_reader::<trading_capnp::cancel_all_orders::Reader>()
480            .expect("Valid capnp message");
481
482        assert!(reader.has_header());
483    }
484
485    #[rstest]
486    fn test_batch_cancel_orders_serialization(
487        trader_id: TraderId,
488        strategy_id: StrategyId,
489        instrument_id: InstrumentId,
490        command_id: UUID4,
491        ts_init: UnixNanos,
492    ) {
493        let cancel1 = CancelOrderBuilder::default()
494            .trader_id(trader_id)
495            .client_id(None)
496            .strategy_id(strategy_id)
497            .instrument_id(instrument_id)
498            .client_order_id(ClientOrderId::new("O-001"))
499            .venue_order_id(None)
500            .command_id(UUID4::new())
501            .ts_init(ts_init)
502            .params(None)
503            .build()
504            .unwrap();
505
506        let cancel2 = CancelOrderBuilder::default()
507            .trader_id(trader_id)
508            .client_id(None)
509            .strategy_id(strategy_id)
510            .instrument_id(instrument_id)
511            .client_order_id(ClientOrderId::new("O-002"))
512            .venue_order_id(None)
513            .command_id(UUID4::new())
514            .ts_init(ts_init)
515            .params(None)
516            .build()
517            .unwrap();
518
519        let command = BatchCancelOrdersBuilder::default()
520            .trader_id(trader_id)
521            .client_id(None)
522            .strategy_id(strategy_id)
523            .instrument_id(instrument_id)
524            .cancels(vec![cancel1, cancel2])
525            .command_id(command_id)
526            .ts_init(ts_init)
527            .params(None)
528            .build()
529            .unwrap();
530
531        let mut message = Builder::new_default();
532        {
533            let builder = message.init_root::<trading_capnp::batch_cancel_orders::Builder>();
534            command.to_capnp(builder);
535        }
536
537        let reader = message
538            .get_root_as_reader::<trading_capnp::batch_cancel_orders::Reader>()
539            .expect("Valid capnp message");
540
541        assert!(reader.has_header());
542        assert!(reader.has_cancellations());
543        assert_eq!(reader.get_cancellations().unwrap().len(), 2);
544    }
545
546    #[rstest]
547    fn test_modify_order_serialization(
548        trader_id: TraderId,
549        strategy_id: StrategyId,
550        instrument_id: InstrumentId,
551        client_order_id: ClientOrderId,
552        command_id: UUID4,
553        ts_init: UnixNanos,
554    ) {
555        let command = ModifyOrderBuilder::default()
556            .trader_id(trader_id)
557            .client_id(None)
558            .strategy_id(strategy_id)
559            .instrument_id(instrument_id)
560            .client_order_id(client_order_id)
561            .venue_order_id(None)
562            .quantity(Some(Quantity::new(100.0, 0)))
563            .price(Some(Price::new(50_000.0, 2)))
564            .trigger_price(Some(Price::new(49_000.0, 2)))
565            .command_id(command_id)
566            .ts_init(ts_init)
567            .params(None)
568            .build()
569            .unwrap();
570
571        let mut message = Builder::new_default();
572        {
573            let builder = message.init_root::<trading_capnp::modify_order::Builder>();
574            command.to_capnp(builder);
575        }
576
577        let reader = message
578            .get_root_as_reader::<trading_capnp::modify_order::Reader>()
579            .expect("Valid capnp message");
580
581        assert!(reader.has_header());
582        assert!(reader.has_quantity());
583        assert!(reader.has_price());
584        assert!(reader.has_trigger_price());
585    }
586
587    #[rstest]
588    fn test_query_order_serialization(
589        trader_id: TraderId,
590        strategy_id: StrategyId,
591        instrument_id: InstrumentId,
592        client_order_id: ClientOrderId,
593        command_id: UUID4,
594        ts_init: UnixNanos,
595    ) {
596        let command = QueryOrderBuilder::default()
597            .trader_id(trader_id)
598            .client_id(None)
599            .strategy_id(strategy_id)
600            .instrument_id(instrument_id)
601            .client_order_id(client_order_id)
602            .venue_order_id(None)
603            .command_id(command_id)
604            .ts_init(ts_init)
605            .build()
606            .unwrap();
607
608        let mut message = Builder::new_default();
609        {
610            let builder = message.init_root::<trading_capnp::query_order::Builder>();
611            command.to_capnp(builder);
612        }
613
614        let reader = message
615            .get_root_as_reader::<trading_capnp::query_order::Reader>()
616            .expect("Valid capnp message");
617
618        assert!(reader.has_header());
619    }
620
621    #[rstest]
622    fn test_query_account_serialization(
623        trader_id: TraderId,
624        command_id: UUID4,
625        ts_init: UnixNanos,
626    ) {
627        let command = QueryAccountBuilder::default()
628            .trader_id(trader_id)
629            .client_id(None)
630            .account_id(AccountId::new("ACC-001"))
631            .command_id(command_id)
632            .ts_init(ts_init)
633            .build()
634            .unwrap();
635
636        let mut message = Builder::new_default();
637        {
638            let builder = message.init_root::<trading_capnp::query_account::Builder>();
639            command.to_capnp(builder);
640        }
641
642        let reader = message
643            .get_root_as_reader::<trading_capnp::query_account::Reader>()
644            .expect("Valid capnp message");
645
646        assert!(reader.has_trader_id());
647        assert!(reader.has_account_id());
648    }
649
650    #[rstest]
651    fn test_submit_order_serialization(command_id: UUID4, ts_init: UnixNanos, client_id: ClientId) {
652        let order = OrderTestBuilder::new(OrderType::Limit)
653            .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
654            .side(OrderSide::Buy)
655            .quantity(Quantity::new(1.0, 8))
656            .price(Price::new(50_000.0, 2))
657            .build();
658
659        let command = SubmitOrder::new(
660            order.trader_id(),
661            Some(client_id),
662            order.strategy_id(),
663            order.instrument_id(),
664            order.client_order_id(),
665            order.init_event().clone(),
666            None,
667            None,
668            None,
669            command_id,
670            ts_init,
671        );
672
673        let mut message = Builder::new_default();
674        {
675            let builder = message.init_root::<trading_capnp::submit_order::Builder>();
676            command.to_capnp(builder);
677        }
678
679        let reader = message
680            .get_root_as_reader::<trading_capnp::submit_order::Reader>()
681            .expect("Valid capnp message");
682
683        assert!(reader.has_header());
684        assert!(reader.has_order_init());
685    }
686
687    #[rstest]
688    fn test_submit_order_list_serialization(
689        command_id: UUID4,
690        ts_init: UnixNanos,
691        client_id: ClientId,
692    ) {
693        let order1 = OrderTestBuilder::new(OrderType::Limit)
694            .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
695            .client_order_id(ClientOrderId::from("O-001"))
696            .side(OrderSide::Buy)
697            .quantity(Quantity::new(1.0, 8))
698            .price(Price::new(50_000.0, 2))
699            .build();
700
701        let order2 = OrderTestBuilder::new(OrderType::Limit)
702            .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
703            .client_order_id(ClientOrderId::from("O-002"))
704            .side(OrderSide::Sell)
705            .quantity(Quantity::new(1.0, 8))
706            .price(Price::new(51_000.0, 2))
707            .build();
708
709        let orders = [order1.clone(), order2];
710        let order_inits: Vec<_> = orders.iter().map(|o| o.init_event().clone()).collect();
711        let order_list = OrderList::new(
712            OrderListId::new("OL-001"),
713            InstrumentId::from("BTCUSDT.BINANCE"),
714            order1.strategy_id(),
715            vec![order1.client_order_id(), orders[1].client_order_id()],
716            ts_init,
717        );
718
719        let command = SubmitOrderList::new(
720            order1.trader_id(),
721            Some(client_id),
722            order1.strategy_id(),
723            order_list,
724            order_inits,
725            None,
726            None,
727            None,
728            command_id,
729            ts_init,
730        );
731
732        let mut message = Builder::new_default();
733        {
734            let builder = message.init_root::<trading_capnp::submit_order_list::Builder>();
735            command.to_capnp(builder);
736        }
737
738        let reader = message
739            .get_root_as_reader::<trading_capnp::submit_order_list::Reader>()
740            .expect("Valid capnp message");
741
742        assert!(reader.has_header());
743        assert!(reader.has_order_inits());
744        assert_eq!(reader.get_order_inits().unwrap().len(), 2);
745    }
746
747    #[rstest]
748    fn test_trading_command_enum_serialization(
749        trader_id: TraderId,
750        strategy_id: StrategyId,
751        instrument_id: InstrumentId,
752        client_order_id: ClientOrderId,
753        command_id: UUID4,
754        ts_init: UnixNanos,
755    ) {
756        let cancel = CancelOrderBuilder::default()
757            .trader_id(trader_id)
758            .client_id(None)
759            .strategy_id(strategy_id)
760            .instrument_id(instrument_id)
761            .client_order_id(client_order_id)
762            .venue_order_id(None)
763            .command_id(command_id)
764            .ts_init(ts_init)
765            .params(None)
766            .build()
767            .unwrap();
768
769        let command = TradingCommand::CancelOrder(cancel);
770
771        let mut message = Builder::new_default();
772        {
773            let builder = message.init_root::<trading_capnp::trading_command::Builder>();
774            command.to_capnp(builder);
775        }
776
777        let reader = message
778            .get_root_as_reader::<trading_capnp::trading_command::Reader>()
779            .expect("Valid capnp message");
780
781        // Verify it's a cancel order variant
782        assert!(reader.has_cancel_order());
783    }
784}