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        Params::new([("depth".into(), "10".into())].into())
398    }
399
400    #[rstest]
401    fn test_generate_order_status_report_assigns_every_field() {
402        let command = GenerateOrderStatusReport::new(
403            command_id(),
404            UnixNanos::from(3),
405            Some(instrument_id()),
406            Some(ClientOrderId::from("O-1")),
407            Some(VenueOrderId::from("V-1")),
408            Some(params()),
409            Some(correlation_id()),
410        );
411
412        assert_eq!(command.command_id, command_id());
413        assert_eq!(command.ts_init, UnixNanos::from(3));
414        assert_eq!(command.instrument_id, Some(instrument_id()));
415        assert_eq!(command.client_order_id, Some(ClientOrderId::from("O-1")));
416        assert_eq!(command.venue_order_id, Some(VenueOrderId::from("V-1")));
417        assert_eq!(command.params, Some(params()));
418        assert_eq!(command.correlation_id, Some(correlation_id()));
419        assert_eq!(command.causation_id, None);
420        assert_eq!(
421            command.to_string(),
422            format!(
423                "GenerateOrderStatusReport(instrument_id=Some(\"ETHUSDT.BINANCE\"), \
424                 client_order_id=Some(\"O-1\"), venue_order_id=Some(\"V-1\"), command_id={COMMAND_ID})"
425            )
426        );
427    }
428
429    #[rstest]
430    fn test_generate_order_status_reports_assigns_every_field() {
431        let command = GenerateOrderStatusReports::new(
432            command_id(),
433            UnixNanos::from(5),
434            true,
435            Some(instrument_id()),
436            Some(UnixNanos::from(7)),
437            Some(UnixNanos::from(11)),
438            Some(params()),
439            Some(correlation_id()),
440        );
441
442        assert_eq!(command.command_id, command_id());
443        assert_eq!(command.ts_init, UnixNanos::from(5));
444        assert!(command.open_only);
445        assert_eq!(command.instrument_id, Some(instrument_id()));
446        assert_eq!(command.start, Some(UnixNanos::from(7)));
447        assert_eq!(command.end, Some(UnixNanos::from(11)));
448        assert_eq!(command.params, Some(params()));
449        assert_eq!(command.log_receipt_level, LogLevel::Info);
450        assert_eq!(command.correlation_id, Some(correlation_id()));
451        assert_eq!(command.causation_id, None);
452        assert_eq!(
453            command.to_string(),
454            format!(
455                "GenerateOrderStatusReports(open_only=true, \
456                 instrument_id=Some(\"ETHUSDT.BINANCE\"), command_id={COMMAND_ID})"
457            )
458        );
459    }
460
461    #[rstest]
462    fn test_generate_fill_reports_assigns_every_field() {
463        let command = GenerateFillReports::new(
464            command_id(),
465            UnixNanos::from(13),
466            Some(instrument_id()),
467            Some(VenueOrderId::from("V-2")),
468            Some(UnixNanos::from(17)),
469            Some(UnixNanos::from(19)),
470            Some(params()),
471            Some(correlation_id()),
472        );
473
474        assert_eq!(command.command_id, command_id());
475        assert_eq!(command.ts_init, UnixNanos::from(13));
476        assert_eq!(command.instrument_id, Some(instrument_id()));
477        assert_eq!(command.venue_order_id, Some(VenueOrderId::from("V-2")));
478        assert_eq!(command.start, Some(UnixNanos::from(17)));
479        assert_eq!(command.end, Some(UnixNanos::from(19)));
480        assert_eq!(command.params, Some(params()));
481        assert_eq!(command.log_receipt_level, LogLevel::Info);
482        assert_eq!(command.correlation_id, Some(correlation_id()));
483        assert_eq!(command.causation_id, None);
484        assert_eq!(
485            command.to_string(),
486            format!(
487                "GenerateFillReports(instrument_id=Some(\"ETHUSDT.BINANCE\"), \
488                 venue_order_id=Some(\"V-2\"), command_id={COMMAND_ID})"
489            )
490        );
491    }
492
493    #[rstest]
494    fn test_generate_position_status_reports_assigns_every_field() {
495        let command = GeneratePositionStatusReports::new(
496            command_id(),
497            UnixNanos::from(23),
498            Some(instrument_id()),
499            Some(UnixNanos::from(29)),
500            Some(UnixNanos::from(31)),
501            Some(params()),
502            Some(correlation_id()),
503        );
504
505        assert_eq!(command.command_id, command_id());
506        assert_eq!(command.ts_init, UnixNanos::from(23));
507        assert_eq!(command.instrument_id, Some(instrument_id()));
508        assert_eq!(command.start, Some(UnixNanos::from(29)));
509        assert_eq!(command.end, Some(UnixNanos::from(31)));
510        assert_eq!(command.params, Some(params()));
511        assert_eq!(command.log_receipt_level, LogLevel::Info);
512        assert_eq!(command.correlation_id, Some(correlation_id()));
513        assert_eq!(command.causation_id, None);
514        assert_eq!(
515            command.to_string(),
516            format!(
517                "GeneratePositionStatusReports(instrument_id=Some(\"ETHUSDT.BINANCE\"), \
518                 command_id={COMMAND_ID})"
519            )
520        );
521    }
522
523    #[rstest]
524    fn test_generate_execution_mass_status_assigns_every_field() {
525        let command = GenerateExecutionMassStatus::new(
526            TraderId::from("TESTER-001"),
527            ClientId::from("BINANCE"),
528            Some(Venue::from("BINANCE")),
529            command_id(),
530            UnixNanos::from(37),
531            Some(params()),
532            Some(correlation_id()),
533        );
534
535        assert_eq!(command.trader_id, TraderId::from("TESTER-001"));
536        assert_eq!(command.client_id, ClientId::from("BINANCE"));
537        assert_eq!(command.venue, Some(Venue::from("BINANCE")));
538        assert_eq!(command.command_id, command_id());
539        assert_eq!(command.ts_init, UnixNanos::from(37));
540        assert_eq!(command.params, Some(params()));
541        assert_eq!(command.correlation_id, Some(correlation_id()));
542        assert_eq!(command.causation_id, None);
543        assert_eq!(
544            command.to_string(),
545            format!(
546                "GenerateExecutionMassStatus(trader_id=TESTER-001, client_id=BINANCE, \
547                 venue=Some(\"BINANCE\"), command_id={COMMAND_ID})"
548            )
549        );
550    }
551
552    #[rstest]
553    fn test_builders_default_the_optional_fields() {
554        let command = GenerateFillReportsBuilder::default()
555            .ts_init(UnixNanos::from(41))
556            .build()
557            .unwrap();
558
559        assert_eq!(command.ts_init, UnixNanos::from(41));
560        assert_eq!(command.instrument_id, None);
561        assert_eq!(command.venue_order_id, None);
562        assert_eq!(command.start, None);
563        assert_eq!(command.end, None);
564        assert_eq!(command.params, None);
565        assert_eq!(command.log_receipt_level, LogLevel::Info);
566        assert_eq!(command.correlation_id, None);
567        assert_eq!(command.causation_id, None);
568    }
569
570    #[rstest]
571    fn test_report_commands_round_trip_through_json() {
572        let command = GenerateOrderStatusReports::new(
573            command_id(),
574            UnixNanos::from(43),
575            false,
576            None,
577            None,
578            None,
579            None,
580            None,
581        );
582
583        let json = serde_json::to_string(&command).unwrap();
584
585        assert_eq!(
586            serde_json::from_str::<GenerateOrderStatusReports>(&json).unwrap(),
587            command
588        );
589    }
590}