1pub 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
47pub const PARAMS_CLOSE_POSITION: &str = "close_position";
49
50#[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#[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 #[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(¶ms));
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}