1use std::fmt::{Debug, Display};
17
18use indexmap::IndexMap;
19use nautilus_core::{UUID4, UnixNanos};
20use rust_decimal::Decimal;
21use serde::{Deserialize, Serialize};
22use ustr::Ustr;
23
24use crate::{
25 enums::{
26 ContingencyType, LiquiditySide, OrderSide, OrderType, TimeInForce, TrailingOffsetType,
27 TriggerType,
28 },
29 events::OrderEvent,
30 identifiers::{
31 AccountId, ClientOrderId, ExecAlgorithmId, InstrumentId, OrderListId, PositionId,
32 StrategyId, TradeId, TraderId, VenueOrderId,
33 },
34 types::{Currency, Money, Price, Quantity},
35};
36
37#[repr(C)]
38#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(tag = "type")]
40#[cfg_attr(
41 feature = "python",
42 pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
43)]
44#[cfg_attr(
45 feature = "python",
46 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
47)]
48pub struct OrderFilled {
49 pub trader_id: TraderId,
51 pub strategy_id: StrategyId,
53 pub instrument_id: InstrumentId,
55 pub client_order_id: ClientOrderId,
57 pub venue_order_id: VenueOrderId,
58 pub account_id: AccountId,
60 pub trade_id: TradeId,
62 pub order_side: OrderSide,
64 pub order_type: OrderType,
66 pub last_qty: Quantity,
68 pub last_px: Price,
70 pub currency: Currency,
72 pub liquidity_side: LiquiditySide,
74 pub event_id: UUID4,
76 pub ts_event: UnixNanos,
78 pub ts_init: UnixNanos,
80 pub reconciliation: bool,
82 pub position_id: Option<PositionId>,
84 pub commission: Option<Money>,
86 pub info: Option<IndexMap<Ustr, Ustr>>,
88 #[serde(default, skip_serializing_if = "Option::is_none")]
90 pub causation_id: Option<UUID4>,
91}
92
93impl OrderFilled {
94 #[expect(clippy::too_many_arguments)]
96 #[must_use]
97 pub fn new(
98 trader_id: TraderId,
99 strategy_id: StrategyId,
100 instrument_id: InstrumentId,
101 client_order_id: ClientOrderId,
102 venue_order_id: VenueOrderId,
103 account_id: AccountId,
104 trade_id: TradeId,
105 order_side: OrderSide,
106 order_type: OrderType,
107 last_qty: Quantity,
108 last_px: Price,
109 currency: Currency,
110 liquidity_side: LiquiditySide,
111 event_id: UUID4,
112 ts_event: UnixNanos,
113 ts_init: UnixNanos,
114 reconciliation: bool,
115 position_id: Option<PositionId>,
116 commission: Option<Money>,
117 info: Option<IndexMap<Ustr, Ustr>>,
118 ) -> Self {
119 Self {
120 trader_id,
121 strategy_id,
122 instrument_id,
123 client_order_id,
124 venue_order_id,
125 account_id,
126 trade_id,
127 order_side,
128 order_type,
129 last_qty,
130 last_px,
131 currency,
132 liquidity_side,
133 event_id,
134 ts_event,
135 ts_init,
136 reconciliation,
137 position_id,
138 commission,
139 info,
140 causation_id: None,
141 }
142 }
143
144 #[must_use]
145 pub fn is_buy(&self) -> bool {
146 self.order_side == OrderSide::Buy
147 }
148
149 #[must_use]
150 pub fn is_sell(&self) -> bool {
151 self.order_side == OrderSide::Sell
152 }
153
154 pub fn split_for_position_flip(
162 &self,
163 closing_qty: Quantity,
164 opening_position_id: Option<PositionId>,
165 opening_event_id: UUID4,
166 ) -> anyhow::Result<(Self, Self)> {
167 anyhow::ensure!(!closing_qty.is_zero(), "closing quantity was zero");
168 anyhow::ensure!(
169 closing_qty.raw < self.last_qty.raw,
170 "closing quantity {closing_qty} must be smaller than fill quantity {}",
171 self.last_qty,
172 );
173
174 let opening_qty =
175 Quantity::from_raw(self.last_qty.raw - closing_qty.raw, closing_qty.precision);
176 let closing_fraction = closing_qty.as_decimal() / self.last_qty.as_decimal();
177 let (closing_commission, opening_commission) = match self.commission {
178 Some(commission) => {
179 let closing = Money::from_decimal(
180 commission.as_decimal() * closing_fraction,
181 commission.currency,
182 )?;
183 (Some(closing), Some(commission - closing))
184 }
185 None => (None, None),
186 };
187
188 let mut closing = self.clone();
189 closing.last_qty = closing_qty;
190 closing.commission = closing_commission;
191
192 let mut opening = self.clone();
193 opening.last_qty = opening_qty;
194 opening.position_id = opening_position_id;
195 opening.commission = opening_commission;
196 opening.event_id = opening_event_id;
197 opening.causation_id = Some(self.event_id);
198
199 Ok((closing, opening))
200 }
201}
202
203impl Debug for OrderFilled {
204 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205 let position_id_str = match self.position_id {
206 Some(position_id) => position_id.to_string(),
207 None => "None".to_string(),
208 };
209 let commission_str = match self.commission {
210 Some(commission) => commission.to_string(),
211 None => "None".to_string(),
212 };
213 write!(
214 f,
215 "{}(\
216 trader_id={}, \
217 strategy_id={}, \
218 instrument_id={}, \
219 client_order_id={}, \
220 venue_order_id={}, \
221 account_id={}, \
222 trade_id={}, \
223 position_id={}, \
224 order_side={}, \
225 order_type={}, \
226 last_qty={}, \
227 last_px={} {}, \
228 commission={}, \
229 liquidity_side={}, \
230 event_id={}, \
231 ts_event={}, \
232 ts_init={})",
233 stringify!(OrderFilled),
234 self.trader_id,
235 self.strategy_id,
236 self.instrument_id,
237 self.client_order_id,
238 self.venue_order_id,
239 self.account_id,
240 self.trade_id,
241 position_id_str,
242 self.order_side,
243 self.order_type,
244 self.last_qty.to_formatted_string(),
245 self.last_px.to_formatted_string(),
246 self.currency,
247 commission_str,
248 self.liquidity_side,
249 self.event_id,
250 self.ts_event,
251 self.ts_init
252 )
253 }
254}
255
256impl Display for OrderFilled {
257 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
258 write!(
259 f,
260 "{}(\
261 instrument_id={}, \
262 client_order_id={}, \
263 venue_order_id={}, \
264 account_id={}, \
265 trade_id={}, \
266 position_id={}, \
267 order_side={}, \
268 order_type={}, \
269 last_qty={}, \
270 last_px={} {}, \
271 commission={}, \
272 liquidity_side={}, \
273 ts_event={})",
274 stringify!(OrderFilled),
275 self.instrument_id,
276 self.client_order_id,
277 self.venue_order_id,
278 self.account_id,
279 self.trade_id,
280 self.position_id
281 .map_or("None".to_string(), |id| id.to_string()),
282 self.order_side,
283 self.order_type,
284 self.last_qty.to_formatted_string(),
285 self.last_px.to_formatted_string(),
286 self.currency,
287 self.commission.unwrap_or(Money::from("0.0 USD")),
288 self.liquidity_side,
289 self.ts_event
290 )
291 }
292}
293
294impl OrderEvent for OrderFilled {
295 fn id(&self) -> UUID4 {
296 self.event_id
297 }
298
299 fn type_name(&self) -> &'static str {
300 stringify!(OrderFilled)
301 }
302
303 fn order_type(&self) -> Option<OrderType> {
304 Some(self.order_type)
305 }
306
307 fn order_side(&self) -> Option<OrderSide> {
308 Some(self.order_side)
309 }
310
311 fn trader_id(&self) -> TraderId {
312 self.trader_id
313 }
314
315 fn strategy_id(&self) -> StrategyId {
316 self.strategy_id
317 }
318
319 fn instrument_id(&self) -> InstrumentId {
320 self.instrument_id
321 }
322
323 fn trade_id(&self) -> Option<TradeId> {
324 Some(self.trade_id)
325 }
326
327 fn currency(&self) -> Option<Currency> {
328 Some(self.currency)
329 }
330
331 fn client_order_id(&self) -> ClientOrderId {
332 self.client_order_id
333 }
334
335 fn reason(&self) -> Option<Ustr> {
336 None
337 }
338
339 fn quantity(&self) -> Option<Quantity> {
340 Some(self.last_qty)
341 }
342
343 fn time_in_force(&self) -> Option<TimeInForce> {
344 None
345 }
346
347 fn liquidity_side(&self) -> Option<LiquiditySide> {
348 Some(self.liquidity_side)
349 }
350
351 fn post_only(&self) -> Option<bool> {
352 None
353 }
354
355 fn reduce_only(&self) -> Option<bool> {
356 None
357 }
358
359 fn quote_quantity(&self) -> Option<bool> {
360 None
361 }
362
363 fn reconciliation(&self) -> bool {
364 self.reconciliation
365 }
366
367 fn price(&self) -> Option<Price> {
368 None
369 }
370
371 fn last_px(&self) -> Option<Price> {
372 Some(self.last_px)
373 }
374
375 fn last_qty(&self) -> Option<Quantity> {
376 Some(self.last_qty)
377 }
378
379 fn activation_price(&self) -> Option<Price> {
380 None
381 }
382
383 fn trigger_price(&self) -> Option<Price> {
384 None
385 }
386
387 fn trigger_type(&self) -> Option<TriggerType> {
388 None
389 }
390
391 fn limit_offset(&self) -> Option<Decimal> {
392 None
393 }
394
395 fn trailing_offset(&self) -> Option<Decimal> {
396 None
397 }
398
399 fn trailing_offset_type(&self) -> Option<TrailingOffsetType> {
400 None
401 }
402
403 fn expire_time(&self) -> Option<UnixNanos> {
404 None
405 }
406
407 fn display_qty(&self) -> Option<Quantity> {
408 None
409 }
410
411 fn emulation_trigger(&self) -> Option<TriggerType> {
412 None
413 }
414
415 fn trigger_instrument_id(&self) -> Option<InstrumentId> {
416 None
417 }
418
419 fn contingency_type(&self) -> Option<ContingencyType> {
420 None
421 }
422
423 fn order_list_id(&self) -> Option<OrderListId> {
424 None
425 }
426
427 fn linked_order_ids(&self) -> Option<Vec<ClientOrderId>> {
428 None
429 }
430
431 fn parent_order_id(&self) -> Option<ClientOrderId> {
432 None
433 }
434
435 fn exec_algorithm_id(&self) -> Option<ExecAlgorithmId> {
436 None
437 }
438
439 fn exec_spawn_id(&self) -> Option<ClientOrderId> {
440 None
441 }
442
443 fn venue_order_id(&self) -> Option<VenueOrderId> {
444 Some(self.venue_order_id)
445 }
446
447 fn account_id(&self) -> Option<AccountId> {
448 Some(self.account_id)
449 }
450
451 fn position_id(&self) -> Option<PositionId> {
452 self.position_id
453 }
454
455 fn commission(&self) -> Option<Money> {
456 self.commission
457 }
458
459 fn ts_event(&self) -> UnixNanos {
460 self.ts_event
461 }
462
463 fn ts_init(&self) -> UnixNanos {
464 self.ts_init
465 }
466}
467
468#[cfg(test)]
469mod tests {
470 use nautilus_core::UnixNanos;
471 use rstest::rstest;
472
473 use super::*;
474 use crate::{
475 enums::OrderSide,
476 events::order::stubs::*,
477 identifiers::PositionId,
478 types::{Currency, Money, Price, Quantity},
479 };
480
481 fn create_test_order_filled() -> OrderFilled {
482 OrderFilled::new(
483 TraderId::from("TRADER-001"),
484 StrategyId::from("EMA-CROSS"),
485 InstrumentId::from("EURUSD.SIM"),
486 ClientOrderId::from("O-19700101-000000-001-001-1"),
487 VenueOrderId::from("V-001"),
488 AccountId::from("SIM-001"),
489 TradeId::from("T-001"),
490 OrderSide::Buy,
491 OrderType::Market,
492 Quantity::from("100"),
493 Price::from("1.0500"),
494 Currency::USD(),
495 LiquiditySide::Taker,
496 UUID4::default(),
497 UnixNanos::from(1_000_000_000),
498 UnixNanos::from(2_000_000_000),
499 false,
500 Some(PositionId::from("P-001")),
501 Some(Money::new(2.5, Currency::USD())),
502 None,
503 )
504 }
505
506 #[rstest]
507 fn test_order_filled_display(order_filled: OrderFilled) {
508 let display = format!("{order_filled}");
509 assert_eq!(
510 display,
511 "OrderFilled(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-000000-001-001-1, \
512 venue_order_id=123456, account_id=SIM-001, trade_id=1, position_id=None, \
513 order_side=BUY, order_type=LIMIT, last_qty=0.561, last_px=22_000 USDT, \
514 commission=12.20000000 USDT, liquidity_side=TAKER, ts_event=0)"
515 );
516 }
517
518 #[rstest]
519 fn test_order_filled_is_buy(order_filled: OrderFilled) {
520 assert!(order_filled.is_buy());
521 assert!(!order_filled.is_sell());
522 }
523
524 #[rstest]
525 fn test_order_filled_info_round_trips_through_serde(order_filled: OrderFilled) {
526 let mut info = IndexMap::new();
527 info.insert(Ustr::from("liquidation"), Ustr::from("true"));
528 info.insert(Ustr::from("maker_order_id"), Ustr::from("ABC-123"));
529 let original = OrderFilled {
530 info: Some(info),
531 ..order_filled
532 };
533
534 let json = serde_json::to_string(&original).unwrap();
535 let deserialized: OrderFilled = serde_json::from_str(&json).unwrap();
536
537 assert_eq!(deserialized.info, original.info);
538 assert_eq!(deserialized, original);
539 }
540
541 #[rstest]
542 fn test_order_filled_is_sell() {
543 let mut order_filled = create_test_order_filled();
544 order_filled.order_side = OrderSide::Sell;
545
546 assert!(order_filled.is_sell());
547 assert!(!order_filled.is_buy());
548 }
549
550 #[rstest]
551 fn test_split_for_position_flip_preserves_provenance_and_commission() {
552 let mut fill = create_test_order_filled();
553 fill.last_qty = Quantity::from(100);
554 fill.commission = Some(Money::new(2.5, Currency::USD()));
555 let source_event_id = fill.event_id;
556 let opening_event_id = UUID4::new();
557 let opening_position_id = PositionId::from("P-FLIPPED");
558
559 let (closing, opening) = fill
560 .split_for_position_flip(
561 Quantity::from(40),
562 Some(opening_position_id),
563 opening_event_id,
564 )
565 .expect("split fill");
566
567 assert_eq!(closing.last_qty, Quantity::from(40));
568 assert_eq!(closing.position_id, fill.position_id);
569 assert_eq!(closing.event_id, source_event_id);
570 assert_eq!(closing.causation_id, fill.causation_id);
571 assert_eq!(closing.commission, Some(Money::new(1.0, Currency::USD())));
572 assert_eq!(opening.last_qty, Quantity::from(60));
573 assert_eq!(opening.position_id, Some(opening_position_id));
574 assert_eq!(opening.event_id, opening_event_id);
575 assert_eq!(opening.causation_id, Some(source_event_id));
576 assert_eq!(opening.commission, Some(Money::new(1.5, Currency::USD())));
577 }
578
579 #[rstest]
580 fn test_order_filled_without_position_id_display() {
581 let mut order_filled = create_test_order_filled();
582 order_filled.position_id = None;
583
584 let display = format!("{order_filled}");
585 assert!(display.contains("position_id=None"));
586 assert!(!display.contains("position_id=P-001"));
587 }
588
589 #[rstest]
590 fn test_order_filled_without_commission_serialization() {
591 let mut order_filled = create_test_order_filled();
592 order_filled.commission = None;
593
594 let json = serde_json::to_string(&order_filled).unwrap();
595 let deserialized: OrderFilled = serde_json::from_str(&json).unwrap();
596
597 assert_eq!(deserialized.commission, None);
598 assert_eq!(deserialized.trade_id, order_filled.trade_id);
599 }
600
601 #[rstest]
602 fn test_order_filled_serialization() {
603 let original = create_test_order_filled();
604
605 let json = serde_json::to_string(&original).unwrap();
606 let deserialized: OrderFilled = serde_json::from_str(&json).unwrap();
607
608 assert_eq!(original, deserialized);
609 }
610
611 #[rstest]
612 fn test_order_filled_serialization_with_causation_id() {
613 let causation_id = UUID4::new();
614 let original = OrderFilled {
615 causation_id: Some(causation_id),
616 ..create_test_order_filled()
617 };
618
619 let json = serde_json::to_string(&original).unwrap();
620 let deserialized: OrderFilled = serde_json::from_str(&json).unwrap();
621
622 assert!(json.contains("\"causation_id\""));
623 assert_eq!(deserialized.causation_id, Some(causation_id));
624 assert_eq!(original, deserialized);
625 }
626}