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 = self.order.init_event();
264        let order_init_builder = builder.reborrow().init_order_init();
265        order_init.to_capnp(order_init_builder);
266
267        if let Some(ref position_id) = self.position_id {
268            let position_id_builder = builder.reborrow().init_position_id();
269            position_id.to_capnp(position_id_builder);
270        }
271
272        if let Some(ref params) = self.params {
273            let params_builder = builder.reborrow().init_params();
274            populate_string_map(params_builder, params);
275        }
276    }
277}
278
279impl<'a> ToCapnp<'a> for SubmitOrderList {
280    type Builder = trading_capnp::submit_order_list::Builder<'a>;
281
282    fn to_capnp(&self, mut builder: Self::Builder) {
283        let header_builder = builder.reborrow().init_header();
284        populate_trading_command_header(
285            header_builder,
286            &self.trader_id,
287            self.client_id.as_ref(),
288            &self.strategy_id,
289            &self.instrument_id,
290            &self.command_id,
291            self.ts_init,
292        );
293
294        let mut order_inits_builder = builder
295            .reborrow()
296            .init_order_inits(self.order_list.orders.len() as u32);
297        for (i, order) in self.order_list.orders.iter().enumerate() {
298            let order_init = order.init_event();
299            let order_init_builder = order_inits_builder.reborrow().get(i as u32);
300            order_init.to_capnp(order_init_builder);
301        }
302
303        if let Some(ref position_id) = self.position_id {
304            let position_id_builder = builder.reborrow().init_position_id();
305            position_id.to_capnp(position_id_builder);
306        }
307
308        if let Some(ref params) = self.params {
309            let params_builder = builder.reborrow().init_params();
310            populate_string_map(params_builder, params);
311        }
312    }
313}
314
315impl<'a> ToCapnp<'a> for TradingCommand {
316    type Builder = trading_capnp::trading_command::Builder<'a>;
317
318    fn to_capnp(&self, builder: Self::Builder) {
319        match self {
320            Self::SubmitOrder(command) => {
321                let submit_builder = builder.init_submit_order();
322                command.to_capnp(submit_builder);
323            }
324            Self::SubmitOrderList(command) => {
325                let submit_list_builder = builder.init_submit_order_list();
326                command.to_capnp(submit_list_builder);
327            }
328            Self::ModifyOrder(command) => {
329                let modify_builder = builder.init_modify_order();
330                command.to_capnp(modify_builder);
331            }
332            Self::CancelOrder(command) => {
333                let cancel_builder = builder.init_cancel_order();
334                command.to_capnp(cancel_builder);
335            }
336            Self::CancelAllOrders(command) => {
337                let cancel_all_builder = builder.init_cancel_all_orders();
338                command.to_capnp(cancel_all_builder);
339            }
340            Self::BatchCancelOrders(command) => {
341                let batch_cancel_builder = builder.init_batch_cancel_orders();
342                command.to_capnp(batch_cancel_builder);
343            }
344            Self::QueryOrder(command) => {
345                let query_builder = builder.init_query_order();
346                command.to_capnp(query_builder);
347            }
348            Self::QueryAccount(command) => {
349                let query_builder = builder.init_query_account();
350                command.to_capnp(query_builder);
351            }
352        }
353    }
354}
355
356#[cfg(test)]
357mod tests {
358    use capnp::message::Builder;
359    use nautilus_core::UnixNanos;
360    use nautilus_model::{
361        enums::{OrderSide, OrderType},
362        identifiers::{
363            AccountId, ClientId, ClientOrderId, InstrumentId, OrderListId, StrategyId, TraderId,
364        },
365        orders::{Order, OrderList, OrderTestBuilder},
366        stubs::TestDefault,
367        types::{Price, Quantity},
368    };
369    use rstest::*;
370
371    use super::*;
372    use crate::messages::execution::{
373        cancel::{BatchCancelOrdersBuilder, CancelAllOrdersBuilder, CancelOrderBuilder},
374        modify::ModifyOrderBuilder,
375        query::{QueryAccountBuilder, QueryOrderBuilder},
376    };
377
378    #[fixture]
379    fn trader_id() -> TraderId {
380        TraderId::test_default()
381    }
382
383    #[fixture]
384    fn strategy_id() -> StrategyId {
385        StrategyId::test_default()
386    }
387
388    #[fixture]
389    fn instrument_id() -> InstrumentId {
390        InstrumentId::test_default()
391    }
392
393    #[fixture]
394    fn client_order_id() -> ClientOrderId {
395        ClientOrderId::test_default()
396    }
397
398    #[fixture]
399    fn command_id() -> UUID4 {
400        UUID4::new()
401    }
402
403    #[fixture]
404    fn ts_init() -> UnixNanos {
405        UnixNanos::default()
406    }
407
408    #[fixture]
409    fn client_id() -> ClientId {
410        ClientId::new("TEST")
411    }
412
413    #[rstest]
414    fn test_cancel_order_serialization(
415        trader_id: TraderId,
416        client_id: ClientId,
417        strategy_id: StrategyId,
418        instrument_id: InstrumentId,
419        client_order_id: ClientOrderId,
420        command_id: UUID4,
421        ts_init: UnixNanos,
422    ) {
423        let command = CancelOrderBuilder::default()
424            .trader_id(trader_id)
425            .client_id(Some(client_id))
426            .strategy_id(strategy_id)
427            .instrument_id(instrument_id)
428            .client_order_id(client_order_id)
429            .venue_order_id(None)
430            .command_id(command_id)
431            .ts_init(ts_init)
432            .params(None)
433            .build()
434            .unwrap();
435
436        let mut message = Builder::new_default();
437        {
438            let builder = message.init_root::<trading_capnp::cancel_order::Builder>();
439            command.to_capnp(builder);
440        }
441
442        let reader = message
443            .get_root_as_reader::<trading_capnp::cancel_order::Reader>()
444            .expect("Valid capnp message");
445
446        // Verify header is populated
447        assert!(reader.has_header());
448        let header = reader.get_header().unwrap();
449        assert!(header.has_trader_id());
450        assert!(header.has_client_id());
451        assert!(header.has_strategy_id());
452        assert!(header.has_instrument_id());
453        assert!(header.has_command_id());
454        assert!(header.has_ts_init());
455    }
456
457    #[rstest]
458    fn test_cancel_all_orders_serialization(
459        trader_id: TraderId,
460        strategy_id: StrategyId,
461        instrument_id: InstrumentId,
462        command_id: UUID4,
463        ts_init: UnixNanos,
464    ) {
465        let command = CancelAllOrdersBuilder::default()
466            .trader_id(trader_id)
467            .client_id(None)
468            .strategy_id(strategy_id)
469            .instrument_id(instrument_id)
470            .order_side(OrderSide::Buy)
471            .command_id(command_id)
472            .ts_init(ts_init)
473            .params(None)
474            .build()
475            .unwrap();
476
477        let mut message = Builder::new_default();
478        {
479            let builder = message.init_root::<trading_capnp::cancel_all_orders::Builder>();
480            command.to_capnp(builder);
481        }
482
483        let reader = message
484            .get_root_as_reader::<trading_capnp::cancel_all_orders::Reader>()
485            .expect("Valid capnp message");
486
487        assert!(reader.has_header());
488    }
489
490    #[rstest]
491    fn test_batch_cancel_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 cancel1 = CancelOrderBuilder::default()
499            .trader_id(trader_id)
500            .client_id(None)
501            .strategy_id(strategy_id)
502            .instrument_id(instrument_id)
503            .client_order_id(ClientOrderId::new("O-001"))
504            .venue_order_id(None)
505            .command_id(UUID4::new())
506            .ts_init(ts_init)
507            .params(None)
508            .build()
509            .unwrap();
510
511        let cancel2 = CancelOrderBuilder::default()
512            .trader_id(trader_id)
513            .client_id(None)
514            .strategy_id(strategy_id)
515            .instrument_id(instrument_id)
516            .client_order_id(ClientOrderId::new("O-002"))
517            .venue_order_id(None)
518            .command_id(UUID4::new())
519            .ts_init(ts_init)
520            .params(None)
521            .build()
522            .unwrap();
523
524        let command = BatchCancelOrdersBuilder::default()
525            .trader_id(trader_id)
526            .client_id(None)
527            .strategy_id(strategy_id)
528            .instrument_id(instrument_id)
529            .cancels(vec![cancel1, cancel2])
530            .command_id(command_id)
531            .ts_init(ts_init)
532            .params(None)
533            .build()
534            .unwrap();
535
536        let mut message = Builder::new_default();
537        {
538            let builder = message.init_root::<trading_capnp::batch_cancel_orders::Builder>();
539            command.to_capnp(builder);
540        }
541
542        let reader = message
543            .get_root_as_reader::<trading_capnp::batch_cancel_orders::Reader>()
544            .expect("Valid capnp message");
545
546        assert!(reader.has_header());
547        assert!(reader.has_cancellations());
548        assert_eq!(reader.get_cancellations().unwrap().len(), 2);
549    }
550
551    #[rstest]
552    fn test_modify_order_serialization(
553        trader_id: TraderId,
554        strategy_id: StrategyId,
555        instrument_id: InstrumentId,
556        client_order_id: ClientOrderId,
557        command_id: UUID4,
558        ts_init: UnixNanos,
559    ) {
560        let command = ModifyOrderBuilder::default()
561            .trader_id(trader_id)
562            .client_id(None)
563            .strategy_id(strategy_id)
564            .instrument_id(instrument_id)
565            .client_order_id(client_order_id)
566            .venue_order_id(None)
567            .quantity(Some(Quantity::new(100.0, 0)))
568            .price(Some(Price::new(50_000.0, 2)))
569            .trigger_price(Some(Price::new(49_000.0, 2)))
570            .command_id(command_id)
571            .ts_init(ts_init)
572            .params(None)
573            .build()
574            .unwrap();
575
576        let mut message = Builder::new_default();
577        {
578            let builder = message.init_root::<trading_capnp::modify_order::Builder>();
579            command.to_capnp(builder);
580        }
581
582        let reader = message
583            .get_root_as_reader::<trading_capnp::modify_order::Reader>()
584            .expect("Valid capnp message");
585
586        assert!(reader.has_header());
587        assert!(reader.has_quantity());
588        assert!(reader.has_price());
589        assert!(reader.has_trigger_price());
590    }
591
592    #[rstest]
593    fn test_query_order_serialization(
594        trader_id: TraderId,
595        strategy_id: StrategyId,
596        instrument_id: InstrumentId,
597        client_order_id: ClientOrderId,
598        command_id: UUID4,
599        ts_init: UnixNanos,
600    ) {
601        let command = QueryOrderBuilder::default()
602            .trader_id(trader_id)
603            .client_id(None)
604            .strategy_id(strategy_id)
605            .instrument_id(instrument_id)
606            .client_order_id(client_order_id)
607            .venue_order_id(None)
608            .command_id(command_id)
609            .ts_init(ts_init)
610            .build()
611            .unwrap();
612
613        let mut message = Builder::new_default();
614        {
615            let builder = message.init_root::<trading_capnp::query_order::Builder>();
616            command.to_capnp(builder);
617        }
618
619        let reader = message
620            .get_root_as_reader::<trading_capnp::query_order::Reader>()
621            .expect("Valid capnp message");
622
623        assert!(reader.has_header());
624    }
625
626    #[rstest]
627    fn test_query_account_serialization(
628        trader_id: TraderId,
629        command_id: UUID4,
630        ts_init: UnixNanos,
631    ) {
632        let command = QueryAccountBuilder::default()
633            .trader_id(trader_id)
634            .client_id(None)
635            .account_id(AccountId::new("ACC-001"))
636            .command_id(command_id)
637            .ts_init(ts_init)
638            .build()
639            .unwrap();
640
641        let mut message = Builder::new_default();
642        {
643            let builder = message.init_root::<trading_capnp::query_account::Builder>();
644            command.to_capnp(builder);
645        }
646
647        let reader = message
648            .get_root_as_reader::<trading_capnp::query_account::Reader>()
649            .expect("Valid capnp message");
650
651        assert!(reader.has_trader_id());
652        assert!(reader.has_account_id());
653    }
654
655    #[rstest]
656    fn test_submit_order_serialization(command_id: UUID4, ts_init: UnixNanos, client_id: ClientId) {
657        let order = OrderTestBuilder::new(OrderType::Limit)
658            .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
659            .side(OrderSide::Buy)
660            .quantity(Quantity::new(1.0, 8))
661            .price(Price::new(50_000.0, 2))
662            .build();
663
664        let command = SubmitOrder::new(
665            order.trader_id(),
666            Some(client_id),
667            order.strategy_id(),
668            order.instrument_id(),
669            order,
670            None,
671            None,
672            None,
673            command_id,
674            ts_init,
675        );
676
677        let mut message = Builder::new_default();
678        {
679            let builder = message.init_root::<trading_capnp::submit_order::Builder>();
680            command.to_capnp(builder);
681        }
682
683        let reader = message
684            .get_root_as_reader::<trading_capnp::submit_order::Reader>()
685            .expect("Valid capnp message");
686
687        assert!(reader.has_header());
688        assert!(reader.has_order_init());
689    }
690
691    #[rstest]
692    fn test_submit_order_list_serialization(
693        command_id: UUID4,
694        ts_init: UnixNanos,
695        client_id: ClientId,
696    ) {
697        let order1 = OrderTestBuilder::new(OrderType::Limit)
698            .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
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            .side(OrderSide::Sell)
707            .quantity(Quantity::new(1.0, 8))
708            .price(Price::new(51_000.0, 2))
709            .build();
710
711        let order_list = OrderList::new(
712            OrderListId::new("OL-001"),
713            InstrumentId::from("BTCUSDT.BINANCE"),
714            order1.strategy_id(),
715            vec![order1.clone(), order2],
716            ts_init,
717        );
718
719        let command = SubmitOrderList::new(
720            order1.trader_id(),
721            Some(client_id),
722            order1.strategy_id(),
723            order1.instrument_id(),
724            order_list,
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}