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 info = Params::new([("source".into(), "unit-test".into())].into());
480
481        let event = factory.generate_account_state(
482            Vec::new(),
483            Vec::new(),
484            false,
485            UnixNanos::from(3),
486            UnixNanos::from(5),
487            Some(info.clone()),
488        );
489
490        assert!(!event.is_reported);
491        assert_eq!(event.info, Some(info));
492    }
493
494    #[rstest]
495    fn test_generate_order_denied_uses_ts_init_for_both_timestamps(
496        factory: OrderEventFactory,
497        order: OrderAny,
498    ) {
499        let OrderEventAny::Denied(event) =
500            factory.generate_order_denied(&order, "Risk limit exceeded", UnixNanos::from(11))
501        else {
502            panic!("expected a denied event");
503        };
504
505        assert_eq!(event.trader_id, TraderId::from(TRADER_ID));
506        assert_eq!(event.strategy_id, order.strategy_id());
507        assert_eq!(event.instrument_id, order.instrument_id());
508        assert_eq!(event.client_order_id, order.client_order_id());
509        assert_eq!(event.reason.as_str(), "Risk limit exceeded");
510        assert_eq!(event.ts_event, UnixNanos::from(11));
511        assert_eq!(event.ts_init, UnixNanos::from(11));
512    }
513
514    #[rstest]
515    fn test_generate_order_submitted_uses_ts_init_for_both_timestamps(
516        factory: OrderEventFactory,
517        order: OrderAny,
518    ) {
519        let OrderEventAny::Submitted(event) =
520            factory.generate_order_submitted(&order, UnixNanos::from(13))
521        else {
522            panic!("expected a submitted event");
523        };
524
525        assert_eq!(event.trader_id, TraderId::from(TRADER_ID));
526        assert_eq!(event.strategy_id, order.strategy_id());
527        assert_eq!(event.instrument_id, order.instrument_id());
528        assert_eq!(event.client_order_id, order.client_order_id());
529        assert_eq!(event.account_id, AccountId::from(ACCOUNT_ID));
530        assert_eq!(event.ts_event, UnixNanos::from(13));
531        assert_eq!(event.ts_init, UnixNanos::from(13));
532    }
533
534    #[rstest]
535    #[case(true)]
536    #[case(false)]
537    fn test_generate_order_rejected_carries_reason_and_post_only_flag(
538        factory: OrderEventFactory,
539        order: OrderAny,
540        #[case] due_post_only: bool,
541    ) {
542        let OrderEventAny::Rejected(event) = factory.generate_order_rejected(
543            &order,
544            "Insufficient margin",
545            UnixNanos::from(17),
546            UnixNanos::from(19),
547            due_post_only,
548        ) else {
549            panic!("expected a rejected event");
550        };
551
552        assert_eq!(event.trader_id, TraderId::from(TRADER_ID));
553        assert_eq!(event.strategy_id, order.strategy_id());
554        assert_eq!(event.instrument_id, order.instrument_id());
555        assert_eq!(event.client_order_id, order.client_order_id());
556        assert_eq!(event.account_id, AccountId::from(ACCOUNT_ID));
557        assert_eq!(event.reason.as_str(), "Insufficient margin");
558        assert_eq!(event.ts_event, UnixNanos::from(17));
559        assert_eq!(event.ts_init, UnixNanos::from(19));
560        assert!(!event.reconciliation);
561        assert_eq!(event.due_post_only, due_post_only);
562    }
563
564    #[rstest]
565    fn test_generate_order_accepted_carries_venue_order_id(
566        factory: OrderEventFactory,
567        order: OrderAny,
568    ) {
569        let OrderEventAny::Accepted(event) = factory.generate_order_accepted(
570            &order,
571            VenueOrderId::from("V-1"),
572            UnixNanos::from(23),
573            UnixNanos::from(29),
574        ) else {
575            panic!("expected an accepted event");
576        };
577
578        assert_eq!(event.trader_id, TraderId::from(TRADER_ID));
579        assert_eq!(event.client_order_id, order.client_order_id());
580        assert_eq!(event.venue_order_id, VenueOrderId::from("V-1"));
581        assert_eq!(event.account_id, AccountId::from(ACCOUNT_ID));
582        assert_eq!(event.ts_event, UnixNanos::from(23));
583        assert_eq!(event.ts_init, UnixNanos::from(29));
584        assert!(!event.reconciliation);
585    }
586
587    #[rstest]
588    fn test_generate_order_modify_rejected_carries_optional_venue_order_id(
589        factory: OrderEventFactory,
590        order: OrderAny,
591    ) {
592        let OrderEventAny::ModifyRejected(event) = factory.generate_order_modify_rejected(
593            &order,
594            Some(VenueOrderId::from("V-2")),
595            "Unknown order",
596            UnixNanos::from(31),
597            UnixNanos::from(37),
598        ) else {
599            panic!("expected a modify rejected event");
600        };
601
602        assert_eq!(event.trader_id, TraderId::from(TRADER_ID));
603        assert_eq!(event.client_order_id, order.client_order_id());
604        assert_eq!(event.venue_order_id, Some(VenueOrderId::from("V-2")));
605        assert_eq!(event.account_id, Some(AccountId::from(ACCOUNT_ID)));
606        assert_eq!(event.reason.as_str(), "Unknown order");
607        assert_eq!(event.ts_event, UnixNanos::from(31));
608        assert_eq!(event.ts_init, UnixNanos::from(37));
609    }
610
611    #[rstest]
612    fn test_generate_order_cancel_rejected_allows_absent_venue_order_id(
613        factory: OrderEventFactory,
614        order: OrderAny,
615    ) {
616        let OrderEventAny::CancelRejected(event) = factory.generate_order_cancel_rejected(
617            &order,
618            None,
619            "Already closed",
620            UnixNanos::from(41),
621            UnixNanos::from(43),
622        ) else {
623            panic!("expected a cancel rejected event");
624        };
625
626        assert_eq!(event.trader_id, TraderId::from(TRADER_ID));
627        assert_eq!(event.client_order_id, order.client_order_id());
628        assert_eq!(event.venue_order_id, None);
629        assert_eq!(event.account_id, Some(AccountId::from(ACCOUNT_ID)));
630        assert_eq!(event.reason.as_str(), "Already closed");
631        assert_eq!(event.ts_event, UnixNanos::from(41));
632        assert_eq!(event.ts_init, UnixNanos::from(43));
633    }
634
635    #[rstest]
636    fn test_generate_order_updated_carries_every_revised_field(
637        factory: OrderEventFactory,
638        order: OrderAny,
639    ) {
640        let OrderEventAny::Updated(event) = factory.generate_order_updated(
641            &order,
642            VenueOrderId::from("V-3"),
643            Quantity::from("2.000"),
644            Some(Price::from("2400.00")),
645            Some(Price::from("2450.00")),
646            Some(Price::from("2350.00")),
647            UnixNanos::from(47),
648            UnixNanos::from(53),
649        ) else {
650            panic!("expected an updated event");
651        };
652
653        assert_eq!(event.trader_id, TraderId::from(TRADER_ID));
654        assert_eq!(event.client_order_id, order.client_order_id());
655        assert_eq!(event.venue_order_id, Some(VenueOrderId::from("V-3")));
656        assert_eq!(event.account_id, Some(AccountId::from(ACCOUNT_ID)));
657        assert_eq!(event.quantity, Quantity::from("2.000"));
658        assert_eq!(event.price, Some(Price::from("2400.00")));
659        assert_eq!(event.trigger_price, Some(Price::from("2450.00")));
660        assert_eq!(event.protection_price, Some(Price::from("2350.00")));
661        assert!(!event.is_quote_quantity);
662        assert_eq!(event.ts_event, UnixNanos::from(47));
663        assert_eq!(event.ts_init, UnixNanos::from(53));
664    }
665
666    #[rstest]
667    fn test_generate_order_canceled_carries_identity(factory: OrderEventFactory, order: OrderAny) {
668        let OrderEventAny::Canceled(event) = factory.generate_order_canceled(
669            &order,
670            Some(VenueOrderId::from("V-4")),
671            UnixNanos::from(59),
672            UnixNanos::from(61),
673        ) else {
674            panic!("expected a canceled event");
675        };
676
677        assert_eq!(event.trader_id, TraderId::from(TRADER_ID));
678        assert_eq!(event.client_order_id, order.client_order_id());
679        assert_eq!(event.venue_order_id, Some(VenueOrderId::from("V-4")));
680        assert_eq!(event.account_id, Some(AccountId::from(ACCOUNT_ID)));
681        assert_eq!(event.ts_event, UnixNanos::from(59));
682        assert_eq!(event.ts_init, UnixNanos::from(61));
683    }
684
685    #[rstest]
686    fn test_generate_order_triggered_carries_identity(factory: OrderEventFactory, order: OrderAny) {
687        let OrderEventAny::Triggered(event) = factory.generate_order_triggered(
688            &order,
689            Some(VenueOrderId::from("V-5")),
690            UnixNanos::from(67),
691            UnixNanos::from(71),
692        ) else {
693            panic!("expected a triggered event");
694        };
695
696        assert_eq!(event.trader_id, TraderId::from(TRADER_ID));
697        assert_eq!(event.client_order_id, order.client_order_id());
698        assert_eq!(event.venue_order_id, Some(VenueOrderId::from("V-5")));
699        assert_eq!(event.account_id, Some(AccountId::from(ACCOUNT_ID)));
700        assert_eq!(event.ts_event, UnixNanos::from(67));
701        assert_eq!(event.ts_init, UnixNanos::from(71));
702    }
703
704    #[rstest]
705    fn test_generate_order_expired_carries_identity(factory: OrderEventFactory, order: OrderAny) {
706        let OrderEventAny::Expired(event) = factory.generate_order_expired(
707            &order,
708            Some(VenueOrderId::from("V-6")),
709            UnixNanos::from(73),
710            UnixNanos::from(79),
711        ) else {
712            panic!("expected an expired event");
713        };
714
715        assert_eq!(event.trader_id, TraderId::from(TRADER_ID));
716        assert_eq!(event.client_order_id, order.client_order_id());
717        assert_eq!(event.venue_order_id, Some(VenueOrderId::from("V-6")));
718        assert_eq!(event.account_id, Some(AccountId::from(ACCOUNT_ID)));
719        assert_eq!(event.ts_event, UnixNanos::from(73));
720        assert_eq!(event.ts_init, UnixNanos::from(79));
721    }
722
723    #[rstest]
724    fn test_generate_order_filled_takes_side_and_type_from_the_order(
725        factory: OrderEventFactory,
726        order: OrderAny,
727    ) {
728        let commission = Money::new(1.25, Currency::USD());
729
730        let OrderEventAny::Filled(event) = factory.generate_order_filled(
731            &order,
732            VenueOrderId::from("V-7"),
733            Some(PositionId::from("P-1")),
734            TradeId::from("T-1"),
735            Quantity::from("1.500"),
736            Price::from("2499.50"),
737            Currency::USDT(),
738            Some(commission),
739            LiquiditySide::Maker,
740            UnixNanos::from(83),
741            UnixNanos::from(89),
742        ) else {
743            panic!("expected a filled event");
744        };
745
746        assert_eq!(event.trader_id, TraderId::from(TRADER_ID));
747        assert_eq!(event.strategy_id, order.strategy_id());
748        assert_eq!(event.instrument_id, order.instrument_id());
749        assert_eq!(event.client_order_id, order.client_order_id());
750        assert_eq!(event.venue_order_id, VenueOrderId::from("V-7"));
751        assert_eq!(event.account_id, AccountId::from(ACCOUNT_ID));
752        assert_eq!(event.trade_id, TradeId::from("T-1"));
753        assert_eq!(event.position_id, Some(PositionId::from("P-1")));
754        assert_eq!(event.order_side, OrderSide::Sell);
755        assert_eq!(event.order_type, OrderType::Limit);
756        assert_eq!(event.last_qty, Quantity::from("1.500"));
757        assert_eq!(event.last_px, Price::from("2499.50"));
758        assert_eq!(event.currency, Currency::USDT());
759        assert_eq!(event.liquidity_side, LiquiditySide::Maker);
760        assert_eq!(event.commission, Some(commission));
761        assert_eq!(event.ts_event, UnixNanos::from(83));
762        assert_eq!(event.ts_init, UnixNanos::from(89));
763    }
764
765    #[rstest]
766    fn test_generate_order_filled_allows_absent_position_and_commission(
767        factory: OrderEventFactory,
768        order: OrderAny,
769    ) {
770        let OrderEventAny::Filled(event) = factory.generate_order_filled(
771            &order,
772            VenueOrderId::from("V-8"),
773            None,
774            TradeId::from("T-2"),
775            Quantity::from("3.000"),
776            Price::from("2501.00"),
777            Currency::USDT(),
778            None,
779            LiquiditySide::Taker,
780            UnixNanos::from(97),
781            UnixNanos::from(101),
782        ) else {
783            panic!("expected a filled event");
784        };
785
786        assert_eq!(event.position_id, None);
787        assert_eq!(event.commission, None);
788        assert_eq!(event.liquidity_side, LiquiditySide::Taker);
789    }
790
791    #[rstest]
792    fn test_generated_events_carry_distinct_event_ids(factory: OrderEventFactory, order: OrderAny) {
793        let (OrderEventAny::Submitted(first), OrderEventAny::Submitted(second)) = (
794            factory.generate_order_submitted(&order, UnixNanos::from(1)),
795            factory.generate_order_submitted(&order, UnixNanos::from(1)),
796        ) else {
797            panic!("expected submitted events");
798        };
799
800        assert_ne!(first.event_id, second.event_id);
801    }
802
803    #[rstest]
804    fn test_factory_clone_preserves_identity(factory: OrderEventFactory) {
805        let mut clone = factory.clone();
806        clone.set_account_id(AccountId::from("SIM-003"));
807
808        assert_eq!(factory.account_id(), AccountId::from(ACCOUNT_ID));
809        assert_eq!(clone.account_id(), AccountId::from("SIM-003"));
810        assert_eq!(clone.trader_id(), factory.trader_id());
811    }
812}