nautilus_model/events/order/
filled.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::{Debug, Display};
17
18use derive_builder::Builder;
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, OrderSideSpecified, OrderType, TimeInForce,
27        TrailingOffsetType, 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, Copy, PartialEq, Eq, Serialize, Deserialize, Builder)]
39#[serde(tag = "type")]
40#[cfg_attr(any(test, feature = "stubs"), builder(default))]
41#[cfg_attr(
42    feature = "python",
43    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
44)]
45pub struct OrderFilled {
46    /// The trader ID associated with the event.
47    pub trader_id: TraderId,
48    /// The strategy ID associated with the event.
49    pub strategy_id: StrategyId,
50    /// The instrument ID associated with the event.
51    pub instrument_id: InstrumentId,
52    /// The client order ID associated with the event.
53    pub client_order_id: ClientOrderId,
54    pub venue_order_id: VenueOrderId,
55    /// The account ID associated with the event.
56    pub account_id: AccountId,
57    /// The trade match ID (assigned by the venue).
58    pub trade_id: TradeId,
59    /// The order side.
60    pub order_side: OrderSide,
61    /// The order type.
62    pub order_type: OrderType,
63    /// The fill quantity for this execution.
64    pub last_qty: Quantity,
65    /// The fill price for this execution.
66    pub last_px: Price,
67    /// The currency of the `last_px`.
68    pub currency: Currency,
69    /// The liquidity side of the execution.
70    pub liquidity_side: LiquiditySide,
71    /// The unique identifier for the event.
72    pub event_id: UUID4,
73    /// UNIX timestamp (nanoseconds) when the event occurred.
74    pub ts_event: UnixNanos,
75    /// UNIX timestamp (nanoseconds) when the event was initialized.
76    pub ts_init: UnixNanos,
77    /// If the event was generated during reconciliation.
78    pub reconciliation: bool,
79    /// The position ID (assigned by the venue).
80    pub position_id: Option<PositionId>,
81    /// The commission generated from this execution.
82    pub commission: Option<Money>,
83}
84
85impl OrderFilled {
86    /// Creates a new [`OrderFilled`] instance.
87    #[allow(clippy::too_many_arguments)]
88    pub fn new(
89        trader_id: TraderId,
90        strategy_id: StrategyId,
91        instrument_id: InstrumentId,
92        client_order_id: ClientOrderId,
93        venue_order_id: VenueOrderId,
94        account_id: AccountId,
95        trade_id: TradeId,
96        order_side: OrderSide,
97        order_type: OrderType,
98        last_qty: Quantity,
99        last_px: Price,
100        currency: Currency,
101        liquidity_side: LiquiditySide,
102        event_id: UUID4,
103        ts_event: UnixNanos,
104        ts_init: UnixNanos,
105        reconciliation: bool,
106        position_id: Option<PositionId>,
107        commission: Option<Money>,
108    ) -> Self {
109        Self {
110            trader_id,
111            strategy_id,
112            instrument_id,
113            client_order_id,
114            venue_order_id,
115            account_id,
116            trade_id,
117            order_side,
118            order_type,
119            last_qty,
120            last_px,
121            currency,
122            liquidity_side,
123            event_id,
124            ts_event,
125            ts_init,
126            reconciliation,
127            position_id,
128            commission,
129        }
130    }
131
132    #[must_use]
133    pub fn specified_side(&self) -> OrderSideSpecified {
134        self.order_side.as_specified()
135    }
136
137    #[must_use]
138    pub fn is_buy(&self) -> bool {
139        self.order_side == OrderSide::Buy
140    }
141
142    #[must_use]
143    pub fn is_sell(&self) -> bool {
144        self.order_side == OrderSide::Sell
145    }
146}
147
148impl Debug for OrderFilled {
149    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150        let position_id_str = match self.position_id {
151            Some(position_id) => position_id.to_string(),
152            None => "None".to_string(),
153        };
154        let commission_str = match self.commission {
155            Some(commission) => commission.to_string(),
156            None => "None".to_string(),
157        };
158        write!(
159            f,
160            "{}(\
161            trader_id={}, \
162            strategy_id={}, \
163            instrument_id={}, \
164            client_order_id={}, \
165            venue_order_id={}, \
166            account_id={}, \
167            trade_id={}, \
168            position_id={}, \
169            order_side={}, \
170            order_type={}, \
171            last_qty={}, \
172            last_px={} {}, \
173            commission={}, \
174            liquidity_side={}, \
175            event_id={}, \
176            ts_event={}, \
177            ts_init={})",
178            stringify!(OrderFilled),
179            self.trader_id,
180            self.strategy_id,
181            self.instrument_id,
182            self.client_order_id,
183            self.venue_order_id,
184            self.account_id,
185            self.trade_id,
186            position_id_str,
187            self.order_side,
188            self.order_type,
189            self.last_qty.to_formatted_string(),
190            self.last_px.to_formatted_string(),
191            self.currency,
192            commission_str,
193            self.liquidity_side,
194            self.event_id,
195            self.ts_event,
196            self.ts_init
197        )
198    }
199}
200
201impl Display for OrderFilled {
202    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203        write!(
204            f,
205            "{}(\
206            instrument_id={}, \
207            client_order_id={}, \
208            venue_order_id={}, \
209            account_id={}, \
210            trade_id={}, \
211            position_id={}, \
212            order_side={}, \
213            order_type={}, \
214            last_qty={}, \
215            last_px={} {}, \
216            commission={}, \
217            liquidity_side={}, \
218            ts_event={})",
219            stringify!(OrderFilled),
220            self.instrument_id,
221            self.client_order_id,
222            self.venue_order_id,
223            self.account_id,
224            self.trade_id,
225            self.position_id
226                .map_or("None".to_string(), |id| id.to_string()),
227            self.order_side,
228            self.order_type,
229            self.last_qty.to_formatted_string(),
230            self.last_px.to_formatted_string(),
231            self.currency,
232            self.commission.unwrap_or(Money::from("0.0 USD")),
233            self.liquidity_side,
234            self.ts_event
235        )
236    }
237}
238
239impl OrderEvent for OrderFilled {
240    fn id(&self) -> UUID4 {
241        self.event_id
242    }
243
244    fn kind(&self) -> &str {
245        stringify!(OrderFilled)
246    }
247
248    fn order_type(&self) -> Option<OrderType> {
249        Some(self.order_type)
250    }
251
252    fn order_side(&self) -> Option<OrderSide> {
253        Some(self.order_side)
254    }
255
256    fn trader_id(&self) -> TraderId {
257        self.trader_id
258    }
259
260    fn strategy_id(&self) -> StrategyId {
261        self.strategy_id
262    }
263
264    fn instrument_id(&self) -> InstrumentId {
265        self.instrument_id
266    }
267
268    fn trade_id(&self) -> Option<TradeId> {
269        Some(self.trade_id)
270    }
271
272    fn currency(&self) -> Option<Currency> {
273        Some(self.currency)
274    }
275
276    fn client_order_id(&self) -> ClientOrderId {
277        self.client_order_id
278    }
279
280    fn reason(&self) -> Option<Ustr> {
281        None
282    }
283
284    fn quantity(&self) -> Option<Quantity> {
285        Some(self.last_qty)
286    }
287
288    fn time_in_force(&self) -> Option<TimeInForce> {
289        None
290    }
291
292    fn liquidity_side(&self) -> Option<LiquiditySide> {
293        Some(self.liquidity_side)
294    }
295
296    fn post_only(&self) -> Option<bool> {
297        None
298    }
299
300    fn reduce_only(&self) -> Option<bool> {
301        None
302    }
303
304    fn quote_quantity(&self) -> Option<bool> {
305        None
306    }
307
308    fn reconciliation(&self) -> bool {
309        self.reconciliation
310    }
311
312    fn price(&self) -> Option<Price> {
313        None
314    }
315
316    fn last_px(&self) -> Option<Price> {
317        Some(self.last_px)
318    }
319
320    fn last_qty(&self) -> Option<Quantity> {
321        Some(self.last_qty)
322    }
323
324    fn trigger_price(&self) -> Option<Price> {
325        None
326    }
327
328    fn trigger_type(&self) -> Option<TriggerType> {
329        None
330    }
331
332    fn limit_offset(&self) -> Option<Decimal> {
333        None
334    }
335
336    fn trailing_offset(&self) -> Option<Decimal> {
337        None
338    }
339
340    fn trailing_offset_type(&self) -> Option<TrailingOffsetType> {
341        None
342    }
343
344    fn expire_time(&self) -> Option<UnixNanos> {
345        None
346    }
347
348    fn display_qty(&self) -> Option<Quantity> {
349        None
350    }
351
352    fn emulation_trigger(&self) -> Option<TriggerType> {
353        None
354    }
355
356    fn trigger_instrument_id(&self) -> Option<InstrumentId> {
357        None
358    }
359
360    fn contingency_type(&self) -> Option<ContingencyType> {
361        None
362    }
363
364    fn order_list_id(&self) -> Option<OrderListId> {
365        None
366    }
367
368    fn linked_order_ids(&self) -> Option<Vec<ClientOrderId>> {
369        None
370    }
371
372    fn parent_order_id(&self) -> Option<ClientOrderId> {
373        None
374    }
375
376    fn exec_algorithm_id(&self) -> Option<ExecAlgorithmId> {
377        None
378    }
379
380    fn exec_spawn_id(&self) -> Option<ClientOrderId> {
381        None
382    }
383
384    fn venue_order_id(&self) -> Option<VenueOrderId> {
385        Some(self.venue_order_id)
386    }
387
388    fn account_id(&self) -> Option<AccountId> {
389        Some(self.account_id)
390    }
391
392    fn position_id(&self) -> Option<PositionId> {
393        self.position_id
394    }
395
396    fn commission(&self) -> Option<Money> {
397        self.commission
398    }
399
400    fn ts_event(&self) -> UnixNanos {
401        self.ts_event
402    }
403
404    fn ts_init(&self) -> UnixNanos {
405        self.ts_init
406    }
407}
408
409#[cfg(test)]
410mod tests {
411    use nautilus_core::UnixNanos;
412    use rstest::rstest;
413
414    use super::*;
415    use crate::{
416        enums::{LiquiditySide, OrderSide, OrderSideSpecified, OrderType},
417        events::order::stubs::*,
418        identifiers::{
419            AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, TraderId,
420            VenueOrderId,
421        },
422        stubs::TestDefault,
423        types::{Currency, Money, Price, Quantity},
424    };
425
426    fn create_test_order_filled() -> OrderFilled {
427        OrderFilled::new(
428            TraderId::from("TRADER-001"),
429            StrategyId::from("EMA-CROSS"),
430            InstrumentId::from("EURUSD.SIM"),
431            ClientOrderId::from("O-19700101-000000-001-001-1"),
432            VenueOrderId::from("V-001"),
433            AccountId::from("SIM-001"),
434            TradeId::from("T-001"),
435            OrderSide::Buy,
436            OrderType::Market,
437            Quantity::from("100"),
438            Price::from("1.0500"),
439            Currency::USD(),
440            LiquiditySide::Taker,
441            Default::default(),
442            UnixNanos::from(1_000_000_000),
443            UnixNanos::from(2_000_000_000),
444            false,
445            Some(PositionId::from("P-001")),
446            Some(Money::new(2.5, Currency::USD())),
447        )
448    }
449
450    #[rstest]
451    fn test_order_filled_new() {
452        let order_filled = create_test_order_filled();
453
454        assert_eq!(order_filled.trader_id, TraderId::from("TRADER-001"));
455        assert_eq!(order_filled.strategy_id, StrategyId::from("EMA-CROSS"));
456        assert_eq!(order_filled.instrument_id, InstrumentId::from("EURUSD.SIM"));
457        assert_eq!(
458            order_filled.client_order_id,
459            ClientOrderId::from("O-19700101-000000-001-001-1")
460        );
461        assert_eq!(order_filled.venue_order_id, VenueOrderId::from("V-001"));
462        assert_eq!(order_filled.account_id, AccountId::from("SIM-001"));
463        assert_eq!(order_filled.trade_id, TradeId::from("T-001"));
464        assert_eq!(order_filled.order_side, OrderSide::Buy);
465        assert_eq!(order_filled.order_type, OrderType::Market);
466        assert_eq!(order_filled.last_qty, Quantity::from("100"));
467        assert_eq!(order_filled.last_px, Price::from("1.0500"));
468        assert_eq!(order_filled.currency, Currency::USD());
469        assert_eq!(order_filled.liquidity_side, LiquiditySide::Taker);
470        assert_eq!(order_filled.position_id, Some(PositionId::from("P-001")));
471        assert_eq!(
472            order_filled.commission,
473            Some(Money::new(2.5, Currency::USD()))
474        );
475        assert!(!order_filled.reconciliation);
476    }
477
478    #[rstest]
479    fn test_order_filled_display(order_filled: OrderFilled) {
480        let display = format!("{order_filled}");
481        assert_eq!(
482            display,
483            "OrderFilled(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-000000-001-001-1, \
484            venue_order_id=123456, account_id=SIM-001, trade_id=1, position_id=None, \
485            order_side=BUY, order_type=LIMIT, last_qty=0.561, last_px=22_000 USDT, \
486            commission=12.20000000 USDT, liquidity_side=TAKER, ts_event=0)"
487        );
488    }
489
490    #[rstest]
491    fn test_order_filled_is_buy(order_filled: OrderFilled) {
492        assert!(order_filled.is_buy());
493        assert!(!order_filled.is_sell());
494    }
495
496    #[rstest]
497    fn test_order_filled_is_sell() {
498        let mut order_filled = create_test_order_filled();
499        order_filled.order_side = OrderSide::Sell;
500
501        assert!(order_filled.is_sell());
502        assert!(!order_filled.is_buy());
503    }
504
505    #[rstest]
506    fn test_order_filled_specified_side() {
507        let buy_order = create_test_order_filled();
508        assert_eq!(buy_order.specified_side(), OrderSideSpecified::Buy);
509
510        let mut sell_order = create_test_order_filled();
511        sell_order.order_side = OrderSide::Sell;
512        assert_eq!(sell_order.specified_side(), OrderSideSpecified::Sell);
513    }
514
515    #[rstest]
516    fn test_order_filled_default() {
517        let order_filled = OrderFilled::default();
518
519        assert_eq!(order_filled.trader_id, TraderId::test_default());
520        assert_eq!(order_filled.strategy_id, StrategyId::test_default());
521        assert_eq!(order_filled.instrument_id, InstrumentId::test_default());
522        assert_eq!(order_filled.client_order_id, ClientOrderId::test_default());
523        assert_eq!(order_filled.venue_order_id, VenueOrderId::test_default());
524        assert_eq!(order_filled.account_id, AccountId::test_default());
525        assert_eq!(order_filled.trade_id, TradeId::test_default());
526        assert_eq!(order_filled.order_side, OrderSide::Buy);
527        assert_eq!(order_filled.order_type, OrderType::Market);
528        assert_eq!(order_filled.currency, Currency::USD());
529        assert_eq!(order_filled.liquidity_side, LiquiditySide::Taker);
530        assert_eq!(order_filled.position_id, None);
531        assert_eq!(order_filled.commission, None);
532        assert!(!order_filled.reconciliation);
533    }
534
535    #[rstest]
536    fn test_order_filled_order_event_trait() {
537        let order_filled = create_test_order_filled();
538
539        assert_eq!(order_filled.id(), order_filled.event_id);
540        assert_eq!(order_filled.kind(), "OrderFilled");
541        assert_eq!(order_filled.order_type(), Some(OrderType::Market));
542        assert_eq!(order_filled.order_side(), Some(OrderSide::Buy));
543        assert_eq!(order_filled.trader_id(), TraderId::from("TRADER-001"));
544        assert_eq!(order_filled.strategy_id(), StrategyId::from("EMA-CROSS"));
545        assert_eq!(
546            order_filled.instrument_id(),
547            InstrumentId::from("EURUSD.SIM")
548        );
549        assert_eq!(order_filled.trade_id(), Some(TradeId::from("T-001")));
550        assert_eq!(order_filled.currency(), Some(Currency::USD()));
551        assert_eq!(
552            order_filled.client_order_id(),
553            ClientOrderId::from("O-19700101-000000-001-001-1")
554        );
555        assert_eq!(order_filled.reason(), None);
556        assert_eq!(order_filled.quantity(), Some(Quantity::from("100")));
557        assert_eq!(order_filled.liquidity_side(), Some(LiquiditySide::Taker));
558        assert!(!order_filled.reconciliation());
559        assert_eq!(
560            order_filled.venue_order_id(),
561            Some(VenueOrderId::from("V-001"))
562        );
563        assert_eq!(order_filled.account_id(), Some(AccountId::from("SIM-001")));
564        assert_eq!(order_filled.position_id(), Some(PositionId::from("P-001")));
565        assert_eq!(
566            order_filled.commission(),
567            Some(Money::new(2.5, Currency::USD()))
568        );
569        assert_eq!(order_filled.last_px(), Some(Price::from("1.0500")));
570        assert_eq!(order_filled.last_qty(), Some(Quantity::from("100")));
571    }
572
573    #[rstest]
574    fn test_order_filled_different_order_types() {
575        let mut market_order = create_test_order_filled();
576        market_order.order_type = OrderType::Market;
577
578        let mut limit_order = create_test_order_filled();
579        limit_order.order_type = OrderType::Limit;
580
581        let mut stop_order = create_test_order_filled();
582        stop_order.order_type = OrderType::StopMarket;
583
584        assert_ne!(market_order, limit_order);
585        assert_ne!(limit_order, stop_order);
586        assert_eq!(market_order.order_type, OrderType::Market);
587        assert_eq!(limit_order.order_type, OrderType::Limit);
588        assert_eq!(stop_order.order_type, OrderType::StopMarket);
589    }
590
591    #[rstest]
592    fn test_order_filled_different_liquidity_sides() {
593        let mut taker = create_test_order_filled();
594        taker.liquidity_side = LiquiditySide::Taker;
595
596        let mut maker = create_test_order_filled();
597        maker.liquidity_side = LiquiditySide::Maker;
598
599        assert_ne!(taker, maker);
600        assert_eq!(taker.liquidity_side, LiquiditySide::Taker);
601        assert_eq!(maker.liquidity_side, LiquiditySide::Maker);
602    }
603
604    #[rstest]
605    fn test_order_filled_without_position_id() {
606        let mut order_filled = create_test_order_filled();
607        order_filled.position_id = None;
608
609        assert!(order_filled.position_id.is_none());
610    }
611
612    #[rstest]
613    fn test_order_filled_without_commission() {
614        let mut order_filled = create_test_order_filled();
615        order_filled.commission = None;
616
617        assert!(order_filled.commission.is_none());
618    }
619
620    #[rstest]
621    fn test_order_filled_with_reconciliation() {
622        let mut order_filled = create_test_order_filled();
623        order_filled.reconciliation = true;
624
625        assert!(order_filled.reconciliation);
626    }
627
628    #[rstest]
629    fn test_order_filled_clone() {
630        let order_filled1 = create_test_order_filled();
631        let order_filled2 = order_filled1;
632
633        assert_eq!(order_filled1, order_filled2);
634    }
635
636    #[rstest]
637    fn test_order_filled_debug() {
638        let order_filled = create_test_order_filled();
639        let debug_str = format!("{order_filled:?}");
640
641        assert!(debug_str.contains("OrderFilled"));
642        assert!(debug_str.contains("TRADER-001"));
643        assert!(debug_str.contains("EMA-CROSS"));
644        assert!(debug_str.contains("EURUSD.SIM"));
645        assert!(debug_str.contains("P-001"));
646    }
647
648    #[rstest]
649    fn test_order_filled_partial_eq() {
650        let order_filled1 = create_test_order_filled();
651        let mut order_filled2 = create_test_order_filled();
652        order_filled2.event_id = order_filled1.event_id; // Make event_ids equal
653        let mut order_filled3 = create_test_order_filled();
654        order_filled3.trade_id = TradeId::from("T-002");
655
656        assert_eq!(order_filled1, order_filled2);
657        assert_ne!(order_filled1, order_filled3);
658    }
659
660    #[rstest]
661    fn test_order_filled_timestamps() {
662        let order_filled = create_test_order_filled();
663
664        assert_eq!(order_filled.ts_event, UnixNanos::from(1_000_000_000));
665        assert_eq!(order_filled.ts_init, UnixNanos::from(2_000_000_000));
666        assert!(order_filled.ts_event < order_filled.ts_init);
667    }
668
669    #[rstest]
670    fn test_order_filled_different_currencies() {
671        let mut usd_fill = create_test_order_filled();
672        usd_fill.currency = Currency::USD();
673
674        let mut eur_fill = create_test_order_filled();
675        eur_fill.currency = Currency::EUR();
676
677        assert_ne!(usd_fill, eur_fill);
678        assert_eq!(usd_fill.currency, Currency::USD());
679        assert_eq!(eur_fill.currency, Currency::EUR());
680    }
681
682    #[rstest]
683    fn test_order_filled_different_prices_and_quantities() {
684        let mut large_fill = create_test_order_filled();
685        large_fill.last_qty = Quantity::from("1000");
686        large_fill.last_px = Price::from("1.1000");
687
688        let mut small_fill = create_test_order_filled();
689        small_fill.last_qty = Quantity::from("100");
690        small_fill.last_px = Price::from("1.0500");
691
692        assert_ne!(large_fill, small_fill);
693        assert_eq!(large_fill.last_qty, Quantity::from("1000"));
694        assert_eq!(large_fill.last_px, Price::from("1.1000"));
695        assert_eq!(small_fill.last_qty, Quantity::from("100"));
696        assert_eq!(small_fill.last_px, Price::from("1.0500"));
697    }
698
699    #[rstest]
700    fn test_order_filled_serialization() {
701        let original = create_test_order_filled();
702
703        let json = serde_json::to_string(&original).unwrap();
704        let deserialized: OrderFilled = serde_json::from_str(&json).unwrap();
705
706        assert_eq!(original, deserialized);
707    }
708}