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