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