Skip to main content

nautilus_common/factories/
event.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
16//! Factory for generating order and account events.
17
18use nautilus_core::{Params, UUID4, UnixNanos};
19use nautilus_model::{
20    enums::{AccountType, LiquiditySide},
21    events::{
22        AccountState, OrderAccepted, OrderCancelRejected, OrderCanceled, OrderDenied,
23        OrderEventAny, OrderExpired, OrderFilled, OrderModifyRejected, OrderRejected,
24        OrderSubmitted, OrderTriggered, OrderUpdated,
25    },
26    identifiers::{AccountId, PositionId, TradeId, TraderId, VenueOrderId},
27    orders::{Order, OrderAny},
28    types::{AccountBalance, Currency, MarginBalance, Money, Price, Quantity},
29};
30
31/// Factory for generating order and account events.
32///
33/// This struct holds the identity information needed to construct events and provides
34/// methods to generate all order event types. It is `Clone` and `Send`, allowing it
35/// to be used in async contexts.
36#[derive(Debug, Clone)]
37pub struct OrderEventFactory {
38    trader_id: TraderId,
39    account_id: AccountId,
40    account_type: AccountType,
41    base_currency: Option<Currency>,
42}
43
44impl OrderEventFactory {
45    /// Creates a new [`OrderEventFactory`] instance.
46    #[must_use]
47    pub fn new(
48        trader_id: TraderId,
49        account_id: AccountId,
50        account_type: AccountType,
51        base_currency: Option<Currency>,
52    ) -> Self {
53        Self {
54            trader_id,
55            account_id,
56            account_type,
57            base_currency,
58        }
59    }
60
61    /// Returns the trader ID.
62    #[must_use]
63    pub fn trader_id(&self) -> TraderId {
64        self.trader_id
65    }
66
67    /// Returns the account ID.
68    #[must_use]
69    pub fn account_id(&self) -> AccountId {
70        self.account_id
71    }
72
73    /// Sets the account ID.
74    pub const fn set_account_id(&mut self, account_id: AccountId) {
75        self.account_id = account_id;
76    }
77
78    /// Generates an account state event.
79    #[must_use]
80    pub fn generate_account_state(
81        &self,
82        balances: Vec<AccountBalance>,
83        margins: Vec<MarginBalance>,
84        reported: bool,
85        ts_event: UnixNanos,
86        ts_init: UnixNanos,
87        info: Option<Params>,
88    ) -> AccountState {
89        AccountState::new(
90            self.account_id,
91            self.account_type,
92            balances,
93            margins,
94            reported,
95            UUID4::new(),
96            ts_event,
97            ts_init,
98            self.base_currency,
99        )
100        .with_info(info)
101    }
102
103    /// Generates an order denied event.
104    ///
105    /// The event timestamp `ts_event` is the same as the initialized timestamp `ts_init`.
106    #[must_use]
107    pub fn generate_order_denied(
108        &self,
109        order: &OrderAny,
110        reason: &str,
111        ts_init: UnixNanos,
112    ) -> OrderEventAny {
113        let event = OrderDenied::new(
114            self.trader_id,
115            order.strategy_id(),
116            order.instrument_id(),
117            order.client_order_id(),
118            reason.into(),
119            UUID4::new(),
120            ts_init,
121            ts_init,
122        );
123        OrderEventAny::Denied(event)
124    }
125
126    /// Generates an order submitted event.
127    ///
128    /// The event timestamp `ts_event` is the same as the initialized timestamp `ts_init`.
129    #[must_use]
130    pub fn generate_order_submitted(&self, order: &OrderAny, ts_init: UnixNanos) -> OrderEventAny {
131        let event = OrderSubmitted::new(
132            self.trader_id,
133            order.strategy_id(),
134            order.instrument_id(),
135            order.client_order_id(),
136            self.account_id,
137            UUID4::new(),
138            ts_init,
139            ts_init,
140        );
141        OrderEventAny::Submitted(event)
142    }
143
144    /// Generates an order rejected event.
145    #[must_use]
146    pub fn generate_order_rejected(
147        &self,
148        order: &OrderAny,
149        reason: &str,
150        ts_event: UnixNanos,
151        ts_init: UnixNanos,
152        due_post_only: bool,
153    ) -> OrderEventAny {
154        let event = OrderRejected::new(
155            self.trader_id,
156            order.strategy_id(),
157            order.instrument_id(),
158            order.client_order_id(),
159            self.account_id,
160            reason.into(),
161            UUID4::new(),
162            ts_event,
163            ts_init,
164            false,
165            due_post_only,
166        );
167        OrderEventAny::Rejected(event)
168    }
169
170    /// Generates an order accepted event.
171    #[must_use]
172    pub fn generate_order_accepted(
173        &self,
174        order: &OrderAny,
175        venue_order_id: VenueOrderId,
176        ts_event: UnixNanos,
177        ts_init: UnixNanos,
178    ) -> OrderEventAny {
179        let event = OrderAccepted::new(
180            self.trader_id,
181            order.strategy_id(),
182            order.instrument_id(),
183            order.client_order_id(),
184            venue_order_id,
185            self.account_id,
186            UUID4::new(),
187            ts_event,
188            ts_init,
189            false,
190        );
191        OrderEventAny::Accepted(event)
192    }
193
194    /// Generates an order modify rejected event.
195    #[must_use]
196    pub fn generate_order_modify_rejected(
197        &self,
198        order: &OrderAny,
199        venue_order_id: Option<VenueOrderId>,
200        reason: &str,
201        ts_event: UnixNanos,
202        ts_init: UnixNanos,
203    ) -> OrderEventAny {
204        let event = OrderModifyRejected::new(
205            self.trader_id,
206            order.strategy_id(),
207            order.instrument_id(),
208            order.client_order_id(),
209            reason.into(),
210            UUID4::new(),
211            ts_event,
212            ts_init,
213            false,
214            venue_order_id,
215            Some(self.account_id),
216        );
217        OrderEventAny::ModifyRejected(event)
218    }
219
220    /// Generates an order cancel rejected event.
221    #[must_use]
222    pub fn generate_order_cancel_rejected(
223        &self,
224        order: &OrderAny,
225        venue_order_id: Option<VenueOrderId>,
226        reason: &str,
227        ts_event: UnixNanos,
228        ts_init: UnixNanos,
229    ) -> OrderEventAny {
230        let event = OrderCancelRejected::new(
231            self.trader_id,
232            order.strategy_id(),
233            order.instrument_id(),
234            order.client_order_id(),
235            reason.into(),
236            UUID4::new(),
237            ts_event,
238            ts_init,
239            false,
240            venue_order_id,
241            Some(self.account_id),
242        );
243        OrderEventAny::CancelRejected(event)
244    }
245
246    /// Generates an order updated event.
247    #[expect(clippy::too_many_arguments)]
248    #[must_use]
249    pub fn generate_order_updated(
250        &self,
251        order: &OrderAny,
252        venue_order_id: VenueOrderId,
253        quantity: Quantity,
254        price: Option<Price>,
255        trigger_price: Option<Price>,
256        protection_price: Option<Price>,
257        ts_event: UnixNanos,
258        ts_init: UnixNanos,
259    ) -> OrderEventAny {
260        let event = OrderUpdated::new(
261            self.trader_id,
262            order.strategy_id(),
263            order.instrument_id(),
264            order.client_order_id(),
265            quantity,
266            UUID4::new(),
267            ts_event,
268            ts_init,
269            false,
270            Some(venue_order_id),
271            Some(self.account_id),
272            price,
273            trigger_price,
274            protection_price,
275            false, // is_quote_quantity
276        );
277        OrderEventAny::Updated(event)
278    }
279
280    /// Generates an order canceled event.
281    #[must_use]
282    pub fn generate_order_canceled(
283        &self,
284        order: &OrderAny,
285        venue_order_id: Option<VenueOrderId>,
286        ts_event: UnixNanos,
287        ts_init: UnixNanos,
288    ) -> OrderEventAny {
289        let event = OrderCanceled::new(
290            self.trader_id,
291            order.strategy_id(),
292            order.instrument_id(),
293            order.client_order_id(),
294            UUID4::new(),
295            ts_event,
296            ts_init,
297            false,
298            venue_order_id,
299            Some(self.account_id),
300            None,
301        );
302        OrderEventAny::Canceled(event)
303    }
304
305    /// Generates an order triggered event.
306    #[must_use]
307    pub fn generate_order_triggered(
308        &self,
309        order: &OrderAny,
310        venue_order_id: Option<VenueOrderId>,
311        ts_event: UnixNanos,
312        ts_init: UnixNanos,
313    ) -> OrderEventAny {
314        let event = OrderTriggered::new(
315            self.trader_id,
316            order.strategy_id(),
317            order.instrument_id(),
318            order.client_order_id(),
319            UUID4::new(),
320            ts_event,
321            ts_init,
322            false,
323            venue_order_id,
324            Some(self.account_id),
325        );
326        OrderEventAny::Triggered(event)
327    }
328
329    /// Generates an order expired event.
330    #[must_use]
331    pub fn generate_order_expired(
332        &self,
333        order: &OrderAny,
334        venue_order_id: Option<VenueOrderId>,
335        ts_event: UnixNanos,
336        ts_init: UnixNanos,
337    ) -> OrderEventAny {
338        let event = OrderExpired::new(
339            self.trader_id,
340            order.strategy_id(),
341            order.instrument_id(),
342            order.client_order_id(),
343            UUID4::new(),
344            ts_event,
345            ts_init,
346            false,
347            venue_order_id,
348            Some(self.account_id),
349        );
350        OrderEventAny::Expired(event)
351    }
352
353    /// Generates an order filled event.
354    #[expect(clippy::too_many_arguments)]
355    #[must_use]
356    pub fn generate_order_filled(
357        &self,
358        order: &OrderAny,
359        venue_order_id: VenueOrderId,
360        venue_position_id: Option<PositionId>,
361        trade_id: TradeId,
362        last_qty: Quantity,
363        last_px: Price,
364        quote_currency: Currency,
365        commission: Option<Money>,
366        liquidity_side: LiquiditySide,
367        ts_event: UnixNanos,
368        ts_init: UnixNanos,
369    ) -> OrderEventAny {
370        let event = OrderFilled::new(
371            self.trader_id,
372            order.strategy_id(),
373            order.instrument_id(),
374            order.client_order_id(),
375            venue_order_id,
376            self.account_id,
377            trade_id,
378            order.order_side(),
379            order.order_type(),
380            last_qty,
381            last_px,
382            quote_currency,
383            liquidity_side,
384            UUID4::new(),
385            ts_event,
386            ts_init,
387            false,
388            venue_position_id,
389            commission,
390            None,
391        );
392        OrderEventAny::Filled(event)
393    }
394}
395
396#[cfg(test)]
397mod tests {
398    use nautilus_model::{
399        enums::{OrderSide, OrderType},
400        identifiers::{ClientOrderId, InstrumentId},
401        orders::OrderTestBuilder,
402        types::Quantity,
403    };
404    use rstest::{fixture, rstest};
405
406    use super::*;
407
408    const TRADER_ID: &str = "TESTER-001";
409    const ACCOUNT_ID: &str = "SIM-002";
410
411    #[fixture]
412    fn factory() -> OrderEventFactory {
413        OrderEventFactory::new(
414            TraderId::from(TRADER_ID),
415            AccountId::from(ACCOUNT_ID),
416            AccountType::Margin,
417            Some(Currency::USD()),
418        )
419    }
420
421    #[fixture]
422    fn order() -> OrderAny {
423        OrderTestBuilder::new(OrderType::Limit)
424            .instrument_id(InstrumentId::from("ETHUSDT.BINANCE"))
425            .client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
426            .side(OrderSide::Sell)
427            .quantity(Quantity::from("3.000"))
428            .price(Price::from("2500.00"))
429            .build()
430    }
431
432    #[rstest]
433    fn test_identity_accessors_and_account_id_override(mut factory: OrderEventFactory) {
434        assert_eq!(factory.trader_id(), TraderId::from(TRADER_ID));
435        assert_eq!(factory.account_id(), AccountId::from(ACCOUNT_ID));
436
437        factory.set_account_id(AccountId::from("SIM-999"));
438
439        assert_eq!(factory.account_id(), AccountId::from("SIM-999"));
440        assert_eq!(factory.trader_id(), TraderId::from(TRADER_ID));
441    }
442
443    #[rstest]
444    fn test_generate_account_state_carries_factory_identity(factory: OrderEventFactory) {
445        let balance = AccountBalance::new(
446            Money::new(1_500.0, Currency::USD()),
447            Money::new(500.0, Currency::USD()),
448            Money::new(1_000.0, Currency::USD()),
449        );
450
451        let margin = MarginBalance::new(
452            Money::new(10.0, Currency::USD()),
453            Money::new(20.0, Currency::USD()),
454            Some(InstrumentId::from("ETHUSDT.BINANCE")),
455        );
456
457        let event = factory.generate_account_state(
458            vec![balance],
459            vec![margin],
460            true,
461            UnixNanos::from(3),
462            UnixNanos::from(5),
463            None,
464        );
465
466        assert_eq!(event.account_id, AccountId::from(ACCOUNT_ID));
467        assert_eq!(event.account_type, AccountType::Margin);
468        assert_eq!(event.base_currency, Some(Currency::USD()));
469        assert_eq!(event.balances, vec![balance]);
470        assert_eq!(event.margins, vec![margin]);
471        assert!(event.is_reported);
472        assert_eq!(event.ts_event, UnixNanos::from(3));
473        assert_eq!(event.ts_init, UnixNanos::from(5));
474        assert_eq!(event.info, None);
475    }
476
477    #[rstest]
478    fn test_generate_account_state_attaches_info(factory: OrderEventFactory) {
479        let mut info = Params::new();
480        info.insert("source".into(), "unit-test".into());
481
482        let event = factory.generate_account_state(
483            Vec::new(),
484            Vec::new(),
485            false,
486            UnixNanos::from(3),
487            UnixNanos::from(5),
488            Some(info.clone()),
489        );
490
491        assert!(!event.is_reported);
492        assert_eq!(event.info, Some(info));
493    }
494
495    #[rstest]
496    fn test_generate_order_denied_uses_ts_init_for_both_timestamps(
497        factory: OrderEventFactory,
498        order: OrderAny,
499    ) {
500        let OrderEventAny::Denied(event) =
501            factory.generate_order_denied(&order, "Risk limit exceeded", UnixNanos::from(11))
502        else {
503            panic!("expected a denied event");
504        };
505
506        assert_eq!(event.trader_id, TraderId::from(TRADER_ID));
507        assert_eq!(event.strategy_id, order.strategy_id());
508        assert_eq!(event.instrument_id, order.instrument_id());
509        assert_eq!(event.client_order_id, order.client_order_id());
510        assert_eq!(event.reason.as_str(), "Risk limit exceeded");
511        assert_eq!(event.ts_event, UnixNanos::from(11));
512        assert_eq!(event.ts_init, UnixNanos::from(11));
513    }
514
515    #[rstest]
516    fn test_generate_order_submitted_uses_ts_init_for_both_timestamps(
517        factory: OrderEventFactory,
518        order: OrderAny,
519    ) {
520        let OrderEventAny::Submitted(event) =
521            factory.generate_order_submitted(&order, UnixNanos::from(13))
522        else {
523            panic!("expected a submitted event");
524        };
525
526        assert_eq!(event.trader_id, TraderId::from(TRADER_ID));
527        assert_eq!(event.strategy_id, order.strategy_id());
528        assert_eq!(event.instrument_id, order.instrument_id());
529        assert_eq!(event.client_order_id, order.client_order_id());
530        assert_eq!(event.account_id, AccountId::from(ACCOUNT_ID));
531        assert_eq!(event.ts_event, UnixNanos::from(13));
532        assert_eq!(event.ts_init, UnixNanos::from(13));
533    }
534
535    #[rstest]
536    #[case(true)]
537    #[case(false)]
538    fn test_generate_order_rejected_carries_reason_and_post_only_flag(
539        factory: OrderEventFactory,
540        order: OrderAny,
541        #[case] due_post_only: bool,
542    ) {
543        let OrderEventAny::Rejected(event) = factory.generate_order_rejected(
544            &order,
545            "Insufficient margin",
546            UnixNanos::from(17),
547            UnixNanos::from(19),
548            due_post_only,
549        ) else {
550            panic!("expected a rejected event");
551        };
552
553        assert_eq!(event.trader_id, TraderId::from(TRADER_ID));
554        assert_eq!(event.strategy_id, order.strategy_id());
555        assert_eq!(event.instrument_id, order.instrument_id());
556        assert_eq!(event.client_order_id, order.client_order_id());
557        assert_eq!(event.account_id, AccountId::from(ACCOUNT_ID));
558        assert_eq!(event.reason.as_str(), "Insufficient margin");
559        assert_eq!(event.ts_event, UnixNanos::from(17));
560        assert_eq!(event.ts_init, UnixNanos::from(19));
561        assert!(!event.reconciliation);
562        assert_eq!(event.due_post_only, due_post_only);
563    }
564
565    #[rstest]
566    fn test_generate_order_accepted_carries_venue_order_id(
567        factory: OrderEventFactory,
568        order: OrderAny,
569    ) {
570        let OrderEventAny::Accepted(event) = factory.generate_order_accepted(
571            &order,
572            VenueOrderId::from("V-1"),
573            UnixNanos::from(23),
574            UnixNanos::from(29),
575        ) else {
576            panic!("expected an accepted event");
577        };
578
579        assert_eq!(event.trader_id, TraderId::from(TRADER_ID));
580        assert_eq!(event.client_order_id, order.client_order_id());
581        assert_eq!(event.venue_order_id, VenueOrderId::from("V-1"));
582        assert_eq!(event.account_id, AccountId::from(ACCOUNT_ID));
583        assert_eq!(event.ts_event, UnixNanos::from(23));
584        assert_eq!(event.ts_init, UnixNanos::from(29));
585        assert!(!event.reconciliation);
586    }
587
588    #[rstest]
589    fn test_generate_order_modify_rejected_carries_optional_venue_order_id(
590        factory: OrderEventFactory,
591        order: OrderAny,
592    ) {
593        let OrderEventAny::ModifyRejected(event) = factory.generate_order_modify_rejected(
594            &order,
595            Some(VenueOrderId::from("V-2")),
596            "Unknown order",
597            UnixNanos::from(31),
598            UnixNanos::from(37),
599        ) else {
600            panic!("expected a modify rejected event");
601        };
602
603        assert_eq!(event.trader_id, TraderId::from(TRADER_ID));
604        assert_eq!(event.client_order_id, order.client_order_id());
605        assert_eq!(event.venue_order_id, Some(VenueOrderId::from("V-2")));
606        assert_eq!(event.account_id, Some(AccountId::from(ACCOUNT_ID)));
607        assert_eq!(event.reason.as_str(), "Unknown order");
608        assert_eq!(event.ts_event, UnixNanos::from(31));
609        assert_eq!(event.ts_init, UnixNanos::from(37));
610    }
611
612    #[rstest]
613    fn test_generate_order_cancel_rejected_allows_absent_venue_order_id(
614        factory: OrderEventFactory,
615        order: OrderAny,
616    ) {
617        let OrderEventAny::CancelRejected(event) = factory.generate_order_cancel_rejected(
618            &order,
619            None,
620            "Already closed",
621            UnixNanos::from(41),
622            UnixNanos::from(43),
623        ) else {
624            panic!("expected a cancel rejected event");
625        };
626
627        assert_eq!(event.trader_id, TraderId::from(TRADER_ID));
628        assert_eq!(event.client_order_id, order.client_order_id());
629        assert_eq!(event.venue_order_id, None);
630        assert_eq!(event.account_id, Some(AccountId::from(ACCOUNT_ID)));
631        assert_eq!(event.reason.as_str(), "Already closed");
632        assert_eq!(event.ts_event, UnixNanos::from(41));
633        assert_eq!(event.ts_init, UnixNanos::from(43));
634    }
635
636    #[rstest]
637    fn test_generate_order_updated_carries_every_revised_field(
638        factory: OrderEventFactory,
639        order: OrderAny,
640    ) {
641        let OrderEventAny::Updated(event) = factory.generate_order_updated(
642            &order,
643            VenueOrderId::from("V-3"),
644            Quantity::from("2.000"),
645            Some(Price::from("2400.00")),
646            Some(Price::from("2450.00")),
647            Some(Price::from("2350.00")),
648            UnixNanos::from(47),
649            UnixNanos::from(53),
650        ) else {
651            panic!("expected an updated event");
652        };
653
654        assert_eq!(event.trader_id, TraderId::from(TRADER_ID));
655        assert_eq!(event.client_order_id, order.client_order_id());
656        assert_eq!(event.venue_order_id, Some(VenueOrderId::from("V-3")));
657        assert_eq!(event.account_id, Some(AccountId::from(ACCOUNT_ID)));
658        assert_eq!(event.quantity, Quantity::from("2.000"));
659        assert_eq!(event.price, Some(Price::from("2400.00")));
660        assert_eq!(event.trigger_price, Some(Price::from("2450.00")));
661        assert_eq!(event.protection_price, Some(Price::from("2350.00")));
662        assert!(!event.is_quote_quantity);
663        assert_eq!(event.ts_event, UnixNanos::from(47));
664        assert_eq!(event.ts_init, UnixNanos::from(53));
665    }
666
667    #[rstest]
668    fn test_generate_order_canceled_carries_identity(factory: OrderEventFactory, order: OrderAny) {
669        let OrderEventAny::Canceled(event) = factory.generate_order_canceled(
670            &order,
671            Some(VenueOrderId::from("V-4")),
672            UnixNanos::from(59),
673            UnixNanos::from(61),
674        ) else {
675            panic!("expected a canceled event");
676        };
677
678        assert_eq!(event.trader_id, TraderId::from(TRADER_ID));
679        assert_eq!(event.client_order_id, order.client_order_id());
680        assert_eq!(event.venue_order_id, Some(VenueOrderId::from("V-4")));
681        assert_eq!(event.account_id, Some(AccountId::from(ACCOUNT_ID)));
682        assert_eq!(event.ts_event, UnixNanos::from(59));
683        assert_eq!(event.ts_init, UnixNanos::from(61));
684    }
685
686    #[rstest]
687    fn test_generate_order_triggered_carries_identity(factory: OrderEventFactory, order: OrderAny) {
688        let OrderEventAny::Triggered(event) = factory.generate_order_triggered(
689            &order,
690            Some(VenueOrderId::from("V-5")),
691            UnixNanos::from(67),
692            UnixNanos::from(71),
693        ) else {
694            panic!("expected a triggered event");
695        };
696
697        assert_eq!(event.trader_id, TraderId::from(TRADER_ID));
698        assert_eq!(event.client_order_id, order.client_order_id());
699        assert_eq!(event.venue_order_id, Some(VenueOrderId::from("V-5")));
700        assert_eq!(event.account_id, Some(AccountId::from(ACCOUNT_ID)));
701        assert_eq!(event.ts_event, UnixNanos::from(67));
702        assert_eq!(event.ts_init, UnixNanos::from(71));
703    }
704
705    #[rstest]
706    fn test_generate_order_expired_carries_identity(factory: OrderEventFactory, order: OrderAny) {
707        let OrderEventAny::Expired(event) = factory.generate_order_expired(
708            &order,
709            Some(VenueOrderId::from("V-6")),
710            UnixNanos::from(73),
711            UnixNanos::from(79),
712        ) else {
713            panic!("expected an expired event");
714        };
715
716        assert_eq!(event.trader_id, TraderId::from(TRADER_ID));
717        assert_eq!(event.client_order_id, order.client_order_id());
718        assert_eq!(event.venue_order_id, Some(VenueOrderId::from("V-6")));
719        assert_eq!(event.account_id, Some(AccountId::from(ACCOUNT_ID)));
720        assert_eq!(event.ts_event, UnixNanos::from(73));
721        assert_eq!(event.ts_init, UnixNanos::from(79));
722    }
723
724    #[rstest]
725    fn test_generate_order_filled_takes_side_and_type_from_the_order(
726        factory: OrderEventFactory,
727        order: OrderAny,
728    ) {
729        let commission = Money::new(1.25, Currency::USD());
730
731        let OrderEventAny::Filled(event) = factory.generate_order_filled(
732            &order,
733            VenueOrderId::from("V-7"),
734            Some(PositionId::from("P-1")),
735            TradeId::from("T-1"),
736            Quantity::from("1.500"),
737            Price::from("2499.50"),
738            Currency::USDT(),
739            Some(commission),
740            LiquiditySide::Maker,
741            UnixNanos::from(83),
742            UnixNanos::from(89),
743        ) else {
744            panic!("expected a filled event");
745        };
746
747        assert_eq!(event.trader_id, TraderId::from(TRADER_ID));
748        assert_eq!(event.strategy_id, order.strategy_id());
749        assert_eq!(event.instrument_id, order.instrument_id());
750        assert_eq!(event.client_order_id, order.client_order_id());
751        assert_eq!(event.venue_order_id, VenueOrderId::from("V-7"));
752        assert_eq!(event.account_id, AccountId::from(ACCOUNT_ID));
753        assert_eq!(event.trade_id, TradeId::from("T-1"));
754        assert_eq!(event.position_id, Some(PositionId::from("P-1")));
755        assert_eq!(event.order_side, OrderSide::Sell);
756        assert_eq!(event.order_type, OrderType::Limit);
757        assert_eq!(event.last_qty, Quantity::from("1.500"));
758        assert_eq!(event.last_px, Price::from("2499.50"));
759        assert_eq!(event.currency, Currency::USDT());
760        assert_eq!(event.liquidity_side, LiquiditySide::Maker);
761        assert_eq!(event.commission, Some(commission));
762        assert_eq!(event.ts_event, UnixNanos::from(83));
763        assert_eq!(event.ts_init, UnixNanos::from(89));
764    }
765
766    #[rstest]
767    fn test_generate_order_filled_allows_absent_position_and_commission(
768        factory: OrderEventFactory,
769        order: OrderAny,
770    ) {
771        let OrderEventAny::Filled(event) = factory.generate_order_filled(
772            &order,
773            VenueOrderId::from("V-8"),
774            None,
775            TradeId::from("T-2"),
776            Quantity::from("3.000"),
777            Price::from("2501.00"),
778            Currency::USDT(),
779            None,
780            LiquiditySide::Taker,
781            UnixNanos::from(97),
782            UnixNanos::from(101),
783        ) else {
784            panic!("expected a filled event");
785        };
786
787        assert_eq!(event.position_id, None);
788        assert_eq!(event.commission, None);
789        assert_eq!(event.liquidity_side, LiquiditySide::Taker);
790    }
791
792    #[rstest]
793    fn test_generated_events_carry_distinct_event_ids(factory: OrderEventFactory, order: OrderAny) {
794        let (OrderEventAny::Submitted(first), OrderEventAny::Submitted(second)) = (
795            factory.generate_order_submitted(&order, UnixNanos::from(1)),
796            factory.generate_order_submitted(&order, UnixNanos::from(1)),
797        ) else {
798            panic!("expected submitted events");
799        };
800
801        assert_ne!(first.event_id, second.event_id);
802    }
803
804    #[rstest]
805    fn test_factory_clone_preserves_identity(factory: OrderEventFactory) {
806        let mut clone = factory.clone();
807        clone.set_account_id(AccountId::from("SIM-003"));
808
809        assert_eq!(factory.account_id(), AccountId::from(ACCOUNT_ID));
810        assert_eq!(clone.account_id(), AccountId::from("SIM-003"));
811        assert_eq!(clone.trader_id(), factory.trader_id());
812    }
813}