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