1use 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#[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 pub account_id: AccountId,
42 pub instrument_id: InstrumentId,
44 pub venue_order_id: VenueOrderId,
46 pub trade_id: TradeId,
48 pub order_side: OrderSide,
50 pub last_qty: Quantity,
52 pub last_px: Price,
54 pub commission: Money,
56 pub liquidity_side: LiquiditySide,
58 pub avg_px: Option<Decimal>,
60 pub report_id: UUID4,
62 pub ts_event: UnixNanos,
64 pub ts_init: UnixNanos,
66 pub client_order_id: Option<ClientOrderId>,
68 pub venue_position_id: Option<PositionId>,
70}
71
72impl FillReport {
73 #[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 #[must_use]
113 pub const fn has_client_order_id(&self) -> bool {
114 self.client_order_id.is_some()
115 }
116
117 #[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, );
212
213 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 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}