nautilus_model/reports/
fill.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
16use std::fmt::Display;
17
18use nautilus_core::{UUID4, UnixNanos};
19use serde::{Deserialize, Serialize};
20
21use crate::{
22    enums::{LiquiditySide, OrderSide},
23    identifiers::{AccountId, ClientOrderId, InstrumentId, PositionId, TradeId, VenueOrderId},
24    types::{Money, Price, Quantity},
25};
26
27/// Represents a fill report of a single order execution.
28#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(tag = "type")]
30#[cfg_attr(
31    feature = "python",
32    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
33)]
34pub struct FillReport {
35    /// The account ID associated with the position.
36    pub account_id: AccountId,
37    /// The instrument ID associated with the event.
38    pub instrument_id: InstrumentId,
39    /// The venue assigned order ID.
40    pub venue_order_id: VenueOrderId,
41    /// The trade match ID (assigned by the venue).
42    pub trade_id: TradeId,
43    /// The order side.
44    pub order_side: OrderSide,
45    /// The last fill quantity for the position.
46    pub last_qty: Quantity,
47    /// The last fill price for the position.
48    pub last_px: Price,
49    /// The commission generated from the fill.
50    pub commission: Money,
51    /// The liquidity side of the execution.
52    pub liquidity_side: LiquiditySide,
53    /// The unique identifier for the event.
54    pub report_id: UUID4,
55    /// UNIX timestamp (nanoseconds) when the event occurred.
56    pub ts_event: UnixNanos,
57    /// UNIX timestamp (nanoseconds) when the event was initialized.
58    pub ts_init: UnixNanos,
59    /// The client order ID.
60    pub client_order_id: Option<ClientOrderId>,
61    /// The position ID (assigned by the venue).
62    pub venue_position_id: Option<PositionId>,
63}
64
65impl FillReport {
66    /// Creates a new [`FillReport`] instance with required fields.
67    #[allow(clippy::too_many_arguments)]
68    #[must_use]
69    pub fn new(
70        account_id: AccountId,
71        instrument_id: InstrumentId,
72        venue_order_id: VenueOrderId,
73        trade_id: TradeId,
74        order_side: OrderSide,
75        last_qty: Quantity,
76        last_px: Price,
77        commission: Money,
78        liquidity_side: LiquiditySide,
79        client_order_id: Option<ClientOrderId>,
80        venue_position_id: Option<PositionId>,
81        ts_event: UnixNanos,
82        ts_init: UnixNanos,
83        report_id: Option<UUID4>,
84    ) -> Self {
85        Self {
86            account_id,
87            instrument_id,
88            venue_order_id,
89            trade_id,
90            order_side,
91            last_qty,
92            last_px,
93            commission,
94            liquidity_side,
95            report_id: report_id.unwrap_or_default(),
96            ts_event,
97            ts_init,
98            client_order_id,
99            venue_position_id,
100        }
101    }
102
103    /// Checks if the fill has a client order ID.
104    #[must_use]
105    pub const fn has_client_order_id(&self) -> bool {
106        self.client_order_id.is_some()
107    }
108
109    /// Utility method to check if the fill has a venue position ID.
110    #[must_use]
111    pub const fn has_venue_position_id(&self) -> bool {
112        self.venue_position_id.is_some()
113    }
114}
115
116impl Display for FillReport {
117    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118        write!(
119            f,
120            "FillReport(instrument={}, side={}, qty={}, last_px={}, trade_id={}, venue_order_id={}, commission={}, liquidity={})",
121            self.instrument_id,
122            self.order_side,
123            self.last_qty,
124            self.last_px,
125            self.trade_id,
126            self.venue_order_id,
127            self.commission,
128            self.liquidity_side,
129        )
130    }
131}
132
133////////////////////////////////////////////////////////////////////////////////
134// Tests
135////////////////////////////////////////////////////////////////////////////////
136#[cfg(test)]
137mod tests {
138    use nautilus_core::UnixNanos;
139    use rstest::*;
140
141    use super::*;
142    use crate::{
143        enums::{LiquiditySide, OrderSide},
144        identifiers::{AccountId, ClientOrderId, InstrumentId, PositionId, TradeId, VenueOrderId},
145        types::{Currency, Money, Price, Quantity},
146    };
147
148    fn test_fill_report() -> FillReport {
149        FillReport::new(
150            AccountId::from("SIM-001"),
151            InstrumentId::from("AUDUSD.SIM"),
152            VenueOrderId::from("1"),
153            TradeId::from("1"),
154            OrderSide::Buy,
155            Quantity::from("100"),
156            Price::from("0.80000"),
157            Money::new(5.0, Currency::USD()),
158            LiquiditySide::Taker,
159            Some(ClientOrderId::from("O-19700101-000000-001-001-1")),
160            Some(PositionId::from("P-001")),
161            UnixNanos::from(1_000_000_000),
162            UnixNanos::from(2_000_000_000),
163            None,
164        )
165    }
166
167    #[rstest]
168    fn test_fill_report_new() {
169        let report = test_fill_report();
170
171        assert_eq!(report.account_id, AccountId::from("SIM-001"));
172        assert_eq!(report.instrument_id, InstrumentId::from("AUDUSD.SIM"));
173        assert_eq!(report.venue_order_id, VenueOrderId::from("1"));
174        assert_eq!(report.trade_id, TradeId::from("1"));
175        assert_eq!(report.order_side, OrderSide::Buy);
176        assert_eq!(report.last_qty, Quantity::from("100"));
177        assert_eq!(report.last_px, Price::from("0.80000"));
178        assert_eq!(report.commission, Money::new(5.0, Currency::USD()));
179        assert_eq!(report.liquidity_side, LiquiditySide::Taker);
180        assert_eq!(
181            report.client_order_id,
182            Some(ClientOrderId::from("O-19700101-000000-001-001-1"))
183        );
184        assert_eq!(report.venue_position_id, Some(PositionId::from("P-001")));
185        assert_eq!(report.ts_event, UnixNanos::from(1_000_000_000));
186        assert_eq!(report.ts_init, UnixNanos::from(2_000_000_000));
187    }
188
189    #[rstest]
190    fn test_fill_report_new_with_generated_report_id() {
191        let report = FillReport::new(
192            AccountId::from("SIM-001"),
193            InstrumentId::from("AUDUSD.SIM"),
194            VenueOrderId::from("1"),
195            TradeId::from("1"),
196            OrderSide::Buy,
197            Quantity::from("100"),
198            Price::from("0.80000"),
199            Money::new(5.0, Currency::USD()),
200            LiquiditySide::Taker,
201            None,
202            None,
203            UnixNanos::from(1_000_000_000),
204            UnixNanos::from(2_000_000_000),
205            None, // No report ID provided, should generate one
206        );
207
208        // Should have a generated UUID
209        assert_ne!(
210            report.report_id.to_string(),
211            "00000000-0000-0000-0000-000000000000"
212        );
213    }
214
215    #[rstest]
216    fn test_has_client_order_id() {
217        let mut report = test_fill_report();
218        assert!(report.has_client_order_id());
219
220        report.client_order_id = None;
221        assert!(!report.has_client_order_id());
222    }
223
224    #[rstest]
225    fn test_has_venue_position_id() {
226        let mut report = test_fill_report();
227        assert!(report.has_venue_position_id());
228
229        report.venue_position_id = None;
230        assert!(!report.has_venue_position_id());
231    }
232
233    #[rstest]
234    fn test_display() {
235        let report = test_fill_report();
236        let display_str = format!("{report}");
237
238        assert!(display_str.contains("FillReport"));
239        assert!(display_str.contains("AUDUSD.SIM"));
240        assert!(display_str.contains("BUY"));
241        assert!(display_str.contains("100"));
242        assert!(display_str.contains("0.80000"));
243        assert!(display_str.contains("5.00 USD"));
244        assert!(display_str.contains("TAKER"));
245    }
246
247    #[rstest]
248    fn test_clone_and_equality() {
249        let report1 = test_fill_report();
250        let report2 = report1.clone();
251
252        assert_eq!(report1, report2);
253    }
254
255    #[rstest]
256    fn test_serialization_roundtrip() {
257        let original = test_fill_report();
258
259        // Test JSON serialization
260        let json = serde_json::to_string(&original).unwrap();
261        let deserialized: FillReport = serde_json::from_str(&json).unwrap();
262        assert_eq!(original, deserialized);
263    }
264
265    #[rstest]
266    fn test_fill_report_with_different_liquidity_sides() {
267        let maker_report = FillReport::new(
268            AccountId::from("SIM-001"),
269            InstrumentId::from("AUDUSD.SIM"),
270            VenueOrderId::from("1"),
271            TradeId::from("1"),
272            OrderSide::Buy,
273            Quantity::from("100"),
274            Price::from("0.80000"),
275            Money::new(2.0, Currency::USD()),
276            LiquiditySide::Maker,
277            None,
278            None,
279            UnixNanos::from(1_000_000_000),
280            UnixNanos::from(2_000_000_000),
281            None,
282        );
283
284        let taker_report = FillReport::new(
285            AccountId::from("SIM-001"),
286            InstrumentId::from("AUDUSD.SIM"),
287            VenueOrderId::from("2"),
288            TradeId::from("2"),
289            OrderSide::Sell,
290            Quantity::from("100"),
291            Price::from("0.80000"),
292            Money::new(5.0, Currency::USD()),
293            LiquiditySide::Taker,
294            None,
295            None,
296            UnixNanos::from(1_000_000_000),
297            UnixNanos::from(2_000_000_000),
298            None,
299        );
300
301        assert_eq!(maker_report.liquidity_side, LiquiditySide::Maker);
302        assert_eq!(taker_report.liquidity_side, LiquiditySide::Taker);
303        assert_ne!(maker_report, taker_report);
304    }
305
306    #[rstest]
307    fn test_fill_report_with_different_order_sides() {
308        let buy_report = FillReport::new(
309            AccountId::from("SIM-001"),
310            InstrumentId::from("AUDUSD.SIM"),
311            VenueOrderId::from("1"),
312            TradeId::from("1"),
313            OrderSide::Buy,
314            Quantity::from("100"),
315            Price::from("0.80000"),
316            Money::new(5.0, Currency::USD()),
317            LiquiditySide::Taker,
318            None,
319            None,
320            UnixNanos::from(1_000_000_000),
321            UnixNanos::from(2_000_000_000),
322            None,
323        );
324
325        let sell_report = FillReport::new(
326            AccountId::from("SIM-001"),
327            InstrumentId::from("AUDUSD.SIM"),
328            VenueOrderId::from("1"),
329            TradeId::from("1"),
330            OrderSide::Sell,
331            Quantity::from("100"),
332            Price::from("0.80000"),
333            Money::new(5.0, Currency::USD()),
334            LiquiditySide::Taker,
335            None,
336            None,
337            UnixNanos::from(1_000_000_000),
338            UnixNanos::from(2_000_000_000),
339            None,
340        );
341
342        assert_eq!(buy_report.order_side, OrderSide::Buy);
343        assert_eq!(sell_report.order_side, OrderSide::Sell);
344        assert_ne!(buy_report, sell_report);
345    }
346}