Skip to main content

nautilus_model/events/order/
mod.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 indexmap::IndexMap;
17use nautilus_core::{UUID4, UnixNanos};
18use rust_decimal::Decimal;
19use ustr::Ustr;
20
21use crate::{
22    enums::{
23        ContingencyType, LiquiditySide, OrderSide, OrderType, TimeInForce, TrailingOffsetType,
24        TriggerType,
25    },
26    identifiers::{
27        AccountId, ClientOrderId, ExecAlgorithmId, InstrumentId, OrderListId, PositionId,
28        StrategyId, TradeId, TraderId, VenueOrderId,
29    },
30    types::{Currency, Money, Price, Quantity},
31};
32
33pub mod accepted;
34pub mod accepted_batch;
35pub mod any;
36pub mod cancel_rejected;
37pub mod canceled;
38pub mod canceled_batch;
39pub mod denied;
40pub mod denied_reason;
41pub mod emulated;
42pub mod expired;
43pub mod fill_voided;
44pub mod filled;
45pub mod initialized;
46pub mod modify_rejected;
47pub mod pending_cancel;
48pub mod pending_update;
49pub mod rejected;
50pub mod released;
51pub mod snapshot;
52pub mod submitted;
53pub mod submitted_batch;
54pub mod triggered;
55pub mod updated;
56
57#[cfg(any(test, feature = "test-support"))]
58pub mod spec;
59#[cfg(any(test, feature = "test-support"))]
60pub mod stubs;
61
62/// Represents a type of [`OrderEvent`].
63#[derive(Debug, PartialEq, Eq)]
64pub enum OrderEventType {
65    Initialized,
66    Denied,
67    Emulated,
68    Released,
69    Submitted,
70    Accepted,
71    Rejected,
72    Canceled,
73    Expired,
74    Triggered,
75    PendingUpdate,
76    PendingCancel,
77    ModifyRejected,
78    CancelRejected,
79    Updated,
80    PartiallyFilled,
81    Filled,
82    FillVoided,
83}
84
85pub trait OrderEvent: 'static + Send {
86    fn id(&self) -> UUID4;
87    fn type_name(&self) -> &'static str;
88    fn order_type(&self) -> Option<OrderType>;
89    fn order_side(&self) -> Option<OrderSide>;
90    fn trader_id(&self) -> TraderId;
91    fn strategy_id(&self) -> StrategyId;
92    fn instrument_id(&self) -> InstrumentId;
93    fn trade_id(&self) -> Option<TradeId>;
94    fn currency(&self) -> Option<Currency>;
95    fn client_order_id(&self) -> ClientOrderId;
96    fn reason(&self) -> Option<Ustr>;
97    fn quantity(&self) -> Option<Quantity>;
98    fn time_in_force(&self) -> Option<TimeInForce>;
99    fn liquidity_side(&self) -> Option<LiquiditySide>;
100    fn post_only(&self) -> Option<bool>;
101    fn reduce_only(&self) -> Option<bool>;
102    fn quote_quantity(&self) -> Option<bool>;
103    fn reconciliation(&self) -> bool;
104    fn price(&self) -> Option<Price>;
105    fn last_px(&self) -> Option<Price>;
106    fn last_qty(&self) -> Option<Quantity>;
107    fn activation_price(&self) -> Option<Price>;
108    fn trigger_price(&self) -> Option<Price>;
109    fn trigger_type(&self) -> Option<TriggerType>;
110    fn limit_offset(&self) -> Option<Decimal>;
111    fn trailing_offset(&self) -> Option<Decimal>;
112    fn trailing_offset_type(&self) -> Option<TrailingOffsetType>;
113    fn expire_time(&self) -> Option<UnixNanos>;
114    fn display_qty(&self) -> Option<Quantity>;
115    fn emulation_trigger(&self) -> Option<TriggerType>;
116    fn trigger_instrument_id(&self) -> Option<InstrumentId>;
117    fn contingency_type(&self) -> Option<ContingencyType>;
118    fn order_list_id(&self) -> Option<OrderListId>;
119    fn linked_order_ids(&self) -> Option<Vec<ClientOrderId>>;
120    fn parent_order_id(&self) -> Option<ClientOrderId>;
121    fn exec_algorithm_id(&self) -> Option<ExecAlgorithmId>;
122    fn exec_algorithm_params(&self) -> Option<IndexMap<Ustr, Ustr>> {
123        None
124    }
125    fn exec_spawn_id(&self) -> Option<ClientOrderId>;
126    fn tags(&self) -> Option<Vec<Ustr>> {
127        None
128    }
129    fn venue_order_id(&self) -> Option<VenueOrderId>;
130    fn account_id(&self) -> Option<AccountId>;
131    fn position_id(&self) -> Option<PositionId>;
132    fn commission(&self) -> Option<Money>;
133    fn ts_event(&self) -> UnixNanos;
134    fn ts_init(&self) -> UnixNanos;
135    fn causation_id(&self) -> Option<UUID4> {
136        None
137    }
138    fn released_price(&self) -> Option<Price> {
139        None
140    }
141    fn protection_price(&self) -> Option<Price> {
142        None
143    }
144    fn due_post_only(&self) -> bool {
145        false
146    }
147    fn correction_id(&self) -> Option<Ustr> {
148        None
149    }
150    fn is_reopened(&self) -> bool {
151        false
152    }
153    fn info(&self) -> Option<IndexMap<Ustr, Ustr>> {
154        None
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use indexmap::IndexMap;
161    use rstest::rstest;
162    use rust_decimal_macros::dec;
163
164    use super::*;
165    use crate::events::order::{any::OrderEventAny, spec::*};
166
167    /// Returns the sorted names of every `Option` accessor that yields `Some` for `event`.
168    fn optional_fields_present(event: &dyn OrderEvent) -> Vec<&'static str> {
169        let mut present: Vec<&'static str> = [
170            ("activation_price", event.activation_price().is_some()),
171            ("account_id", event.account_id().is_some()),
172            ("causation_id", event.causation_id().is_some()),
173            ("commission", event.commission().is_some()),
174            ("contingency_type", event.contingency_type().is_some()),
175            ("correction_id", event.correction_id().is_some()),
176            ("currency", event.currency().is_some()),
177            ("display_qty", event.display_qty().is_some()),
178            ("emulation_trigger", event.emulation_trigger().is_some()),
179            ("exec_algorithm_id", event.exec_algorithm_id().is_some()),
180            (
181                "exec_algorithm_params",
182                event.exec_algorithm_params().is_some(),
183            ),
184            ("exec_spawn_id", event.exec_spawn_id().is_some()),
185            ("expire_time", event.expire_time().is_some()),
186            ("info", event.info().is_some()),
187            ("last_px", event.last_px().is_some()),
188            ("last_qty", event.last_qty().is_some()),
189            ("limit_offset", event.limit_offset().is_some()),
190            ("linked_order_ids", event.linked_order_ids().is_some()),
191            ("liquidity_side", event.liquidity_side().is_some()),
192            ("order_list_id", event.order_list_id().is_some()),
193            ("order_side", event.order_side().is_some()),
194            ("order_type", event.order_type().is_some()),
195            ("parent_order_id", event.parent_order_id().is_some()),
196            ("position_id", event.position_id().is_some()),
197            ("post_only", event.post_only().is_some()),
198            ("price", event.price().is_some()),
199            ("protection_price", event.protection_price().is_some()),
200            ("quantity", event.quantity().is_some()),
201            ("quote_quantity", event.quote_quantity().is_some()),
202            ("reason", event.reason().is_some()),
203            ("reduce_only", event.reduce_only().is_some()),
204            ("released_price", event.released_price().is_some()),
205            ("tags", event.tags().is_some()),
206            ("time_in_force", event.time_in_force().is_some()),
207            ("trade_id", event.trade_id().is_some()),
208            ("trailing_offset", event.trailing_offset().is_some()),
209            (
210                "trailing_offset_type",
211                event.trailing_offset_type().is_some(),
212            ),
213            (
214                "trigger_instrument_id",
215                event.trigger_instrument_id().is_some(),
216            ),
217            ("trigger_price", event.trigger_price().is_some()),
218            ("trigger_type", event.trigger_type().is_some()),
219            ("venue_order_id", event.venue_order_id().is_some()),
220        ]
221        .into_iter()
222        .filter(|(_, present)| *present)
223        .map(|(name, _)| name)
224        .collect();
225        present.sort_unstable();
226        present
227    }
228
229    fn params() -> IndexMap<Ustr, Ustr> {
230        IndexMap::from([(Ustr::from("k"), Ustr::from("v"))])
231    }
232
233    fn fully_populated_events() -> Vec<(OrderEventAny, Vec<&'static str>)> {
234        let venue_order_id = VenueOrderId::from("V-1");
235        let account_id = AccountId::from("SIM-001");
236        let reason = Ustr::from("REASON");
237        let px = Price::from("1.00");
238
239        vec![
240            (
241                OrderEventAny::Initialized(
242                    OrderInitializedSpec::builder()
243                        .price(px)
244                        .activation_price(px)
245                        .trigger_price(px)
246                        .trigger_type(TriggerType::LastPrice)
247                        .limit_offset(dec!(0.5))
248                        .trailing_offset(dec!(0.5))
249                        .trailing_offset_type(TrailingOffsetType::Price)
250                        .expire_time(UnixNanos::from(9))
251                        .display_qty(Quantity::from(1))
252                        .emulation_trigger(TriggerType::BidAsk)
253                        .trigger_instrument_id(InstrumentId::from("AUD/USD.SIM"))
254                        .contingency_type(ContingencyType::Oto)
255                        .order_list_id(OrderListId::from("OL-1"))
256                        .linked_order_ids(vec![ClientOrderId::from("O-2")])
257                        .parent_order_id(ClientOrderId::from("O-3"))
258                        .exec_algorithm_id(ExecAlgorithmId::from("TWAP"))
259                        .exec_algorithm_params(params())
260                        .exec_spawn_id(ClientOrderId::from("O-4"))
261                        .tags(vec![Ustr::from("TAG")])
262                        .build(),
263                ),
264                vec![
265                    "activation_price",
266                    "contingency_type",
267                    "display_qty",
268                    "emulation_trigger",
269                    "exec_algorithm_id",
270                    "exec_algorithm_params",
271                    "exec_spawn_id",
272                    "expire_time",
273                    "limit_offset",
274                    "linked_order_ids",
275                    "order_list_id",
276                    "order_side",
277                    "order_type",
278                    "parent_order_id",
279                    "post_only",
280                    "price",
281                    "quantity",
282                    "quote_quantity",
283                    "reduce_only",
284                    "tags",
285                    "time_in_force",
286                    "trailing_offset",
287                    "trailing_offset_type",
288                    "trigger_instrument_id",
289                    "trigger_price",
290                    "trigger_type",
291                ],
292            ),
293            (
294                OrderEventAny::Denied(OrderDeniedSpec::builder().build()),
295                vec!["reason"],
296            ),
297            (
298                OrderEventAny::Emulated(OrderEmulatedSpec::builder().build()),
299                vec![],
300            ),
301            (
302                OrderEventAny::Released(OrderReleasedSpec::builder().build()),
303                vec!["released_price"],
304            ),
305            (
306                OrderEventAny::Submitted(OrderSubmittedSpec::builder().build()),
307                vec!["account_id"],
308            ),
309            (
310                OrderEventAny::Accepted(OrderAcceptedSpec::builder().build()),
311                vec!["account_id", "venue_order_id"],
312            ),
313            (
314                OrderEventAny::Rejected(OrderRejectedSpec::builder().build()),
315                vec!["account_id", "reason"],
316            ),
317            (
318                OrderEventAny::Canceled(
319                    OrderCanceledSpec::builder()
320                        .venue_order_id(venue_order_id)
321                        .account_id(account_id)
322                        .reason(reason)
323                        .build(),
324                ),
325                vec!["account_id", "reason", "venue_order_id"],
326            ),
327            (
328                OrderEventAny::Expired(
329                    OrderExpiredSpec::builder()
330                        .venue_order_id(venue_order_id)
331                        .account_id(account_id)
332                        .build(),
333                ),
334                vec!["account_id", "venue_order_id"],
335            ),
336            (
337                OrderEventAny::Triggered(
338                    OrderTriggeredSpec::builder()
339                        .venue_order_id(venue_order_id)
340                        .account_id(account_id)
341                        .build(),
342                ),
343                vec!["account_id", "venue_order_id"],
344            ),
345            (
346                OrderEventAny::PendingUpdate(
347                    OrderPendingUpdateSpec::builder()
348                        .venue_order_id(venue_order_id)
349                        .account_id(account_id)
350                        .build(),
351                ),
352                vec!["account_id", "venue_order_id"],
353            ),
354            (
355                OrderEventAny::PendingCancel(
356                    OrderPendingCancelSpec::builder()
357                        .venue_order_id(venue_order_id)
358                        .account_id(account_id)
359                        .build(),
360                ),
361                vec!["account_id", "venue_order_id"],
362            ),
363            (
364                OrderEventAny::ModifyRejected(
365                    OrderModifyRejectedSpec::builder()
366                        .venue_order_id(venue_order_id)
367                        .account_id(account_id)
368                        .build(),
369                ),
370                vec!["account_id", "reason", "venue_order_id"],
371            ),
372            (
373                OrderEventAny::CancelRejected(
374                    OrderCancelRejectedSpec::builder()
375                        .venue_order_id(venue_order_id)
376                        .account_id(account_id)
377                        .build(),
378                ),
379                vec!["account_id", "reason", "venue_order_id"],
380            ),
381            (
382                OrderEventAny::Updated(
383                    OrderUpdatedSpec::builder()
384                        .venue_order_id(venue_order_id)
385                        .account_id(account_id)
386                        .price(px)
387                        .trigger_price(px)
388                        .protection_price(px)
389                        .build(),
390                ),
391                vec![
392                    "account_id",
393                    "price",
394                    "protection_price",
395                    "quantity",
396                    "quote_quantity",
397                    "trigger_price",
398                    "venue_order_id",
399                ],
400            ),
401            (
402                OrderEventAny::Filled(
403                    OrderFilledSpec::builder()
404                        .position_id(PositionId::from("P-1"))
405                        .commission(Money::from("1.00 USD"))
406                        .info(params())
407                        .build(),
408                ),
409                vec![
410                    "account_id",
411                    "commission",
412                    "currency",
413                    "info",
414                    "last_px",
415                    "last_qty",
416                    "liquidity_side",
417                    "order_side",
418                    "order_type",
419                    "position_id",
420                    "quantity",
421                    "trade_id",
422                    "venue_order_id",
423                ],
424            ),
425            (
426                OrderEventAny::FillVoided(
427                    OrderFillVoidedSpec::builder()
428                        .commission_voided(Money::from("1.00 USD"))
429                        .position_id(PositionId::from("P-1"))
430                        .reason(reason)
431                        .info(params())
432                        .build(),
433                ),
434                vec![
435                    "account_id",
436                    "commission",
437                    "correction_id",
438                    "currency",
439                    "info",
440                    "last_px",
441                    "last_qty",
442                    "liquidity_side",
443                    "order_side",
444                    "order_type",
445                    "position_id",
446                    "quantity",
447                    "reason",
448                    "trade_id",
449                    "venue_order_id",
450                ],
451            ),
452        ]
453    }
454
455    #[rstest]
456    fn test_optional_field_surface_matches_event_payload() {
457        for (event, expected) in fully_populated_events() {
458            let event_type = event.event_type();
459            let boxed = event.into_boxed();
460
461            assert_eq!(
462                optional_fields_present(boxed.as_ref()),
463                expected,
464                "optional accessor surface changed for {event_type:?}"
465            );
466        }
467    }
468
469    #[rstest]
470    fn test_initialized_accessors_map_distinct_fields() {
471        let event = OrderInitializedSpec::builder()
472            .quantity(Quantity::from(100))
473            .price(Price::from("1.00"))
474            .activation_price(Price::from("2.00"))
475            .trigger_price(Price::from("3.00"))
476            .trigger_type(TriggerType::LastPrice)
477            .emulation_trigger(TriggerType::BidAsk)
478            .limit_offset(dec!(0.5))
479            .trailing_offset(dec!(1.5))
480            .display_qty(Quantity::from(10))
481            .parent_order_id(ClientOrderId::from("O-PARENT"))
482            .exec_spawn_id(ClientOrderId::from("O-SPAWN"))
483            .ts_event(UnixNanos::from(7))
484            .ts_init(UnixNanos::from(8))
485            .build();
486
487        assert_eq!(event.price(), Some(Price::from("1.00")));
488        assert_eq!(event.activation_price(), Some(Price::from("2.00")));
489        assert_eq!(event.trigger_price(), Some(Price::from("3.00")));
490        assert_eq!(event.trigger_type(), Some(TriggerType::LastPrice));
491        assert_eq!(event.emulation_trigger(), Some(TriggerType::BidAsk));
492        assert_eq!(event.limit_offset(), Some(dec!(0.5)));
493        assert_eq!(event.trailing_offset(), Some(dec!(1.5)));
494        assert_eq!(event.quantity(), Some(Quantity::from(100)));
495        assert_eq!(event.display_qty(), Some(Quantity::from(10)));
496        assert_eq!(
497            event.parent_order_id(),
498            Some(ClientOrderId::from("O-PARENT"))
499        );
500        assert_eq!(event.exec_spawn_id(), Some(ClientOrderId::from("O-SPAWN")));
501        assert_eq!(event.ts_event(), UnixNanos::from(7));
502        assert_eq!(event.ts_init(), UnixNanos::from(8));
503    }
504
505    #[rstest]
506    fn test_filled_accessors_map_distinct_fields() {
507        let event = OrderFilledSpec::builder()
508            .last_qty(Quantity::from(25))
509            .last_px(Price::from("4.00"))
510            .commission(Money::from("2.00 USD"))
511            .ts_event(UnixNanos::from(7))
512            .ts_init(UnixNanos::from(8))
513            .build();
514
515        assert_eq!(event.last_px(), Some(Price::from("4.00")));
516        assert_eq!(event.price(), None);
517        assert_eq!(event.last_qty(), Some(Quantity::from(25)));
518        assert_eq!(event.quantity(), Some(Quantity::from(25)));
519        assert_eq!(event.commission(), Some(Money::from("2.00 USD")));
520        assert_eq!(event.ts_event(), UnixNanos::from(7));
521        assert_eq!(event.ts_init(), UnixNanos::from(8));
522    }
523
524    #[rstest]
525    fn test_updated_accessors_map_distinct_prices() {
526        let event = OrderUpdatedSpec::builder()
527            .quantity(Quantity::from(50))
528            .price(Price::from("1.00"))
529            .trigger_price(Price::from("2.00"))
530            .protection_price(Price::from("3.00"))
531            .ts_event(UnixNanos::from(7))
532            .ts_init(UnixNanos::from(8))
533            .build();
534
535        assert_eq!(event.price(), Some(Price::from("1.00")));
536        assert_eq!(event.trigger_price(), Some(Price::from("2.00")));
537        assert_eq!(event.protection_price(), Some(Price::from("3.00")));
538        assert_eq!(event.quantity(), Some(Quantity::from(50)));
539        assert_eq!(event.ts_event(), UnixNanos::from(7));
540        assert_eq!(event.ts_init(), UnixNanos::from(8));
541    }
542
543    #[rstest]
544    fn test_type_name_matches_variant() {
545        let expected = [
546            "OrderInitialized",
547            "OrderDenied",
548            "OrderEmulated",
549            "OrderReleased",
550            "OrderSubmitted",
551            "OrderAccepted",
552            "OrderRejected",
553            "OrderCanceled",
554            "OrderExpired",
555            "OrderTriggered",
556            "OrderPendingUpdate",
557            "OrderPendingCancel",
558            "OrderModifyRejected",
559            "OrderCancelRejected",
560            "OrderUpdated",
561            "OrderFilled",
562            "OrderFillVoided",
563        ];
564
565        let names: Vec<&'static str> = fully_populated_events()
566            .into_iter()
567            .map(|(event, _)| event.into_boxed().type_name())
568            .collect();
569
570        assert_eq!(names, expected);
571    }
572}