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 nautilus_core::{UUID4, UnixNanos};
19use rust_decimal::Decimal;
20use serde::{Deserialize, Serialize};
21use ustr::Ustr;
22
23use crate::{
24    enums::{
25        ContingencyType, LiquiditySide, OrderSide, OrderSideSpecified, OrderType, TimeInForce,
26        TrailingOffsetType, TriggerType,
27    },
28    events::OrderEvent,
29    identifiers::{
30        AccountId, ClientOrderId, ExecAlgorithmId, InstrumentId, OrderListId, PositionId,
31        StrategyId, TradeId, TraderId, VenueOrderId,
32    },
33    types::{Currency, Money, Price, Quantity},
34};
35
36#[repr(C)]
37#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(tag = "type")]
39#[cfg_attr(
40    feature = "python",
41    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
42)]
43#[cfg_attr(
44    feature = "python",
45    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
46)]
47pub struct OrderFilled {
48    /// The trader ID associated with the event.
49    pub trader_id: TraderId,
50    /// The strategy ID associated with the event.
51    pub strategy_id: StrategyId,
52    /// The instrument ID associated with the event.
53    pub instrument_id: InstrumentId,
54    /// The client order ID associated with the event.
55    pub client_order_id: ClientOrderId,
56    pub venue_order_id: VenueOrderId,
57    /// The account ID associated with the event.
58    pub account_id: AccountId,
59    /// The trade match ID (assigned by the venue).
60    pub trade_id: TradeId,
61    /// The order side.
62    pub order_side: OrderSide,
63    /// The order type.
64    pub order_type: OrderType,
65    /// The fill quantity for this execution.
66    pub last_qty: Quantity,
67    /// The fill price for this execution.
68    pub last_px: Price,
69    /// The currency of the `last_px`.
70    pub currency: Currency,
71    /// The liquidity side of the execution.
72    pub liquidity_side: LiquiditySide,
73    /// The unique identifier for the event.
74    pub event_id: UUID4,
75    /// UNIX timestamp (nanoseconds) when the event occurred.
76    pub ts_event: UnixNanos,
77    /// UNIX timestamp (nanoseconds) when the event was initialized.
78    pub ts_init: UnixNanos,
79    /// If the event was generated during reconciliation.
80    pub reconciliation: bool,
81    /// The position ID (assigned by the venue).
82    pub position_id: Option<PositionId>,
83    /// The commission generated from this execution.
84    pub commission: Option<Money>,
85    /// The causation ID associated with the event.
86    #[serde(default, skip_serializing_if = "Option::is_none")]
87    pub causation_id: Option<UUID4>,
88}
89
90impl OrderFilled {
91    /// Creates a new [`OrderFilled`] instance.
92    #[expect(clippy::too_many_arguments)]
93    #[must_use]
94    pub fn new(
95        trader_id: TraderId,
96        strategy_id: StrategyId,
97        instrument_id: InstrumentId,
98        client_order_id: ClientOrderId,
99        venue_order_id: VenueOrderId,
100        account_id: AccountId,
101        trade_id: TradeId,
102        order_side: OrderSide,
103        order_type: OrderType,
104        last_qty: Quantity,
105        last_px: Price,
106        currency: Currency,
107        liquidity_side: LiquiditySide,
108        event_id: UUID4,
109        ts_event: UnixNanos,
110        ts_init: UnixNanos,
111        reconciliation: bool,
112        position_id: Option<PositionId>,
113        commission: Option<Money>,
114    ) -> Self {
115        Self {
116            trader_id,
117            strategy_id,
118            instrument_id,
119            client_order_id,
120            venue_order_id,
121            account_id,
122            trade_id,
123            order_side,
124            order_type,
125            last_qty,
126            last_px,
127            currency,
128            liquidity_side,
129            event_id,
130            ts_event,
131            ts_init,
132            reconciliation,
133            position_id,
134            commission,
135            causation_id: None,
136        }
137    }
138
139    #[must_use]
140    pub fn specified_side(&self) -> OrderSideSpecified {
141        self.order_side.as_specified()
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
155impl Debug for OrderFilled {
156    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157        let position_id_str = match self.position_id {
158            Some(position_id) => position_id.to_string(),
159            None => "None".to_string(),
160        };
161        let commission_str = match self.commission {
162            Some(commission) => commission.to_string(),
163            None => "None".to_string(),
164        };
165        write!(
166            f,
167            "{}(\
168            trader_id={}, \
169            strategy_id={}, \
170            instrument_id={}, \
171            client_order_id={}, \
172            venue_order_id={}, \
173            account_id={}, \
174            trade_id={}, \
175            position_id={}, \
176            order_side={}, \
177            order_type={}, \
178            last_qty={}, \
179            last_px={} {}, \
180            commission={}, \
181            liquidity_side={}, \
182            event_id={}, \
183            ts_event={}, \
184            ts_init={})",
185            stringify!(OrderFilled),
186            self.trader_id,
187            self.strategy_id,
188            self.instrument_id,
189            self.client_order_id,
190            self.venue_order_id,
191            self.account_id,
192            self.trade_id,
193            position_id_str,
194            self.order_side,
195            self.order_type,
196            self.last_qty.to_formatted_string(),
197            self.last_px.to_formatted_string(),
198            self.currency,
199            commission_str,
200            self.liquidity_side,
201            self.event_id,
202            self.ts_event,
203            self.ts_init
204        )
205    }
206}
207
208impl Display for OrderFilled {
209    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210        write!(
211            f,
212            "{}(\
213            instrument_id={}, \
214            client_order_id={}, \
215            venue_order_id={}, \
216            account_id={}, \
217            trade_id={}, \
218            position_id={}, \
219            order_side={}, \
220            order_type={}, \
221            last_qty={}, \
222            last_px={} {}, \
223            commission={}, \
224            liquidity_side={}, \
225            ts_event={})",
226            stringify!(OrderFilled),
227            self.instrument_id,
228            self.client_order_id,
229            self.venue_order_id,
230            self.account_id,
231            self.trade_id,
232            self.position_id
233                .map_or("None".to_string(), |id| id.to_string()),
234            self.order_side,
235            self.order_type,
236            self.last_qty.to_formatted_string(),
237            self.last_px.to_formatted_string(),
238            self.currency,
239            self.commission.unwrap_or(Money::from("0.0 USD")),
240            self.liquidity_side,
241            self.ts_event
242        )
243    }
244}
245
246impl OrderEvent for OrderFilled {
247    fn id(&self) -> UUID4 {
248        self.event_id
249    }
250
251    fn type_name(&self) -> &'static str {
252        stringify!(OrderFilled)
253    }
254
255    fn order_type(&self) -> Option<OrderType> {
256        Some(self.order_type)
257    }
258
259    fn order_side(&self) -> Option<OrderSide> {
260        Some(self.order_side)
261    }
262
263    fn trader_id(&self) -> TraderId {
264        self.trader_id
265    }
266
267    fn strategy_id(&self) -> StrategyId {
268        self.strategy_id
269    }
270
271    fn instrument_id(&self) -> InstrumentId {
272        self.instrument_id
273    }
274
275    fn trade_id(&self) -> Option<TradeId> {
276        Some(self.trade_id)
277    }
278
279    fn currency(&self) -> Option<Currency> {
280        Some(self.currency)
281    }
282
283    fn client_order_id(&self) -> ClientOrderId {
284        self.client_order_id
285    }
286
287    fn reason(&self) -> Option<Ustr> {
288        None
289    }
290
291    fn quantity(&self) -> Option<Quantity> {
292        Some(self.last_qty)
293    }
294
295    fn time_in_force(&self) -> Option<TimeInForce> {
296        None
297    }
298
299    fn liquidity_side(&self) -> Option<LiquiditySide> {
300        Some(self.liquidity_side)
301    }
302
303    fn post_only(&self) -> Option<bool> {
304        None
305    }
306
307    fn reduce_only(&self) -> Option<bool> {
308        None
309    }
310
311    fn quote_quantity(&self) -> Option<bool> {
312        None
313    }
314
315    fn reconciliation(&self) -> bool {
316        self.reconciliation
317    }
318
319    fn price(&self) -> Option<Price> {
320        None
321    }
322
323    fn last_px(&self) -> Option<Price> {
324        Some(self.last_px)
325    }
326
327    fn last_qty(&self) -> Option<Quantity> {
328        Some(self.last_qty)
329    }
330
331    fn trigger_price(&self) -> Option<Price> {
332        None
333    }
334
335    fn trigger_type(&self) -> Option<TriggerType> {
336        None
337    }
338
339    fn limit_offset(&self) -> Option<Decimal> {
340        None
341    }
342
343    fn trailing_offset(&self) -> Option<Decimal> {
344        None
345    }
346
347    fn trailing_offset_type(&self) -> Option<TrailingOffsetType> {
348        None
349    }
350
351    fn expire_time(&self) -> Option<UnixNanos> {
352        None
353    }
354
355    fn display_qty(&self) -> Option<Quantity> {
356        None
357    }
358
359    fn emulation_trigger(&self) -> Option<TriggerType> {
360        None
361    }
362
363    fn trigger_instrument_id(&self) -> Option<InstrumentId> {
364        None
365    }
366
367    fn contingency_type(&self) -> Option<ContingencyType> {
368        None
369    }
370
371    fn order_list_id(&self) -> Option<OrderListId> {
372        None
373    }
374
375    fn linked_order_ids(&self) -> Option<Vec<ClientOrderId>> {
376        None
377    }
378
379    fn parent_order_id(&self) -> Option<ClientOrderId> {
380        None
381    }
382
383    fn exec_algorithm_id(&self) -> Option<ExecAlgorithmId> {
384        None
385    }
386
387    fn exec_spawn_id(&self) -> Option<ClientOrderId> {
388        None
389    }
390
391    fn venue_order_id(&self) -> Option<VenueOrderId> {
392        Some(self.venue_order_id)
393    }
394
395    fn account_id(&self) -> Option<AccountId> {
396        Some(self.account_id)
397    }
398
399    fn position_id(&self) -> Option<PositionId> {
400        self.position_id
401    }
402
403    fn commission(&self) -> Option<Money> {
404        self.commission
405    }
406
407    fn ts_event(&self) -> UnixNanos {
408        self.ts_event
409    }
410
411    fn ts_init(&self) -> UnixNanos {
412        self.ts_init
413    }
414}
415
416#[cfg(test)]
417mod tests {
418    use nautilus_core::UnixNanos;
419    use rstest::rstest;
420
421    use super::*;
422    use crate::{
423        enums::{OrderSide, OrderSideSpecified},
424        events::order::stubs::*,
425        identifiers::PositionId,
426        types::{Currency, Money, Price, Quantity},
427    };
428
429    fn create_test_order_filled() -> OrderFilled {
430        OrderFilled::new(
431            TraderId::from("TRADER-001"),
432            StrategyId::from("EMA-CROSS"),
433            InstrumentId::from("EURUSD.SIM"),
434            ClientOrderId::from("O-19700101-000000-001-001-1"),
435            VenueOrderId::from("V-001"),
436            AccountId::from("SIM-001"),
437            TradeId::from("T-001"),
438            OrderSide::Buy,
439            OrderType::Market,
440            Quantity::from("100"),
441            Price::from("1.0500"),
442            Currency::USD(),
443            LiquiditySide::Taker,
444            UUID4::default(),
445            UnixNanos::from(1_000_000_000),
446            UnixNanos::from(2_000_000_000),
447            false,
448            Some(PositionId::from("P-001")),
449            Some(Money::new(2.5, Currency::USD())),
450        )
451    }
452
453    #[rstest]
454    fn test_order_filled_display(order_filled: OrderFilled) {
455        let display = format!("{order_filled}");
456        assert_eq!(
457            display,
458            "OrderFilled(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-000000-001-001-1, \
459            venue_order_id=123456, account_id=SIM-001, trade_id=1, position_id=None, \
460            order_side=BUY, order_type=LIMIT, last_qty=0.561, last_px=22_000 USDT, \
461            commission=12.20000000 USDT, liquidity_side=TAKER, ts_event=0)"
462        );
463    }
464
465    #[rstest]
466    fn test_order_filled_is_buy(order_filled: OrderFilled) {
467        assert!(order_filled.is_buy());
468        assert!(!order_filled.is_sell());
469    }
470
471    #[rstest]
472    fn test_order_filled_is_sell() {
473        let mut order_filled = create_test_order_filled();
474        order_filled.order_side = OrderSide::Sell;
475
476        assert!(order_filled.is_sell());
477        assert!(!order_filled.is_buy());
478    }
479
480    #[rstest]
481    fn test_order_filled_specified_side() {
482        let buy_order = create_test_order_filled();
483        assert_eq!(buy_order.specified_side(), OrderSideSpecified::Buy);
484
485        let mut sell_order = create_test_order_filled();
486        sell_order.order_side = OrderSide::Sell;
487        assert_eq!(sell_order.specified_side(), OrderSideSpecified::Sell);
488    }
489
490    #[rstest]
491    fn test_order_filled_without_position_id_display() {
492        let mut order_filled = create_test_order_filled();
493        order_filled.position_id = None;
494
495        let display = format!("{order_filled}");
496        assert!(display.contains("position_id=None"));
497        assert!(!display.contains("position_id=P-001"));
498    }
499
500    #[rstest]
501    fn test_order_filled_without_commission_serialization() {
502        let mut order_filled = create_test_order_filled();
503        order_filled.commission = None;
504
505        let json = serde_json::to_string(&order_filled).unwrap();
506        let deserialized: OrderFilled = serde_json::from_str(&json).unwrap();
507
508        assert_eq!(deserialized.commission, None);
509        assert_eq!(deserialized.trade_id, order_filled.trade_id);
510    }
511
512    #[rstest]
513    fn test_order_filled_serialization() {
514        let original = create_test_order_filled();
515
516        let json = serde_json::to_string(&original).unwrap();
517        let deserialized: OrderFilled = serde_json::from_str(&json).unwrap();
518
519        assert_eq!(original, deserialized);
520    }
521
522    #[rstest]
523    fn test_order_filled_serialization_with_causation_id() {
524        let causation_id = UUID4::new();
525        let original = OrderFilled {
526            causation_id: Some(causation_id),
527            ..create_test_order_filled()
528        };
529
530        let json = serde_json::to_string(&original).unwrap();
531        let deserialized: OrderFilled = serde_json::from_str(&json).unwrap();
532
533        assert!(json.contains("\"causation_id\""));
534        assert_eq!(deserialized.causation_id, Some(causation_id));
535        assert_eq!(original, deserialized);
536    }
537}