Skip to main content

nautilus_common/messages/execution/
report.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
16use std::fmt::Display;
17
18use derive_builder::Builder;
19use nautilus_core::{Params, UUID4, UnixNanos};
20use nautilus_model::identifiers::{
21    ClientId, ClientOrderId, InstrumentId, TraderId, Venue, VenueOrderId,
22};
23use serde::{Deserialize, Serialize};
24
25use crate::enums::LogLevel;
26
27const fn default_report_log_level() -> LogLevel {
28    LogLevel::Info
29}
30
31#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Builder)]
32#[cfg_attr(
33    feature = "python",
34    pyo3::pyclass(module = "nautilus_trader.live", frozen, from_py_object)
35)]
36#[cfg_attr(
37    feature = "python",
38    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")
39)]
40pub struct GenerateOrderStatusReport {
41    #[builder(default = "UUID4::new()")]
42    pub command_id: UUID4,
43    pub ts_init: UnixNanos,
44    #[builder(default)]
45    pub instrument_id: Option<InstrumentId>,
46    #[builder(default)]
47    pub client_order_id: Option<ClientOrderId>,
48    #[builder(default)]
49    pub venue_order_id: Option<VenueOrderId>,
50    #[builder(default)]
51    pub params: Option<Params>,
52    #[builder(default)]
53    #[serde(default, skip_serializing_if = "Option::is_none")]
54    pub correlation_id: Option<UUID4>,
55    #[builder(default)]
56    #[serde(default, skip_serializing_if = "Option::is_none")]
57    pub causation_id: Option<UUID4>,
58}
59
60impl GenerateOrderStatusReport {
61    #[must_use]
62    pub fn new(
63        command_id: UUID4,
64        ts_init: UnixNanos,
65        instrument_id: Option<InstrumentId>,
66        client_order_id: Option<ClientOrderId>,
67        venue_order_id: Option<VenueOrderId>,
68        params: Option<Params>,
69        correlation_id: Option<UUID4>,
70    ) -> Self {
71        Self {
72            command_id,
73            ts_init,
74            instrument_id,
75            client_order_id,
76            venue_order_id,
77            params,
78            correlation_id,
79            causation_id: None,
80        }
81    }
82}
83
84impl Display for GenerateOrderStatusReport {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        write!(
87            f,
88            "{}(instrument_id={:?}, client_order_id={:?}, venue_order_id={:?}, command_id={})",
89            stringify!(GenerateOrderStatusReport),
90            self.instrument_id,
91            self.client_order_id,
92            self.venue_order_id,
93            self.command_id,
94        )
95    }
96}
97
98#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Builder)]
99#[cfg_attr(
100    feature = "python",
101    pyo3::pyclass(module = "nautilus_trader.live", frozen, from_py_object)
102)]
103#[cfg_attr(
104    feature = "python",
105    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")
106)]
107pub struct GenerateOrderStatusReports {
108    #[builder(default = "UUID4::new()")]
109    pub command_id: UUID4,
110    pub ts_init: UnixNanos,
111    pub open_only: bool,
112    #[builder(default)]
113    pub instrument_id: Option<InstrumentId>,
114    #[builder(default)]
115    pub start: Option<UnixNanos>,
116    #[builder(default)]
117    pub end: Option<UnixNanos>,
118    #[builder(default)]
119    pub params: Option<Params>,
120    /// The log level for receipt logging.
121    #[builder(default = "default_report_log_level()")]
122    #[serde(default = "default_report_log_level")]
123    pub log_receipt_level: LogLevel,
124    #[builder(default)]
125    #[serde(default, skip_serializing_if = "Option::is_none")]
126    pub correlation_id: Option<UUID4>,
127    #[builder(default)]
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    pub causation_id: Option<UUID4>,
130}
131
132impl GenerateOrderStatusReports {
133    #[expect(clippy::too_many_arguments)]
134    #[must_use]
135    pub fn new(
136        command_id: UUID4,
137        ts_init: UnixNanos,
138        open_only: bool,
139        instrument_id: Option<InstrumentId>,
140        start: Option<UnixNanos>,
141        end: Option<UnixNanos>,
142        params: Option<Params>,
143        correlation_id: Option<UUID4>,
144    ) -> Self {
145        Self {
146            command_id,
147            ts_init,
148            open_only,
149            instrument_id,
150            start,
151            end,
152            params,
153            log_receipt_level: LogLevel::Info,
154            correlation_id,
155            causation_id: None,
156        }
157    }
158}
159
160impl Display for GenerateOrderStatusReports {
161    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162        write!(
163            f,
164            "{}(open_only={}, instrument_id={:?}, command_id={})",
165            stringify!(GenerateOrderStatusReports),
166            self.open_only,
167            self.instrument_id,
168            self.command_id,
169        )
170    }
171}
172
173#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Builder)]
174#[cfg_attr(
175    feature = "python",
176    pyo3::pyclass(module = "nautilus_trader.live", frozen, from_py_object)
177)]
178#[cfg_attr(
179    feature = "python",
180    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")
181)]
182pub struct GenerateFillReports {
183    #[builder(default = "UUID4::new()")]
184    pub command_id: UUID4,
185    pub ts_init: UnixNanos,
186    #[builder(default)]
187    pub instrument_id: Option<InstrumentId>,
188    #[builder(default)]
189    pub venue_order_id: Option<VenueOrderId>,
190    #[builder(default)]
191    pub start: Option<UnixNanos>,
192    #[builder(default)]
193    pub end: Option<UnixNanos>,
194    #[builder(default)]
195    pub params: Option<Params>,
196    /// The log level for receipt logging.
197    #[builder(default = "default_report_log_level()")]
198    #[serde(default = "default_report_log_level")]
199    pub log_receipt_level: LogLevel,
200    #[builder(default)]
201    #[serde(default, skip_serializing_if = "Option::is_none")]
202    pub correlation_id: Option<UUID4>,
203    #[builder(default)]
204    #[serde(default, skip_serializing_if = "Option::is_none")]
205    pub causation_id: Option<UUID4>,
206}
207
208impl GenerateFillReports {
209    #[expect(clippy::too_many_arguments)]
210    #[must_use]
211    pub fn new(
212        command_id: UUID4,
213        ts_init: UnixNanos,
214        instrument_id: Option<InstrumentId>,
215        venue_order_id: Option<VenueOrderId>,
216        start: Option<UnixNanos>,
217        end: Option<UnixNanos>,
218        params: Option<Params>,
219        correlation_id: Option<UUID4>,
220    ) -> Self {
221        Self {
222            command_id,
223            ts_init,
224            instrument_id,
225            venue_order_id,
226            start,
227            end,
228            params,
229            log_receipt_level: LogLevel::Info,
230            correlation_id,
231            causation_id: None,
232        }
233    }
234}
235
236impl Display for GenerateFillReports {
237    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238        write!(
239            f,
240            "{}(instrument_id={:?}, venue_order_id={:?}, command_id={})",
241            stringify!(GenerateFillReports),
242            self.instrument_id,
243            self.venue_order_id,
244            self.command_id,
245        )
246    }
247}
248
249#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Builder)]
250#[cfg_attr(
251    feature = "python",
252    pyo3::pyclass(module = "nautilus_trader.live", frozen, from_py_object)
253)]
254#[cfg_attr(
255    feature = "python",
256    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.live")
257)]
258pub struct GeneratePositionStatusReports {
259    #[builder(default = "UUID4::new()")]
260    pub command_id: UUID4,
261    pub ts_init: UnixNanos,
262    #[builder(default)]
263    pub instrument_id: Option<InstrumentId>,
264    #[builder(default)]
265    pub start: Option<UnixNanos>,
266    #[builder(default)]
267    pub end: Option<UnixNanos>,
268    #[builder(default)]
269    pub params: Option<Params>,
270    /// The log level for receipt logging.
271    #[builder(default = "default_report_log_level()")]
272    #[serde(default = "default_report_log_level")]
273    pub log_receipt_level: LogLevel,
274    #[builder(default)]
275    #[serde(default, skip_serializing_if = "Option::is_none")]
276    pub correlation_id: Option<UUID4>,
277    #[builder(default)]
278    #[serde(default, skip_serializing_if = "Option::is_none")]
279    pub causation_id: Option<UUID4>,
280}
281
282impl GeneratePositionStatusReports {
283    #[must_use]
284    pub fn new(
285        command_id: UUID4,
286        ts_init: UnixNanos,
287        instrument_id: Option<InstrumentId>,
288        start: Option<UnixNanos>,
289        end: Option<UnixNanos>,
290        params: Option<Params>,
291        correlation_id: Option<UUID4>,
292    ) -> Self {
293        Self {
294            command_id,
295            ts_init,
296            instrument_id,
297            start,
298            end,
299            params,
300            log_receipt_level: LogLevel::Info,
301            correlation_id,
302            causation_id: None,
303        }
304    }
305}
306
307impl Display for GeneratePositionStatusReports {
308    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
309        write!(
310            f,
311            "{}(instrument_id={:?}, command_id={})",
312            stringify!(GeneratePositionStatusReports),
313            self.instrument_id,
314            self.command_id,
315        )
316    }
317}
318
319#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Builder)]
320pub struct GenerateExecutionMassStatus {
321    pub trader_id: TraderId,
322    pub client_id: ClientId,
323    #[builder(default)]
324    pub venue: Option<Venue>,
325    #[builder(default = "UUID4::new()")]
326    pub command_id: UUID4,
327    pub ts_init: UnixNanos,
328    #[builder(default)]
329    pub params: Option<Params>,
330    #[builder(default)]
331    #[serde(default, skip_serializing_if = "Option::is_none")]
332    pub correlation_id: Option<UUID4>,
333    #[builder(default)]
334    #[serde(default, skip_serializing_if = "Option::is_none")]
335    pub causation_id: Option<UUID4>,
336}
337
338impl GenerateExecutionMassStatus {
339    #[must_use]
340    pub fn new(
341        trader_id: TraderId,
342        client_id: ClientId,
343        venue: Option<Venue>,
344        command_id: UUID4,
345        ts_init: UnixNanos,
346        params: Option<Params>,
347        correlation_id: Option<UUID4>,
348    ) -> Self {
349        Self {
350            trader_id,
351            client_id,
352            venue,
353            command_id,
354            ts_init,
355            params,
356            correlation_id,
357            causation_id: None,
358        }
359    }
360}
361
362impl Display for GenerateExecutionMassStatus {
363    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
364        write!(
365            f,
366            "{}(trader_id={}, client_id={}, venue={:?}, command_id={})",
367            stringify!(GenerateExecutionMassStatus),
368            self.trader_id,
369            self.client_id,
370            self.venue,
371            self.command_id,
372        )
373    }
374}
375
376#[cfg(test)]
377mod tests {
378    use rstest::rstest;
379
380    use super::*;
381
382    const COMMAND_ID: &str = "00000000-0000-4000-8000-000000000001";
383
384    fn command_id() -> UUID4 {
385        UUID4::from(COMMAND_ID)
386    }
387
388    fn correlation_id() -> UUID4 {
389        UUID4::from("00000000-0000-4000-8000-000000000002")
390    }
391
392    fn instrument_id() -> InstrumentId {
393        InstrumentId::from("ETHUSDT.BINANCE")
394    }
395
396    fn params() -> Params {
397        let mut params = Params::new();
398        params.insert("depth".into(), "10".into());
399        params
400    }
401
402    #[rstest]
403    fn test_generate_order_status_report_assigns_every_field() {
404        let command = GenerateOrderStatusReport::new(
405            command_id(),
406            UnixNanos::from(3),
407            Some(instrument_id()),
408            Some(ClientOrderId::from("O-1")),
409            Some(VenueOrderId::from("V-1")),
410            Some(params()),
411            Some(correlation_id()),
412        );
413
414        assert_eq!(command.command_id, command_id());
415        assert_eq!(command.ts_init, UnixNanos::from(3));
416        assert_eq!(command.instrument_id, Some(instrument_id()));
417        assert_eq!(command.client_order_id, Some(ClientOrderId::from("O-1")));
418        assert_eq!(command.venue_order_id, Some(VenueOrderId::from("V-1")));
419        assert_eq!(command.params, Some(params()));
420        assert_eq!(command.correlation_id, Some(correlation_id()));
421        assert_eq!(command.causation_id, None);
422        assert_eq!(
423            command.to_string(),
424            format!(
425                "GenerateOrderStatusReport(instrument_id=Some(\"ETHUSDT.BINANCE\"), \
426                 client_order_id=Some(\"O-1\"), venue_order_id=Some(\"V-1\"), command_id={COMMAND_ID})"
427            )
428        );
429    }
430
431    #[rstest]
432    fn test_generate_order_status_reports_assigns_every_field() {
433        let command = GenerateOrderStatusReports::new(
434            command_id(),
435            UnixNanos::from(5),
436            true,
437            Some(instrument_id()),
438            Some(UnixNanos::from(7)),
439            Some(UnixNanos::from(11)),
440            Some(params()),
441            Some(correlation_id()),
442        );
443
444        assert_eq!(command.command_id, command_id());
445        assert_eq!(command.ts_init, UnixNanos::from(5));
446        assert!(command.open_only);
447        assert_eq!(command.instrument_id, Some(instrument_id()));
448        assert_eq!(command.start, Some(UnixNanos::from(7)));
449        assert_eq!(command.end, Some(UnixNanos::from(11)));
450        assert_eq!(command.params, Some(params()));
451        assert_eq!(command.log_receipt_level, LogLevel::Info);
452        assert_eq!(command.correlation_id, Some(correlation_id()));
453        assert_eq!(command.causation_id, None);
454        assert_eq!(
455            command.to_string(),
456            format!(
457                "GenerateOrderStatusReports(open_only=true, \
458                 instrument_id=Some(\"ETHUSDT.BINANCE\"), command_id={COMMAND_ID})"
459            )
460        );
461    }
462
463    #[rstest]
464    fn test_generate_fill_reports_assigns_every_field() {
465        let command = GenerateFillReports::new(
466            command_id(),
467            UnixNanos::from(13),
468            Some(instrument_id()),
469            Some(VenueOrderId::from("V-2")),
470            Some(UnixNanos::from(17)),
471            Some(UnixNanos::from(19)),
472            Some(params()),
473            Some(correlation_id()),
474        );
475
476        assert_eq!(command.command_id, command_id());
477        assert_eq!(command.ts_init, UnixNanos::from(13));
478        assert_eq!(command.instrument_id, Some(instrument_id()));
479        assert_eq!(command.venue_order_id, Some(VenueOrderId::from("V-2")));
480        assert_eq!(command.start, Some(UnixNanos::from(17)));
481        assert_eq!(command.end, Some(UnixNanos::from(19)));
482        assert_eq!(command.params, Some(params()));
483        assert_eq!(command.log_receipt_level, LogLevel::Info);
484        assert_eq!(command.correlation_id, Some(correlation_id()));
485        assert_eq!(command.causation_id, None);
486        assert_eq!(
487            command.to_string(),
488            format!(
489                "GenerateFillReports(instrument_id=Some(\"ETHUSDT.BINANCE\"), \
490                 venue_order_id=Some(\"V-2\"), command_id={COMMAND_ID})"
491            )
492        );
493    }
494
495    #[rstest]
496    fn test_generate_position_status_reports_assigns_every_field() {
497        let command = GeneratePositionStatusReports::new(
498            command_id(),
499            UnixNanos::from(23),
500            Some(instrument_id()),
501            Some(UnixNanos::from(29)),
502            Some(UnixNanos::from(31)),
503            Some(params()),
504            Some(correlation_id()),
505        );
506
507        assert_eq!(command.command_id, command_id());
508        assert_eq!(command.ts_init, UnixNanos::from(23));
509        assert_eq!(command.instrument_id, Some(instrument_id()));
510        assert_eq!(command.start, Some(UnixNanos::from(29)));
511        assert_eq!(command.end, Some(UnixNanos::from(31)));
512        assert_eq!(command.params, Some(params()));
513        assert_eq!(command.log_receipt_level, LogLevel::Info);
514        assert_eq!(command.correlation_id, Some(correlation_id()));
515        assert_eq!(command.causation_id, None);
516        assert_eq!(
517            command.to_string(),
518            format!(
519                "GeneratePositionStatusReports(instrument_id=Some(\"ETHUSDT.BINANCE\"), \
520                 command_id={COMMAND_ID})"
521            )
522        );
523    }
524
525    #[rstest]
526    fn test_generate_execution_mass_status_assigns_every_field() {
527        let command = GenerateExecutionMassStatus::new(
528            TraderId::from("TESTER-001"),
529            ClientId::from("BINANCE"),
530            Some(Venue::from("BINANCE")),
531            command_id(),
532            UnixNanos::from(37),
533            Some(params()),
534            Some(correlation_id()),
535        );
536
537        assert_eq!(command.trader_id, TraderId::from("TESTER-001"));
538        assert_eq!(command.client_id, ClientId::from("BINANCE"));
539        assert_eq!(command.venue, Some(Venue::from("BINANCE")));
540        assert_eq!(command.command_id, command_id());
541        assert_eq!(command.ts_init, UnixNanos::from(37));
542        assert_eq!(command.params, Some(params()));
543        assert_eq!(command.correlation_id, Some(correlation_id()));
544        assert_eq!(command.causation_id, None);
545        assert_eq!(
546            command.to_string(),
547            format!(
548                "GenerateExecutionMassStatus(trader_id=TESTER-001, client_id=BINANCE, \
549                 venue=Some(\"BINANCE\"), command_id={COMMAND_ID})"
550            )
551        );
552    }
553
554    #[rstest]
555    fn test_builders_default_the_optional_fields() {
556        let command = GenerateFillReportsBuilder::default()
557            .ts_init(UnixNanos::from(41))
558            .build()
559            .unwrap();
560
561        assert_eq!(command.ts_init, UnixNanos::from(41));
562        assert_eq!(command.instrument_id, None);
563        assert_eq!(command.venue_order_id, None);
564        assert_eq!(command.start, None);
565        assert_eq!(command.end, None);
566        assert_eq!(command.params, None);
567        assert_eq!(command.log_receipt_level, LogLevel::Info);
568        assert_eq!(command.correlation_id, None);
569        assert_eq!(command.causation_id, None);
570    }
571
572    #[rstest]
573    fn test_report_commands_round_trip_through_json() {
574        let command = GenerateOrderStatusReports::new(
575            command_id(),
576            UnixNanos::from(43),
577            false,
578            None,
579            None,
580            None,
581            None,
582            None,
583        );
584
585        let json = serde_json::to_string(&command).unwrap();
586
587        assert_eq!(
588            serde_json::from_str::<GenerateOrderStatusReports>(&json).unwrap(),
589            command
590        );
591    }
592}