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