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