Skip to main content

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