nautilus_common/serialization/capnp/
trading.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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::{UUID4, UnixNanos};
19use nautilus_model::identifiers::{ClientId, InstrumentId, StrategyId, TraderId};
20use nautilus_serialization::{
21    capnp::{ToCapnp, order_side_to_capnp},
22    trading_capnp,
23};
24
25use crate::messages::execution::{
26    BatchCancelOrders, CancelAllOrders, CancelOrder, ModifyOrder, QueryAccount, QueryOrder,
27    SubmitOrder, SubmitOrderList, TradingCommand,
28};
29
30/// Helper function to populate a TradingCommandHeader builder
31fn populate_trading_command_header<'a>(
32    mut builder: trading_capnp::trading_command_header::Builder<'a>,
33    trader_id: &TraderId,
34    client_id: &ClientId,
35    strategy_id: &StrategyId,
36    instrument_id: &InstrumentId,
37    command_id: &UUID4,
38    ts_init: UnixNanos,
39) {
40    let trader_id_builder = builder.reborrow().init_trader_id();
41    trader_id.to_capnp(trader_id_builder);
42
43    let client_id_builder = builder.reborrow().init_client_id();
44    client_id.to_capnp(client_id_builder);
45
46    let strategy_id_builder = builder.reborrow().init_strategy_id();
47    strategy_id.to_capnp(strategy_id_builder);
48
49    let instrument_id_builder = builder.reborrow().init_instrument_id();
50    instrument_id.to_capnp(instrument_id_builder);
51
52    let command_id_builder = builder.reborrow().init_command_id();
53    command_id.to_capnp(command_id_builder);
54
55    let mut ts_init_builder = builder.reborrow().init_ts_init();
56    ts_init_builder.set_value(*ts_init);
57}
58
59impl<'a> ToCapnp<'a> for CancelOrder {
60    type Builder = trading_capnp::cancel_order::Builder<'a>;
61
62    fn to_capnp(&self, mut builder: Self::Builder) {
63        let header_builder = builder.reborrow().init_header();
64        populate_trading_command_header(
65            header_builder,
66            &self.trader_id,
67            &self.client_id,
68            &self.strategy_id,
69            &self.instrument_id,
70            &self.command_id,
71            self.ts_init,
72        );
73
74        let client_order_id_builder = builder.reborrow().init_client_order_id();
75        self.client_order_id.to_capnp(client_order_id_builder);
76
77        let venue_order_id_builder = builder.reborrow().init_venue_order_id();
78        self.venue_order_id.to_capnp(venue_order_id_builder);
79    }
80}
81
82impl<'a> ToCapnp<'a> for CancelAllOrders {
83    type Builder = trading_capnp::cancel_all_orders::Builder<'a>;
84
85    fn to_capnp(&self, mut builder: Self::Builder) {
86        let header_builder = builder.reborrow().init_header();
87        populate_trading_command_header(
88            header_builder,
89            &self.trader_id,
90            &self.client_id,
91            &self.strategy_id,
92            &self.instrument_id,
93            &self.command_id,
94            self.ts_init,
95        );
96
97        builder.set_order_side(order_side_to_capnp(self.order_side));
98    }
99}
100
101impl<'a> ToCapnp<'a> for BatchCancelOrders {
102    type Builder = trading_capnp::batch_cancel_orders::Builder<'a>;
103
104    fn to_capnp(&self, mut builder: Self::Builder) {
105        let header_builder = builder.reborrow().init_header();
106        populate_trading_command_header(
107            header_builder,
108            &self.trader_id,
109            &self.client_id,
110            &self.strategy_id,
111            &self.instrument_id,
112            &self.command_id,
113            self.ts_init,
114        );
115
116        let mut cancellations_builder = builder
117            .reborrow()
118            .init_cancellations(self.cancels.len() as u32);
119        for (i, cancel) in self.cancels.iter().enumerate() {
120            let cancel_builder = cancellations_builder.reborrow().get(i as u32);
121            cancel.to_capnp(cancel_builder);
122        }
123    }
124}
125
126impl<'a> ToCapnp<'a> for ModifyOrder {
127    type Builder = trading_capnp::modify_order::Builder<'a>;
128
129    fn to_capnp(&self, mut builder: Self::Builder) {
130        let header_builder = builder.reborrow().init_header();
131        populate_trading_command_header(
132            header_builder,
133            &self.trader_id,
134            &self.client_id,
135            &self.strategy_id,
136            &self.instrument_id,
137            &self.command_id,
138            self.ts_init,
139        );
140
141        let client_order_id_builder = builder.reborrow().init_client_order_id();
142        self.client_order_id.to_capnp(client_order_id_builder);
143
144        let venue_order_id_builder = builder.reborrow().init_venue_order_id();
145        self.venue_order_id.to_capnp(venue_order_id_builder);
146
147        if let Some(ref quantity) = self.quantity {
148            let quantity_builder = builder.reborrow().init_quantity();
149            quantity.to_capnp(quantity_builder);
150        }
151
152        if let Some(ref price) = self.price {
153            let price_builder = builder.reborrow().init_price();
154            price.to_capnp(price_builder);
155        }
156
157        if let Some(ref trigger_price) = self.trigger_price {
158            let trigger_price_builder = builder.reborrow().init_trigger_price();
159            trigger_price.to_capnp(trigger_price_builder);
160        }
161    }
162}
163
164impl<'a> ToCapnp<'a> for QueryOrder {
165    type Builder = trading_capnp::query_order::Builder<'a>;
166
167    fn to_capnp(&self, mut builder: Self::Builder) {
168        let header_builder = builder.reborrow().init_header();
169        populate_trading_command_header(
170            header_builder,
171            &self.trader_id,
172            &self.client_id,
173            &self.strategy_id,
174            &self.instrument_id,
175            &self.command_id,
176            self.ts_init,
177        );
178
179        let client_order_id_builder = builder.reborrow().init_client_order_id();
180        self.client_order_id.to_capnp(client_order_id_builder);
181
182        let venue_order_id_builder = builder.reborrow().init_venue_order_id();
183        self.venue_order_id.to_capnp(venue_order_id_builder);
184    }
185}
186
187impl<'a> ToCapnp<'a> for QueryAccount {
188    type Builder = trading_capnp::query_account::Builder<'a>;
189
190    fn to_capnp(&self, mut builder: Self::Builder) {
191        let trader_id_builder = builder.reborrow().init_trader_id();
192        self.trader_id.to_capnp(trader_id_builder);
193
194        let account_id_builder = builder.reborrow().init_account_id();
195        self.account_id.to_capnp(account_id_builder);
196
197        let command_id_builder = builder.reborrow().init_command_id();
198        self.command_id.to_capnp(command_id_builder);
199
200        let mut ts_init_builder = builder.reborrow().init_ts_init();
201        ts_init_builder.set_value(*self.ts_init);
202    }
203}
204
205impl<'a> ToCapnp<'a> for SubmitOrder {
206    type Builder = trading_capnp::submit_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,
214            &self.strategy_id,
215            &self.instrument_id,
216            &self.command_id,
217            self.ts_init,
218        );
219
220        let order_init = self.order.init_event();
221        let order_init_builder = builder.reborrow().init_order_init();
222        order_init.to_capnp(order_init_builder);
223
224        if let Some(ref position_id) = self.position_id {
225            let position_id_builder = builder.reborrow().init_position_id();
226            position_id.to_capnp(position_id_builder);
227        }
228    }
229}
230
231impl<'a> ToCapnp<'a> for SubmitOrderList {
232    type Builder = trading_capnp::submit_order_list::Builder<'a>;
233
234    fn to_capnp(&self, mut builder: Self::Builder) {
235        let header_builder = builder.reborrow().init_header();
236        populate_trading_command_header(
237            header_builder,
238            &self.trader_id,
239            &self.client_id,
240            &self.strategy_id,
241            &self.instrument_id,
242            &self.command_id,
243            self.ts_init,
244        );
245
246        let mut order_inits_builder = builder
247            .reborrow()
248            .init_order_inits(self.order_list.orders.len() as u32);
249        for (i, order) in self.order_list.orders.iter().enumerate() {
250            let order_init = order.init_event();
251            let order_init_builder = order_inits_builder.reborrow().get(i as u32);
252            order_init.to_capnp(order_init_builder);
253        }
254
255        if let Some(ref position_id) = self.position_id {
256            let position_id_builder = builder.reborrow().init_position_id();
257            position_id.to_capnp(position_id_builder);
258        }
259    }
260}
261
262impl<'a> ToCapnp<'a> for TradingCommand {
263    type Builder = trading_capnp::trading_command::Builder<'a>;
264
265    fn to_capnp(&self, builder: Self::Builder) {
266        match self {
267            Self::SubmitOrder(command) => {
268                let submit_builder = builder.init_submit_order();
269                command.to_capnp(submit_builder);
270            }
271            Self::SubmitOrderList(command) => {
272                let submit_list_builder = builder.init_submit_order_list();
273                command.to_capnp(submit_list_builder);
274            }
275            Self::ModifyOrder(command) => {
276                let modify_builder = builder.init_modify_order();
277                command.to_capnp(modify_builder);
278            }
279            Self::CancelOrder(command) => {
280                let cancel_builder = builder.init_cancel_order();
281                command.to_capnp(cancel_builder);
282            }
283            Self::CancelAllOrders(command) => {
284                let cancel_all_builder = builder.init_cancel_all_orders();
285                command.to_capnp(cancel_all_builder);
286            }
287            Self::BatchCancelOrders(command) => {
288                let batch_cancel_builder = builder.init_batch_cancel_orders();
289                command.to_capnp(batch_cancel_builder);
290            }
291            Self::QueryOrder(command) => {
292                let query_builder = builder.init_query_order();
293                command.to_capnp(query_builder);
294            }
295            Self::QueryAccount(command) => {
296                let query_builder = builder.init_query_account();
297                command.to_capnp(query_builder);
298            }
299        }
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use capnp::message::Builder;
306    use nautilus_core::UnixNanos;
307    use nautilus_model::{
308        enums::{OrderSide, OrderType},
309        identifiers::{AccountId, ClientId, ClientOrderId, InstrumentId, OrderListId},
310        orders::{Order, OrderList, OrderTestBuilder},
311        types::{Price, Quantity},
312    };
313    use rstest::*;
314
315    use super::*;
316    use crate::messages::execution::{
317        cancel::{BatchCancelOrdersBuilder, CancelAllOrdersBuilder, CancelOrderBuilder},
318        modify::ModifyOrderBuilder,
319        query::{QueryAccountBuilder, QueryOrderBuilder},
320    };
321
322    #[fixture]
323    fn command_id() -> UUID4 {
324        UUID4::new()
325    }
326
327    #[fixture]
328    fn ts_init() -> UnixNanos {
329        UnixNanos::default()
330    }
331
332    #[fixture]
333    fn client_id() -> ClientId {
334        ClientId::new("TEST")
335    }
336
337    #[rstest]
338    fn test_cancel_order_serialization(command_id: UUID4, ts_init: UnixNanos) {
339        let command = CancelOrderBuilder::default()
340            .command_id(command_id)
341            .ts_init(ts_init)
342            .build()
343            .unwrap();
344
345        let mut message = Builder::new_default();
346        {
347            let builder = message.init_root::<trading_capnp::cancel_order::Builder>();
348            command.to_capnp(builder);
349        }
350
351        let reader = message
352            .get_root_as_reader::<trading_capnp::cancel_order::Reader>()
353            .expect("Valid capnp message");
354
355        // Verify header is populated
356        assert!(reader.has_header());
357        let header = reader.get_header().unwrap();
358        assert!(header.has_trader_id());
359        assert!(header.has_client_id());
360        assert!(header.has_strategy_id());
361        assert!(header.has_instrument_id());
362        assert!(header.has_command_id());
363        assert!(header.has_ts_init());
364    }
365
366    #[rstest]
367    fn test_cancel_all_orders_serialization(command_id: UUID4, ts_init: UnixNanos) {
368        let command = CancelAllOrdersBuilder::default()
369            .order_side(OrderSide::Buy)
370            .command_id(command_id)
371            .ts_init(ts_init)
372            .build()
373            .unwrap();
374
375        let mut message = Builder::new_default();
376        {
377            let builder = message.init_root::<trading_capnp::cancel_all_orders::Builder>();
378            command.to_capnp(builder);
379        }
380
381        let reader = message
382            .get_root_as_reader::<trading_capnp::cancel_all_orders::Reader>()
383            .expect("Valid capnp message");
384
385        assert!(reader.has_header());
386    }
387
388    #[rstest]
389    fn test_batch_cancel_orders_serialization(command_id: UUID4, ts_init: UnixNanos) {
390        let cancel1 = CancelOrderBuilder::default()
391            .client_order_id(ClientOrderId::new("O-001"))
392            .command_id(UUID4::new())
393            .ts_init(ts_init)
394            .build()
395            .unwrap();
396
397        let cancel2 = CancelOrderBuilder::default()
398            .client_order_id(ClientOrderId::new("O-002"))
399            .command_id(UUID4::new())
400            .ts_init(ts_init)
401            .build()
402            .unwrap();
403
404        let command = BatchCancelOrdersBuilder::default()
405            .cancels(vec![cancel1, cancel2])
406            .command_id(command_id)
407            .ts_init(ts_init)
408            .build()
409            .unwrap();
410
411        let mut message = Builder::new_default();
412        {
413            let builder = message.init_root::<trading_capnp::batch_cancel_orders::Builder>();
414            command.to_capnp(builder);
415        }
416
417        let reader = message
418            .get_root_as_reader::<trading_capnp::batch_cancel_orders::Reader>()
419            .expect("Valid capnp message");
420
421        assert!(reader.has_header());
422        assert!(reader.has_cancellations());
423        assert_eq!(reader.get_cancellations().unwrap().len(), 2);
424    }
425
426    #[rstest]
427    fn test_modify_order_serialization(command_id: UUID4, ts_init: UnixNanos) {
428        let command = ModifyOrderBuilder::default()
429            .quantity(Some(Quantity::new(100.0, 0)))
430            .price(Some(Price::new(50_000.0, 2)))
431            .trigger_price(Some(Price::new(49_000.0, 2)))
432            .command_id(command_id)
433            .ts_init(ts_init)
434            .build()
435            .unwrap();
436
437        let mut message = Builder::new_default();
438        {
439            let builder = message.init_root::<trading_capnp::modify_order::Builder>();
440            command.to_capnp(builder);
441        }
442
443        let reader = message
444            .get_root_as_reader::<trading_capnp::modify_order::Reader>()
445            .expect("Valid capnp message");
446
447        assert!(reader.has_header());
448        assert!(reader.has_quantity());
449        assert!(reader.has_price());
450        assert!(reader.has_trigger_price());
451    }
452
453    #[rstest]
454    fn test_query_order_serialization(command_id: UUID4, ts_init: UnixNanos) {
455        let command = QueryOrderBuilder::default()
456            .command_id(command_id)
457            .ts_init(ts_init)
458            .build()
459            .unwrap();
460
461        let mut message = Builder::new_default();
462        {
463            let builder = message.init_root::<trading_capnp::query_order::Builder>();
464            command.to_capnp(builder);
465        }
466
467        let reader = message
468            .get_root_as_reader::<trading_capnp::query_order::Reader>()
469            .expect("Valid capnp message");
470
471        assert!(reader.has_header());
472    }
473
474    #[rstest]
475    fn test_query_account_serialization(command_id: UUID4, ts_init: UnixNanos) {
476        let command = QueryAccountBuilder::default()
477            .account_id(AccountId::new("ACC-001"))
478            .command_id(command_id)
479            .ts_init(ts_init)
480            .build()
481            .unwrap();
482
483        let mut message = Builder::new_default();
484        {
485            let builder = message.init_root::<trading_capnp::query_account::Builder>();
486            command.to_capnp(builder);
487        }
488
489        let reader = message
490            .get_root_as_reader::<trading_capnp::query_account::Reader>()
491            .expect("Valid capnp message");
492
493        assert!(reader.has_trader_id());
494        assert!(reader.has_account_id());
495    }
496
497    #[rstest]
498    fn test_submit_order_serialization(command_id: UUID4, ts_init: UnixNanos, client_id: ClientId) {
499        let order = OrderTestBuilder::new(OrderType::Limit)
500            .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
501            .side(OrderSide::Buy)
502            .quantity(Quantity::new(1.0, 8))
503            .price(Price::new(50_000.0, 2))
504            .build();
505
506        let command = SubmitOrder::new(
507            order.trader_id(),
508            client_id,
509            order.strategy_id(),
510            order.instrument_id(),
511            order.client_order_id(),
512            order.venue_order_id().unwrap_or_default(),
513            order,
514            None,
515            None,
516            None,
517            command_id,
518            ts_init,
519        )
520        .unwrap();
521
522        let mut message = Builder::new_default();
523        {
524            let builder = message.init_root::<trading_capnp::submit_order::Builder>();
525            command.to_capnp(builder);
526        }
527
528        let reader = message
529            .get_root_as_reader::<trading_capnp::submit_order::Reader>()
530            .expect("Valid capnp message");
531
532        assert!(reader.has_header());
533        assert!(reader.has_order_init());
534    }
535
536    #[rstest]
537    fn test_submit_order_list_serialization(
538        command_id: UUID4,
539        ts_init: UnixNanos,
540        client_id: ClientId,
541    ) {
542        let order1 = OrderTestBuilder::new(OrderType::Limit)
543            .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
544            .side(OrderSide::Buy)
545            .quantity(Quantity::new(1.0, 8))
546            .price(Price::new(50_000.0, 2))
547            .build();
548
549        let order2 = OrderTestBuilder::new(OrderType::Limit)
550            .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
551            .side(OrderSide::Sell)
552            .quantity(Quantity::new(1.0, 8))
553            .price(Price::new(51_000.0, 2))
554            .build();
555
556        let order_list = OrderList::new(
557            OrderListId::new("OL-001"),
558            InstrumentId::from("BTCUSDT.BINANCE"),
559            order1.strategy_id(),
560            vec![order1.clone(), order2],
561            ts_init,
562        );
563
564        let command = SubmitOrderList::new(
565            order1.trader_id(),
566            client_id,
567            order1.strategy_id(),
568            order1.instrument_id(),
569            order1.client_order_id(),
570            order1.venue_order_id().unwrap_or_default(),
571            order_list,
572            None,
573            None,
574            command_id,
575            ts_init,
576        )
577        .unwrap();
578
579        let mut message = Builder::new_default();
580        {
581            let builder = message.init_root::<trading_capnp::submit_order_list::Builder>();
582            command.to_capnp(builder);
583        }
584
585        let reader = message
586            .get_root_as_reader::<trading_capnp::submit_order_list::Reader>()
587            .expect("Valid capnp message");
588
589        assert!(reader.has_header());
590        assert!(reader.has_order_inits());
591        assert_eq!(reader.get_order_inits().unwrap().len(), 2);
592    }
593
594    #[rstest]
595    fn test_trading_command_enum_serialization(command_id: UUID4, ts_init: UnixNanos) {
596        let cancel = CancelOrderBuilder::default()
597            .command_id(command_id)
598            .ts_init(ts_init)
599            .build()
600            .unwrap();
601
602        let command = TradingCommand::CancelOrder(cancel);
603
604        let mut message = Builder::new_default();
605        {
606            let builder = message.init_root::<trading_capnp::trading_command::Builder>();
607            command.to_capnp(builder);
608        }
609
610        let reader = message
611            .get_root_as_reader::<trading_capnp::trading_command::Reader>()
612            .expect("Valid capnp message");
613
614        // Verify it's a cancel order variant
615        assert!(reader.has_cancel_order());
616    }
617}