Skip to main content

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 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    /// The trader ID associated with the event.
51    pub trader_id: TraderId,
52    /// The strategy ID associated with the event.
53    pub strategy_id: StrategyId,
54    /// The instrument ID associated with the event.
55    pub instrument_id: InstrumentId,
56    /// The client order ID associated with the event.
57    pub client_order_id: ClientOrderId,
58    /// The venue order ID associated with the event.
59    pub venue_order_id: VenueOrderId,
60    /// The account ID associated with the event.
61    pub account_id: AccountId,
62    /// The trade match ID (assigned by the venue).
63    pub trade_id: TradeId,
64    /// The order side.
65    pub order_side: OrderSide,
66    /// The order type.
67    pub order_type: OrderType,
68    /// The fill quantity for this execution.
69    pub last_qty: Quantity,
70    /// The fill price for this execution.
71    pub last_px: Price,
72    /// The currency of the `last_px`.
73    pub currency: Currency,
74    /// The liquidity side of the execution.
75    pub liquidity_side: LiquiditySide,
76    /// The unique identifier for the event.
77    pub event_id: UUID4,
78    /// UNIX timestamp (nanoseconds) when the event occurred.
79    pub ts_event: UnixNanos,
80    /// UNIX timestamp (nanoseconds) when the event was initialized.
81    pub ts_init: UnixNanos,
82    /// If the event was generated during reconciliation.
83    pub reconciliation: bool,
84    /// The position ID (assigned by the venue).
85    pub position_id: Option<PositionId>,
86    /// The commission generated from this execution.
87    pub commission: Option<Money>,
88    /// Additional fill metadata (venue/adapter specific).
89    pub info: Option<IndexMap<Ustr, Ustr>>,
90    /// The causation ID associated with the event.
91    #[serde(default, skip_serializing_if = "Option::is_none")]
92    pub causation_id: Option<UUID4>,
93}
94
95impl OrderFilled {
96    /// Creates a new [`OrderFilled`] instance.
97    #[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    /// Splits an overfill into the fragment which closes the current position and the
157    /// fragment which opens the flipped position.
158    ///
159    /// The closing commission truncates its exact proportional share toward zero in native Money
160    /// units. The opening commission receives the remainder, conserving fees and rebates exactly.
161    ///
162    /// # Errors
163    ///
164    /// Returns an error if `closing_qty` is zero, is not smaller than the fill quantity, or the
165    /// proportional commission cannot be represented.
166    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}