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