Skip to main content

nautilus_common/messages/execution/
mod.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//! Execution specific messages such as order commands.
17
18pub mod cancel;
19pub mod modify;
20pub mod query;
21pub mod report;
22pub mod submit;
23
24use std::fmt::Display;
25
26use nautilus_core::{Params, UnixNanos};
27use nautilus_model::{
28    identifiers::{ClientId, InstrumentId, StrategyId},
29    reports::{ExecutionMassStatus, FillReport, OrderStatusReport, PositionStatusReport},
30};
31use serde::{Deserialize, Deserializer, Serialize, de::Error as _};
32use strum::Display as StrumDisplay;
33
34pub use self::{
35    cancel::{BatchCancelOrders, CancelAllOrders, CancelOrder},
36    modify::{BatchModifyOrders, ModifyOrder},
37    query::{QueryAccount, QueryOrder},
38    report::{
39        GenerateExecutionMassStatus, GenerateExecutionMassStatusBuilder, GenerateFillReports,
40        GenerateFillReportsBuilder, GenerateOrderStatusReport, GenerateOrderStatusReportBuilder,
41        GenerateOrderStatusReports, GenerateOrderStatusReportsBuilder,
42        GeneratePositionStatusReports, GeneratePositionStatusReportsBuilder,
43    },
44    submit::{SubmitOrder, SubmitOrderList},
45};
46
47/// Parameter indicating that a conditional order should close the whole position at trigger time.
48pub const PARAMS_CLOSE_POSITION: &str = "close_position";
49
50/// Execution report variants for reconciliation.
51#[derive(Clone, Debug)]
52pub enum ExecutionReport {
53    Order(Box<OrderStatusReport>),
54    Fill(Box<FillReport>),
55    OrderWithFills(Box<OrderStatusReport>, Vec<FillReport>),
56    Position(Box<PositionStatusReport>),
57    MassStatus(Box<ExecutionMassStatus>),
58}
59
60impl Display for ExecutionReport {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        match self {
63            Self::Order(report) => write!(f, "{report}"),
64            Self::Fill(report) => write!(f, "{report}"),
65            Self::OrderWithFills(report, fills) => {
66                write!(f, "OrderWithFills(order={report}, fills=[")?;
67                for (index, fill) in fills.iter().enumerate() {
68                    if index > 0 {
69                        f.write_str(", ")?;
70                    }
71                    write!(f, "{fill}")?;
72                }
73                f.write_str("])")
74            }
75            Self::Position(report) => write!(f, "{report}"),
76            Self::MassStatus(report) => write!(f, "{report}"),
77        }
78    }
79}
80
81/// An execution command sent to an execution client.
82///
83/// Serializes as the contained command object. Deserialization requires its string `type` field to
84/// select the variant.
85#[expect(clippy::large_enum_variant)]
86#[derive(Clone, Debug, Eq, PartialEq, Serialize, StrumDisplay)]
87#[serde(untagged)]
88pub enum TradingCommand {
89    #[strum(transparent)]
90    SubmitOrder(SubmitOrder),
91    #[strum(transparent)]
92    SubmitOrderList(SubmitOrderList),
93    #[strum(transparent)]
94    ModifyOrder(ModifyOrder),
95    #[strum(transparent)]
96    ModifyOrders(BatchModifyOrders),
97    #[strum(transparent)]
98    CancelOrder(CancelOrder),
99    #[strum(transparent)]
100    CancelOrders(BatchCancelOrders),
101    #[strum(transparent)]
102    CancelAllOrders(CancelAllOrders),
103    #[strum(transparent)]
104    QueryOrder(QueryOrder),
105    #[strum(transparent)]
106    QueryAccount(QueryAccount),
107}
108
109#[derive(Deserialize)]
110enum TradingCommandType {
111    SubmitOrder,
112    SubmitOrderList,
113    ModifyOrder,
114    BatchModifyOrders,
115    CancelOrder,
116    BatchCancelOrders,
117    CancelAllOrders,
118    QueryOrder,
119    QueryAccount,
120}
121
122#[derive(Deserialize)]
123struct TradingCommandHeader {
124    #[serde(rename = "type")]
125    command_type: TradingCommandType,
126}
127
128impl<'de> Deserialize<'de> for TradingCommand {
129    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
130    where
131        D: Deserializer<'de>,
132    {
133        let value = serde_json::Value::deserialize(deserializer)?;
134        let command_type = TradingCommandHeader::deserialize(&value)
135            .map_err(D::Error::custom)?
136            .command_type;
137
138        match command_type {
139            TradingCommandType::SubmitOrder => serde_json::from_value(value)
140                .map(Self::SubmitOrder)
141                .map_err(D::Error::custom),
142            TradingCommandType::SubmitOrderList => serde_json::from_value(value)
143                .map(Self::SubmitOrderList)
144                .map_err(D::Error::custom),
145            TradingCommandType::ModifyOrder => serde_json::from_value(value)
146                .map(Self::ModifyOrder)
147                .map_err(D::Error::custom),
148            TradingCommandType::BatchModifyOrders => serde_json::from_value(value)
149                .map(Self::ModifyOrders)
150                .map_err(D::Error::custom),
151            TradingCommandType::CancelOrder => serde_json::from_value(value)
152                .map(Self::CancelOrder)
153                .map_err(D::Error::custom),
154            TradingCommandType::BatchCancelOrders => serde_json::from_value(value)
155                .map(Self::CancelOrders)
156                .map_err(D::Error::custom),
157            TradingCommandType::CancelAllOrders => serde_json::from_value(value)
158                .map(Self::CancelAllOrders)
159                .map_err(D::Error::custom),
160            TradingCommandType::QueryOrder => serde_json::from_value(value)
161                .map(Self::QueryOrder)
162                .map_err(D::Error::custom),
163            TradingCommandType::QueryAccount => serde_json::from_value(value)
164                .map(Self::QueryAccount)
165                .map_err(D::Error::custom),
166        }
167    }
168}
169
170impl TradingCommand {
171    #[must_use]
172    pub const fn client_id(&self) -> Option<ClientId> {
173        match self {
174            Self::SubmitOrder(command) => command.client_id,
175            Self::SubmitOrderList(command) => command.client_id,
176            Self::ModifyOrder(command) => command.client_id,
177            Self::ModifyOrders(command) => command.client_id,
178            Self::CancelOrder(command) => command.client_id,
179            Self::CancelOrders(command) => command.client_id,
180            Self::CancelAllOrders(command) => command.client_id,
181            Self::QueryOrder(command) => command.client_id,
182            Self::QueryAccount(command) => command.client_id,
183        }
184    }
185
186    /// Returns the instrument ID for the command.
187    ///
188    /// # Panics
189    ///
190    /// Panics if the command is `QueryAccount` which does not have an instrument ID.
191    #[must_use]
192    pub const fn instrument_id(&self) -> InstrumentId {
193        match self {
194            Self::SubmitOrder(command) => command.instrument_id,
195            Self::SubmitOrderList(command) => command.instrument_id,
196            Self::ModifyOrder(command) => command.instrument_id,
197            Self::ModifyOrders(command) => command.instrument_id,
198            Self::CancelOrder(command) => command.instrument_id,
199            Self::CancelOrders(command) => command.instrument_id,
200            Self::CancelAllOrders(command) => command.instrument_id,
201            Self::QueryOrder(command) => command.instrument_id,
202            Self::QueryAccount(_) => panic!("No instrument ID for command"),
203        }
204    }
205
206    #[must_use]
207    pub const fn ts_init(&self) -> UnixNanos {
208        match self {
209            Self::SubmitOrder(command) => command.ts_init,
210            Self::SubmitOrderList(command) => command.ts_init,
211            Self::ModifyOrder(command) => command.ts_init,
212            Self::ModifyOrders(command) => command.ts_init,
213            Self::CancelOrder(command) => command.ts_init,
214            Self::CancelOrders(command) => command.ts_init,
215            Self::CancelAllOrders(command) => command.ts_init,
216            Self::QueryOrder(command) => command.ts_init,
217            Self::QueryAccount(command) => command.ts_init,
218        }
219    }
220
221    #[must_use]
222    pub const fn strategy_id(&self) -> Option<StrategyId> {
223        match self {
224            Self::SubmitOrder(command) => Some(command.strategy_id),
225            Self::SubmitOrderList(command) => Some(command.strategy_id),
226            Self::ModifyOrder(command) => Some(command.strategy_id),
227            Self::ModifyOrders(command) => Some(command.strategy_id),
228            Self::CancelOrder(command) => Some(command.strategy_id),
229            Self::CancelOrders(command) => Some(command.strategy_id),
230            Self::CancelAllOrders(command) => Some(command.strategy_id),
231            Self::QueryOrder(command) => Some(command.strategy_id),
232            Self::QueryAccount(_) => None,
233        }
234    }
235
236    #[must_use]
237    pub const fn params(&self) -> Option<&Params> {
238        match self {
239            Self::SubmitOrder(command) => command.params.as_ref(),
240            Self::SubmitOrderList(command) => command.params.as_ref(),
241            Self::ModifyOrder(command) => command.params.as_ref(),
242            Self::ModifyOrders(command) => command.params.as_ref(),
243            Self::CancelOrder(command) => command.params.as_ref(),
244            Self::CancelOrders(command) => command.params.as_ref(),
245            Self::CancelAllOrders(command) => command.params.as_ref(),
246            Self::QueryOrder(command) => command.params.as_ref(),
247            Self::QueryAccount(command) => command.params.as_ref(),
248        }
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use nautilus_core::{UUID4, UnixNanos};
255    use nautilus_model::{
256        enums::{LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce},
257        events::OrderInitialized,
258        identifiers::{AccountId, OrderListId, TradeId, TraderId, VenueOrderId},
259        orders::OrderList,
260        reports::{FillReport, OrderStatusReport},
261        types::{Currency, Money, Price, Quantity},
262    };
263    use rstest::rstest;
264
265    use super::*;
266
267    fn trading_commands() -> Vec<TradingCommand> {
268        let trader_id = TraderId::from("TRADER-001");
269        let client_id = Some(ClientId::from("EXTERNAL"));
270        let strategy_id = StrategyId::from("STRATEGY-001");
271        let instrument_id = InstrumentId::from("AUD/USD.SIM");
272        let ts_init = UnixNanos::from(1_000_000_000);
273        let order_init = OrderInitialized::default();
274        let client_order_id = order_init.client_order_id;
275        let submit_order = SubmitOrder::new(
276            trader_id,
277            client_id,
278            strategy_id,
279            instrument_id,
280            client_order_id,
281            order_init.clone(),
282            None,
283            None,
284            None,
285            UUID4::from("00000000-0000-4000-8000-000000000001"),
286            ts_init,
287            None,
288        );
289        let order_list = OrderList::new(
290            OrderListId::from("OL-001"),
291            instrument_id,
292            strategy_id,
293            vec![client_order_id],
294            ts_init,
295        );
296        let submit_order_list = SubmitOrderList::new(
297            trader_id,
298            client_id,
299            strategy_id,
300            order_list,
301            vec![order_init],
302            None,
303            None,
304            None,
305            UUID4::from("00000000-0000-4000-8000-000000000002"),
306            ts_init,
307            None,
308        );
309        let modify_order = ModifyOrder::new(
310            trader_id,
311            client_id,
312            strategy_id,
313            instrument_id,
314            client_order_id,
315            None,
316            None,
317            None,
318            None,
319            UUID4::from("00000000-0000-4000-8000-000000000003"),
320            ts_init,
321            None,
322            None,
323        );
324        let cancel_order = CancelOrder::new(
325            trader_id,
326            client_id,
327            strategy_id,
328            instrument_id,
329            client_order_id,
330            None,
331            UUID4::from("00000000-0000-4000-8000-000000000005"),
332            ts_init,
333            None,
334            None,
335        );
336
337        vec![
338            TradingCommand::SubmitOrder(submit_order),
339            TradingCommand::SubmitOrderList(submit_order_list),
340            TradingCommand::ModifyOrder(modify_order.clone()),
341            TradingCommand::ModifyOrders(BatchModifyOrders::new(
342                trader_id,
343                client_id,
344                strategy_id,
345                instrument_id,
346                vec![modify_order],
347                UUID4::from("00000000-0000-4000-8000-000000000004"),
348                ts_init,
349                None,
350                None,
351            )),
352            TradingCommand::CancelOrder(cancel_order.clone()),
353            TradingCommand::CancelOrders(BatchCancelOrders::new(
354                trader_id,
355                client_id,
356                strategy_id,
357                instrument_id,
358                vec![cancel_order],
359                UUID4::from("00000000-0000-4000-8000-000000000006"),
360                ts_init,
361                None,
362                None,
363            )),
364            TradingCommand::CancelAllOrders(CancelAllOrders::new(
365                trader_id,
366                client_id,
367                strategy_id,
368                instrument_id,
369                None,
370                UUID4::from("00000000-0000-4000-8000-000000000007"),
371                ts_init,
372                None,
373                None,
374            )),
375            TradingCommand::QueryOrder(QueryOrder::new(
376                trader_id,
377                client_id,
378                strategy_id,
379                instrument_id,
380                client_order_id,
381                None,
382                UUID4::from("00000000-0000-4000-8000-000000000008"),
383                ts_init,
384                None,
385                None,
386            )),
387            TradingCommand::QueryAccount(QueryAccount::new(
388                trader_id,
389                client_id,
390                AccountId::from("SIM-001"),
391                UUID4::from("00000000-0000-4000-8000-000000000009"),
392                ts_init,
393                None,
394                None,
395            )),
396        ]
397    }
398
399    fn order_status_report() -> OrderStatusReport {
400        OrderStatusReport::new(
401            AccountId::from("SIM-001"),
402            InstrumentId::from("AUD/USD.SIM"),
403            None,
404            VenueOrderId::from("V-001"),
405            OrderSide::Buy.into(),
406            OrderType::Limit,
407            TimeInForce::Gtc,
408            OrderStatus::PartiallyFilled,
409            Quantity::from("2"),
410            Quantity::from("1"),
411            UnixNanos::from(1),
412            UnixNanos::from(2),
413            UnixNanos::from(3),
414            Some(UUID4::from("00000000-0000-4000-8000-000000000010")),
415        )
416    }
417
418    fn fill_report(trade_id: &str) -> FillReport {
419        FillReport::new(
420            AccountId::from("SIM-001"),
421            InstrumentId::from("AUD/USD.SIM"),
422            VenueOrderId::from("V-001"),
423            TradeId::from(trade_id),
424            OrderSide::Buy,
425            Quantity::from("1"),
426            Price::from("1.25"),
427            Money::new(0.01, Currency::USD()),
428            LiquiditySide::Taker,
429            None,
430            None,
431            UnixNanos::from(4),
432            UnixNanos::from(5),
433            Some(UUID4::from("00000000-0000-4000-8000-000000000011")),
434        )
435    }
436
437    #[rstest]
438    fn trading_command_round_trips_each_variant() {
439        for command in trading_commands() {
440            let json = serde_json::to_vec(&command).expect("command must serialize as JSON");
441            let json_decoded =
442                serde_json::from_slice::<TradingCommand>(&json).expect("command JSON must decode");
443            let msgpack =
444                rmp_serde::to_vec_named(&command).expect("command must serialize as MsgPack");
445            let msgpack_decoded = rmp_serde::from_slice::<TradingCommand>(&msgpack)
446                .expect("command MsgPack must decode");
447
448            assert_eq!(json_decoded, command);
449            assert_eq!(msgpack_decoded, command);
450        }
451    }
452
453    #[rstest]
454    fn trading_command_display_delegates_to_inner() {
455        for command in trading_commands() {
456            let expected = match &command {
457                TradingCommand::SubmitOrder(command) => command.to_string(),
458                TradingCommand::SubmitOrderList(command) => command.to_string(),
459                TradingCommand::ModifyOrder(command) => command.to_string(),
460                TradingCommand::ModifyOrders(command) => command.to_string(),
461                TradingCommand::CancelOrder(command) => command.to_string(),
462                TradingCommand::CancelOrders(command) => command.to_string(),
463                TradingCommand::CancelAllOrders(command) => command.to_string(),
464                TradingCommand::QueryOrder(command) => command.to_string(),
465                TradingCommand::QueryAccount(command) => command.to_string(),
466            };
467
468            assert_eq!(command.to_string(), expected);
469        }
470    }
471
472    #[rstest]
473    fn execution_report_order_with_fills_uses_inner_display() {
474        let order_report = order_status_report();
475        let fill_report_1 = fill_report("T-001");
476        let fill_report_2 = fill_report("T-002");
477        let expected = format!(
478            "OrderWithFills(order={order_report}, fills=[{fill_report_1}, {fill_report_2}])"
479        );
480        let report = ExecutionReport::OrderWithFills(
481            Box::new(order_report),
482            vec![fill_report_1, fill_report_2],
483        );
484
485        assert_eq!(report.to_string(), expected);
486    }
487
488    #[rstest]
489    fn trading_command_rejects_unknown_type() {
490        let error = serde_json::from_value::<TradingCommand>(serde_json::json!({
491            "type": "UnknownCommand",
492        }))
493        .expect_err("unknown command type must be rejected");
494
495        assert_eq!(
496            error.to_string(),
497            "unknown variant `UnknownCommand`, expected one of `SubmitOrder`, `SubmitOrderList`, \
498             `ModifyOrder`, `BatchModifyOrders`, `CancelOrder`, `BatchCancelOrders`, \
499             `CancelAllOrders`, `QueryOrder`, `QueryAccount`",
500        );
501    }
502
503    #[rstest]
504    fn trading_command_accessors_report_the_inner_identity() {
505        let instrument_id = InstrumentId::from("AUD/USD.SIM");
506        let strategy_id = StrategyId::from("STRATEGY-001");
507        let ts_init = UnixNanos::from(1_000_000_000);
508
509        for command in trading_commands() {
510            assert_eq!(command.client_id(), Some(ClientId::from("EXTERNAL")));
511            assert_eq!(command.ts_init(), ts_init);
512            assert_eq!(command.params(), None);
513
514            if matches!(command, TradingCommand::QueryAccount(_)) {
515                assert_eq!(command.strategy_id(), None);
516            } else {
517                assert_eq!(command.strategy_id(), Some(strategy_id));
518                assert_eq!(command.instrument_id(), instrument_id);
519            }
520        }
521    }
522
523    #[rstest]
524    fn trading_command_params_expose_the_inner_params() {
525        let mut params = Params::new();
526        params.insert("reduce_only".into(), "true".into());
527
528        let command = TradingCommand::QueryOrder(QueryOrder::new(
529            TraderId::from("TRADER-001"),
530            Some(ClientId::from("EXTERNAL")),
531            StrategyId::from("STRATEGY-001"),
532            InstrumentId::from("AUD/USD.SIM"),
533            OrderInitialized::default().client_order_id,
534            None,
535            UUID4::from("00000000-0000-4000-8000-000000000012"),
536            UnixNanos::from(1),
537            Some(params.clone()),
538            None,
539        ));
540
541        assert_eq!(command.params(), Some(&params));
542    }
543
544    #[rstest]
545    #[should_panic(expected = "No instrument ID for command")]
546    fn trading_command_instrument_id_panics_for_query_account() {
547        let command = trading_commands()
548            .into_iter()
549            .find(|command| matches!(command, TradingCommand::QueryAccount(_)))
550            .expect("query account command must be present");
551
552        let _ = command.instrument_id();
553    }
554}