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}
86
87impl OrderFilled {
88    /// Creates a new [`OrderFilled`] instance.
89    #[expect(clippy::too_many_arguments)]
90    #[must_use]
91    pub fn new(
92        trader_id: TraderId,
93        strategy_id: StrategyId,
94        instrument_id: InstrumentId,
95        client_order_id: ClientOrderId,
96        venue_order_id: VenueOrderId,
97        account_id: AccountId,
98        trade_id: TradeId,
99        order_side: OrderSide,
100        order_type: OrderType,
101        last_qty: Quantity,
102        last_px: Price,
103        currency: Currency,
104        liquidity_side: LiquiditySide,
105        event_id: UUID4,
106        ts_event: UnixNanos,
107        ts_init: UnixNanos,
108        reconciliation: bool,
109        position_id: Option<PositionId>,
110        commission: Option<Money>,
111    ) -> Self {
112        Self {
113            trader_id,
114            strategy_id,
115            instrument_id,
116            client_order_id,
117            venue_order_id,
118            account_id,
119            trade_id,
120            order_side,
121            order_type,
122            last_qty,
123            last_px,
124            currency,
125            liquidity_side,
126            event_id,
127            ts_event,
128            ts_init,
129            reconciliation,
130            position_id,
131            commission,
132        }
133    }
134
135    #[must_use]
136    pub fn specified_side(&self) -> OrderSideSpecified {
137        self.order_side.as_specified()
138    }
139
140    #[must_use]
141    pub fn is_buy(&self) -> bool {
142        self.order_side == OrderSide::Buy
143    }
144
145    #[must_use]
146    pub fn is_sell(&self) -> bool {
147        self.order_side == OrderSide::Sell
148    }
149}
150
151impl Debug for OrderFilled {
152    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153        let position_id_str = match self.position_id {
154            Some(position_id) => position_id.to_string(),
155            None => "None".to_string(),
156        };
157        let commission_str = match self.commission {
158            Some(commission) => commission.to_string(),
159            None => "None".to_string(),
160        };
161        write!(
162            f,
163            "{}(\
164            trader_id={}, \
165            strategy_id={}, \
166            instrument_id={}, \
167            client_order_id={}, \
168            venue_order_id={}, \
169            account_id={}, \
170            trade_id={}, \
171            position_id={}, \
172            order_side={}, \
173            order_type={}, \
174            last_qty={}, \
175            last_px={} {}, \
176            commission={}, \
177            liquidity_side={}, \
178            event_id={}, \
179            ts_event={}, \
180            ts_init={})",
181            stringify!(OrderFilled),
182            self.trader_id,
183            self.strategy_id,
184            self.instrument_id,
185            self.client_order_id,
186            self.venue_order_id,
187            self.account_id,
188            self.trade_id,
189            position_id_str,
190            self.order_side,
191            self.order_type,
192            self.last_qty.to_formatted_string(),
193            self.last_px.to_formatted_string(),
194            self.currency,
195            commission_str,
196            self.liquidity_side,
197            self.event_id,
198            self.ts_event,
199            self.ts_init
200        )
201    }
202}
203
204impl Display for OrderFilled {
205    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206        write!(
207            f,
208            "{}(\
209            instrument_id={}, \
210            client_order_id={}, \
211            venue_order_id={}, \
212            account_id={}, \
213            trade_id={}, \
214            position_id={}, \
215            order_side={}, \
216            order_type={}, \
217            last_qty={}, \
218            last_px={} {}, \
219            commission={}, \
220            liquidity_side={}, \
221            ts_event={})",
222            stringify!(OrderFilled),
223            self.instrument_id,
224            self.client_order_id,
225            self.venue_order_id,
226            self.account_id,
227            self.trade_id,
228            self.position_id
229                .map_or("None".to_string(), |id| id.to_string()),
230            self.order_side,
231            self.order_type,
232            self.last_qty.to_formatted_string(),
233            self.last_px.to_formatted_string(),
234            self.currency,
235            self.commission.unwrap_or(Money::from("0.0 USD")),
236            self.liquidity_side,
237            self.ts_event
238        )
239    }
240}
241
242impl OrderEvent for OrderFilled {
243    fn id(&self) -> UUID4 {
244        self.event_id
245    }
246
247    fn type_name(&self) -> &'static str {
248        stringify!(OrderFilled)
249    }
250
251    fn order_type(&self) -> Option<OrderType> {
252        Some(self.order_type)
253    }
254
255    fn order_side(&self) -> Option<OrderSide> {
256        Some(self.order_side)
257    }
258
259    fn trader_id(&self) -> TraderId {
260        self.trader_id
261    }
262
263    fn strategy_id(&self) -> StrategyId {
264        self.strategy_id
265    }
266
267    fn instrument_id(&self) -> InstrumentId {
268        self.instrument_id
269    }
270
271    fn trade_id(&self) -> Option<TradeId> {
272        Some(self.trade_id)
273    }
274
275    fn currency(&self) -> Option<Currency> {
276        Some(self.currency)
277    }
278
279    fn client_order_id(&self) -> ClientOrderId {
280        self.client_order_id
281    }
282
283    fn reason(&self) -> Option<Ustr> {
284        None
285    }
286
287    fn quantity(&self) -> Option<Quantity> {
288        Some(self.last_qty)
289    }
290
291    fn time_in_force(&self) -> Option<TimeInForce> {
292        None
293    }
294
295    fn liquidity_side(&self) -> Option<LiquiditySide> {
296        Some(self.liquidity_side)
297    }
298
299    fn post_only(&self) -> Option<bool> {
300        None
301    }
302
303    fn reduce_only(&self) -> Option<bool> {
304        None
305    }
306
307    fn quote_quantity(&self) -> Option<bool> {
308        None
309    }
310
311    fn reconciliation(&self) -> bool {
312        self.reconciliation
313    }
314
315    fn price(&self) -> Option<Price> {
316        None
317    }
318
319    fn last_px(&self) -> Option<Price> {
320        Some(self.last_px)
321    }
322
323    fn last_qty(&self) -> Option<Quantity> {
324        Some(self.last_qty)
325    }
326
327    fn trigger_price(&self) -> Option<Price> {
328        None
329    }
330
331    fn trigger_type(&self) -> Option<TriggerType> {
332        None
333    }
334
335    fn limit_offset(&self) -> Option<Decimal> {
336        None
337    }
338
339    fn trailing_offset(&self) -> Option<Decimal> {
340        None
341    }
342
343    fn trailing_offset_type(&self) -> Option<TrailingOffsetType> {
344        None
345    }
346
347    fn expire_time(&self) -> Option<UnixNanos> {
348        None
349    }
350
351    fn display_qty(&self) -> Option<Quantity> {
352        None
353    }
354
355    fn emulation_trigger(&self) -> Option<TriggerType> {
356        None
357    }
358
359    fn trigger_instrument_id(&self) -> Option<InstrumentId> {
360        None
361    }
362
363    fn contingency_type(&self) -> Option<ContingencyType> {
364        None
365    }
366
367    fn order_list_id(&self) -> Option<OrderListId> {
368        None
369    }
370
371    fn linked_order_ids(&self) -> Option<Vec<ClientOrderId>> {
372        None
373    }
374
375    fn parent_order_id(&self) -> Option<ClientOrderId> {
376        None
377    }
378
379    fn exec_algorithm_id(&self) -> Option<ExecAlgorithmId> {
380        None
381    }
382
383    fn exec_spawn_id(&self) -> Option<ClientOrderId> {
384        None
385    }
386
387    fn venue_order_id(&self) -> Option<VenueOrderId> {
388        Some(self.venue_order_id)
389    }
390
391    fn account_id(&self) -> Option<AccountId> {
392        Some(self.account_id)
393    }
394
395    fn position_id(&self) -> Option<PositionId> {
396        self.position_id
397    }
398
399    fn commission(&self) -> Option<Money> {
400        self.commission
401    }
402
403    fn ts_event(&self) -> UnixNanos {
404        self.ts_event
405    }
406
407    fn ts_init(&self) -> UnixNanos {
408        self.ts_init
409    }
410}
411
412#[cfg(test)]
413mod tests {
414    use nautilus_core::UnixNanos;
415    use rstest::rstest;
416
417    use super::*;
418    use crate::{
419        enums::{OrderSide, OrderSideSpecified},
420        events::order::stubs::*,
421        identifiers::PositionId,
422        types::{Currency, Money, Price, Quantity},
423    };
424
425    fn create_test_order_filled() -> OrderFilled {
426        OrderFilled::new(
427            TraderId::from("TRADER-001"),
428            StrategyId::from("EMA-CROSS"),
429            InstrumentId::from("EURUSD.SIM"),
430            ClientOrderId::from("O-19700101-000000-001-001-1"),
431            VenueOrderId::from("V-001"),
432            AccountId::from("SIM-001"),
433            TradeId::from("T-001"),
434            OrderSide::Buy,
435            OrderType::Market,
436            Quantity::from("100"),
437            Price::from("1.0500"),
438            Currency::USD(),
439            LiquiditySide::Taker,
440            UUID4::default(),
441            UnixNanos::from(1_000_000_000),
442            UnixNanos::from(2_000_000_000),
443            false,
444            Some(PositionId::from("P-001")),
445            Some(Money::new(2.5, Currency::USD())),
446        )
447    }
448
449    #[rstest]
450    fn test_order_filled_display(order_filled: OrderFilled) {
451        let display = format!("{order_filled}");
452        assert_eq!(
453            display,
454            "OrderFilled(instrument_id=BTCUSDT.COINBASE, client_order_id=O-19700101-000000-001-001-1, \
455            venue_order_id=123456, account_id=SIM-001, trade_id=1, position_id=None, \
456            order_side=BUY, order_type=LIMIT, last_qty=0.561, last_px=22_000 USDT, \
457            commission=12.20000000 USDT, liquidity_side=TAKER, ts_event=0)"
458        );
459    }
460
461    #[rstest]
462    fn test_order_filled_is_buy(order_filled: OrderFilled) {
463        assert!(order_filled.is_buy());
464        assert!(!order_filled.is_sell());
465    }
466
467    #[rstest]
468    fn test_order_filled_is_sell() {
469        let mut order_filled = create_test_order_filled();
470        order_filled.order_side = OrderSide::Sell;
471
472        assert!(order_filled.is_sell());
473        assert!(!order_filled.is_buy());
474    }
475
476    #[rstest]
477    fn test_order_filled_specified_side() {
478        let buy_order = create_test_order_filled();
479        assert_eq!(buy_order.specified_side(), OrderSideSpecified::Buy);
480
481        let mut sell_order = create_test_order_filled();
482        sell_order.order_side = OrderSide::Sell;
483        assert_eq!(sell_order.specified_side(), OrderSideSpecified::Sell);
484    }
485
486    #[rstest]
487    fn test_order_filled_without_position_id_display() {
488        let mut order_filled = create_test_order_filled();
489        order_filled.position_id = None;
490
491        let display = format!("{order_filled}");
492        assert!(display.contains("position_id=None"));
493        assert!(!display.contains("position_id=P-001"));
494    }
495
496    #[rstest]
497    fn test_order_filled_without_commission_serialization() {
498        let mut order_filled = create_test_order_filled();
499        order_filled.commission = None;
500
501        let json = serde_json::to_string(&order_filled).unwrap();
502        let deserialized: OrderFilled = serde_json::from_str(&json).unwrap();
503
504        assert_eq!(deserialized.commission, None);
505        assert_eq!(deserialized.trade_id, order_filled.trade_id);
506    }
507
508    #[rstest]
509    fn test_order_filled_serialization() {
510        let original = create_test_order_filled();
511
512        let json = serde_json::to_string(&original).unwrap();
513        let deserialized: OrderFilled = serde_json::from_str(&json).unwrap();
514
515        assert_eq!(original, deserialized);
516    }
517}