Skip to main content

nautilus_model/orders/
stubs.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::collections::HashMap;
17
18use nautilus_core::{UUID4, UnixNanos};
19use rust_decimal_macros::dec;
20
21use super::{
22    any::OrderAny, limit::LimitOrder, limit_if_touched::LimitIfTouchedOrder, market::MarketOrder,
23    market_if_touched::MarketIfTouchedOrder, market_to_limit::MarketToLimitOrder,
24    stop_limit::StopLimitOrder, stop_market::StopMarketOrder,
25    trailing_stop_limit::TrailingStopLimitOrder, trailing_stop_market::TrailingStopMarketOrder,
26};
27use crate::{
28    enums::{LiquiditySide, OrderSide, OrderType, TimeInForce, TrailingOffsetType, TriggerType},
29    events::{
30        OrderEventAny,
31        order::spec::{OrderAcceptedSpec, OrderCanceledSpec, OrderFilledSpec, OrderSubmittedSpec},
32    },
33    identifiers::{
34        AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, TraderId, Venue,
35        VenueOrderId,
36    },
37    instruments::{Instrument, InstrumentAny},
38    orders::{Order, OrderTestBuilder},
39    stubs::TestDefault,
40    types::{Money, Price, Quantity},
41};
42
43impl TestDefault for LimitOrder {
44    /// Creates a new test default [`LimitOrder`] instance.
45    fn test_default() -> Self {
46        Self::new(
47            TraderId::test_default(),
48            StrategyId::test_default(),
49            InstrumentId::test_default(),
50            ClientOrderId::test_default(),
51            OrderSide::Buy,
52            Quantity::from(100_000),
53            Price::from("1.00000"),
54            TimeInForce::Gtc,
55            None,
56            false,
57            false,
58            false,
59            None,
60            None,
61            None,
62            None,
63            None,
64            None,
65            None,
66            None,
67            None,
68            None,
69            None,
70            UUID4::default(),
71            UnixNanos::default(),
72        )
73    }
74}
75
76impl TestDefault for LimitIfTouchedOrder {
77    /// Creates a new test default [`LimitIfTouchedOrder`] instance.
78    fn test_default() -> Self {
79        Self::new(
80            TraderId::test_default(),
81            StrategyId::test_default(),
82            InstrumentId::test_default(),
83            ClientOrderId::test_default(),
84            OrderSide::Buy,
85            Quantity::from(100_000),
86            Price::from("1.00000"),
87            Price::from("1.00000"),
88            TriggerType::BidAsk,
89            TimeInForce::Gtc,
90            None,
91            false,
92            false,
93            false,
94            None,
95            None,
96            None,
97            None,
98            None,
99            None,
100            None,
101            None,
102            None,
103            None,
104            None,
105            UUID4::default(),
106            UnixNanos::default(),
107        )
108    }
109}
110
111impl TestDefault for MarketOrder {
112    /// Creates a new test default [`MarketOrder`] instance.
113    fn test_default() -> Self {
114        Self::new(
115            TraderId::test_default(),
116            StrategyId::test_default(),
117            InstrumentId::test_default(),
118            ClientOrderId::test_default(),
119            OrderSide::Buy,
120            Quantity::from(100_000),
121            TimeInForce::Day,
122            UUID4::default(),
123            UnixNanos::default(),
124            false,
125            false,
126            None,
127            None,
128            None,
129            None,
130            None,
131            None,
132            None,
133            None,
134        )
135    }
136}
137
138impl TestDefault for MarketIfTouchedOrder {
139    /// Creates a new test default [`MarketIfTouchedOrder`] instance.
140    fn test_default() -> Self {
141        Self::new(
142            TraderId::test_default(),
143            StrategyId::test_default(),
144            InstrumentId::test_default(),
145            ClientOrderId::test_default(),
146            OrderSide::Buy,
147            Quantity::from(100_000),
148            Price::from("1.00000"),
149            TriggerType::BidAsk,
150            TimeInForce::Gtc,
151            None,
152            false,
153            false,
154            None,
155            None,
156            None,
157            None,
158            None,
159            None,
160            None,
161            None,
162            None,
163            None,
164            UUID4::default(),
165            UnixNanos::default(),
166        )
167    }
168}
169
170impl TestDefault for MarketToLimitOrder {
171    /// Creates a new test default [`MarketToLimitOrder`] instance.
172    fn test_default() -> Self {
173        Self::new(
174            TraderId::test_default(),
175            StrategyId::test_default(),
176            InstrumentId::test_default(),
177            ClientOrderId::test_default(),
178            OrderSide::Buy,
179            Quantity::from(100_000),
180            TimeInForce::Gtc,
181            None,
182            false,
183            false,
184            false,
185            None,
186            None,
187            None,
188            None,
189            None,
190            None,
191            None,
192            None,
193            None,
194            UUID4::default(),
195            UnixNanos::default(),
196        )
197    }
198}
199
200impl TestDefault for StopLimitOrder {
201    /// Creates a new test default [`StopLimitOrder`] instance.
202    fn test_default() -> Self {
203        Self::new(
204            TraderId::test_default(),
205            StrategyId::test_default(),
206            InstrumentId::test_default(),
207            ClientOrderId::test_default(),
208            OrderSide::Buy,
209            Quantity::from(100_000),
210            Price::from("1.00000"),
211            Price::from("1.00000"),
212            TriggerType::BidAsk,
213            TimeInForce::Gtc,
214            None,
215            false,
216            false,
217            false,
218            None,
219            None,
220            None,
221            None,
222            None,
223            None,
224            None,
225            None,
226            None,
227            None,
228            None,
229            UUID4::default(),
230            UnixNanos::default(),
231        )
232    }
233}
234
235impl TestDefault for StopMarketOrder {
236    /// Creates a new test default [`StopMarketOrder`] instance.
237    fn test_default() -> Self {
238        Self::new(
239            TraderId::test_default(),
240            StrategyId::test_default(),
241            InstrumentId::test_default(),
242            ClientOrderId::test_default(),
243            OrderSide::Buy,
244            Quantity::from(100_000),
245            Price::from("1.00000"),
246            TriggerType::BidAsk,
247            TimeInForce::Gtc,
248            None,
249            false,
250            false,
251            None,
252            None,
253            None,
254            None,
255            None,
256            None,
257            None,
258            None,
259            None,
260            None,
261            None,
262            UUID4::default(),
263            UnixNanos::default(),
264        )
265    }
266}
267
268impl TestDefault for TrailingStopLimitOrder {
269    /// Creates a new test default [`TrailingStopLimitOrder`] instance.
270    fn test_default() -> Self {
271        Self::new(
272            TraderId::test_default(),
273            StrategyId::test_default(),
274            InstrumentId::test_default(),
275            ClientOrderId::test_default(),
276            OrderSide::Buy,
277            Quantity::from(100_000),
278            None,
279            Price::from("1.00000"),
280            Price::from("1.00000"),
281            TriggerType::BidAsk,
282            dec!(0.001),
283            dec!(0.001),
284            TrailingOffsetType::Price,
285            TimeInForce::Gtc,
286            None,
287            false,
288            false,
289            false,
290            None,
291            None,
292            None,
293            None,
294            None,
295            None,
296            None,
297            None,
298            None,
299            None,
300            None,
301            UUID4::default(),
302            UnixNanos::default(),
303        )
304    }
305}
306
307impl TestDefault for TrailingStopMarketOrder {
308    /// Creates a new test default [`TrailingStopMarketOrder`] instance.
309    fn test_default() -> Self {
310        Self::new(
311            TraderId::test_default(),
312            StrategyId::test_default(),
313            InstrumentId::test_default(),
314            ClientOrderId::test_default(),
315            OrderSide::Buy,
316            Quantity::from(100_000),
317            None,
318            Price::from("1.00000"),
319            TriggerType::BidAsk,
320            dec!(0.001),
321            TrailingOffsetType::Price,
322            TimeInForce::Gtc,
323            None,
324            false,
325            false,
326            None,
327            None,
328            None,
329            None,
330            None,
331            None,
332            None,
333            None,
334            None,
335            None,
336            None,
337            UUID4::default(),
338            UnixNanos::default(),
339        )
340    }
341}
342
343#[derive(Debug)]
344pub struct TestOrderEventStubs;
345
346impl TestOrderEventStubs {
347    #[must_use]
348    pub fn submitted(order: &OrderAny, account_id: AccountId) -> OrderEventAny {
349        let event = OrderSubmittedSpec::builder()
350            .trader_id(order.trader_id())
351            .strategy_id(order.strategy_id())
352            .instrument_id(order.instrument_id())
353            .client_order_id(order.client_order_id())
354            .account_id(account_id)
355            .build();
356        OrderEventAny::Submitted(event)
357    }
358
359    #[must_use]
360    pub fn accepted(
361        order: &OrderAny,
362        account_id: AccountId,
363        venue_order_id: VenueOrderId,
364    ) -> OrderEventAny {
365        let event = OrderAcceptedSpec::builder()
366            .trader_id(order.trader_id())
367            .strategy_id(order.strategy_id())
368            .instrument_id(order.instrument_id())
369            .client_order_id(order.client_order_id())
370            .venue_order_id(venue_order_id)
371            .account_id(account_id)
372            .build();
373        OrderEventAny::Accepted(event)
374    }
375
376    #[must_use]
377    pub fn canceled(
378        order: &OrderAny,
379        account_id: AccountId,
380        venue_order_id: Option<VenueOrderId>,
381    ) -> OrderEventAny {
382        let event = OrderCanceledSpec::builder()
383            .trader_id(order.trader_id())
384            .strategy_id(order.strategy_id())
385            .instrument_id(order.instrument_id())
386            .client_order_id(order.client_order_id())
387            .account_id(account_id)
388            .maybe_venue_order_id(venue_order_id)
389            .build();
390        OrderEventAny::Canceled(event)
391    }
392
393    /// # Panics
394    ///
395    /// Panics if parsing the fallback price string fails or unwrapping default values fails.
396    #[expect(clippy::too_many_arguments)]
397    #[must_use]
398    pub fn filled(
399        order: &OrderAny,
400        instrument: &InstrumentAny,
401        trade_id: Option<TradeId>,
402        position_id: Option<PositionId>,
403        last_px: Option<Price>,
404        last_qty: Option<Quantity>,
405        liquidity_side: Option<LiquiditySide>,
406        commission: Option<Money>,
407        ts_filled_ns: Option<UnixNanos>,
408        account_id: Option<AccountId>,
409    ) -> OrderEventAny {
410        let mut builder = OrderFilledTestBuilder::new(order, instrument);
411
412        if let Some(trade_id) = trade_id {
413            builder.trade_id(trade_id);
414        }
415
416        if let Some(position_id) = position_id {
417            builder.position_id(position_id);
418        }
419
420        if let Some(last_px) = last_px {
421            builder.last_px(last_px);
422        }
423
424        if let Some(last_qty) = last_qty {
425            builder.last_qty(last_qty);
426        }
427
428        if let Some(liquidity_side) = liquidity_side {
429            builder.liquidity_side(liquidity_side);
430        }
431
432        if let Some(commission) = commission {
433            builder.commission(commission);
434        }
435
436        if let Some(ts_event) = ts_filled_ns {
437            builder.ts_event(ts_event);
438        }
439
440        if let Some(account_id) = account_id {
441            builder.account_id(account_id);
442        }
443
444        builder.build()
445    }
446}
447
448/// Fluent test builder for a [`crate::events::OrderFilled`] event derived from an order and instrument.
449#[derive(Debug)]
450pub struct OrderFilledTestBuilder<'a> {
451    order: &'a OrderAny,
452    instrument: &'a InstrumentAny,
453    trade_id: Option<TradeId>,
454    position_id: Option<PositionId>,
455    last_px: Option<Price>,
456    last_qty: Option<Quantity>,
457    liquidity_side: Option<LiquiditySide>,
458    commission: Option<Money>,
459    ts_event: Option<UnixNanos>,
460    account_id: Option<AccountId>,
461    without_position_id: bool,
462    without_commission: bool,
463}
464
465impl<'a> OrderFilledTestBuilder<'a> {
466    /// Creates an order-derived [`crate::events::OrderFilled`] test builder.
467    #[must_use]
468    pub fn new(order: &'a OrderAny, instrument: &'a InstrumentAny) -> Self {
469        Self {
470            order,
471            instrument,
472            trade_id: None,
473            position_id: None,
474            last_px: None,
475            last_qty: None,
476            liquidity_side: None,
477            commission: None,
478            ts_event: None,
479            account_id: None,
480            without_position_id: false,
481            without_commission: false,
482        }
483    }
484
485    /// Sets the trade ID.
486    pub fn trade_id(&mut self, trade_id: TradeId) -> &mut Self {
487        self.trade_id = Some(trade_id);
488        self
489    }
490
491    /// Sets the position ID.
492    pub fn position_id(&mut self, position_id: PositionId) -> &mut Self {
493        self.position_id = Some(position_id);
494        self.without_position_id = false;
495        self
496    }
497
498    /// Omits the position ID.
499    pub fn without_position_id(&mut self) -> &mut Self {
500        self.without_position_id = true;
501        self
502    }
503
504    /// Sets the fill price.
505    pub fn last_px(&mut self, last_px: Price) -> &mut Self {
506        self.last_px = Some(last_px);
507        self
508    }
509
510    /// Sets the fill quantity.
511    pub fn last_qty(&mut self, last_qty: Quantity) -> &mut Self {
512        self.last_qty = Some(last_qty);
513        self
514    }
515
516    /// Sets the liquidity side.
517    pub fn liquidity_side(&mut self, liquidity_side: LiquiditySide) -> &mut Self {
518        self.liquidity_side = Some(liquidity_side);
519        self
520    }
521
522    /// Sets the commission.
523    pub fn commission(&mut self, commission: Money) -> &mut Self {
524        self.commission = Some(commission);
525        self.without_commission = false;
526        self
527    }
528
529    /// Omits the commission.
530    pub fn without_commission(&mut self) -> &mut Self {
531        self.without_commission = true;
532        self
533    }
534
535    /// Sets the event timestamp.
536    pub fn ts_event(&mut self, ts_event: UnixNanos) -> &mut Self {
537        self.ts_event = Some(ts_event);
538        self
539    }
540
541    /// Sets the account ID.
542    pub fn account_id(&mut self, account_id: AccountId) -> &mut Self {
543        self.account_id = Some(account_id);
544        self
545    }
546
547    /// Builds the [`OrderEventAny::Filled`] event.
548    #[must_use]
549    pub fn build(&self) -> OrderEventAny {
550        let venue_order_id = self
551            .order
552            .venue_order_id()
553            .unwrap_or_else(VenueOrderId::test_default);
554        let account_id = self
555            .account_id
556            .or(self.order.account_id())
557            .unwrap_or_else(AccountId::test_default);
558        let trade_id = self.trade_id.unwrap_or_else(|| {
559            TradeId::new(
560                self.order
561                    .client_order_id()
562                    .as_str()
563                    .replace('O', "E")
564                    .as_str(),
565            )
566        });
567        let position_id = (!self.without_position_id).then(|| {
568            self.position_id
569                .or(self.order.position_id())
570                .unwrap_or(PositionId::new("1"))
571        });
572        let commission =
573            (!self.without_commission).then(|| self.commission.unwrap_or(Money::from("2 USD")));
574        let event = OrderFilledSpec::builder()
575            .trader_id(self.order.trader_id())
576            .strategy_id(self.order.strategy_id())
577            .instrument_id(self.instrument.id())
578            .client_order_id(self.order.client_order_id())
579            .venue_order_id(venue_order_id)
580            .account_id(account_id)
581            .trade_id(trade_id)
582            .order_side(self.order.order_side())
583            .order_type(self.order.order_type())
584            .last_qty(self.last_qty.unwrap_or(self.order.quantity()))
585            .last_px(self.last_px.unwrap_or(Price::from("1.0")))
586            .currency(self.instrument.quote_currency())
587            .liquidity_side(self.liquidity_side.unwrap_or(LiquiditySide::Maker))
588            .ts_event(self.ts_event.unwrap_or_default())
589            .maybe_position_id(position_id)
590            .maybe_commission(commission)
591            .build();
592        OrderEventAny::Filled(event)
593    }
594}
595
596#[derive(Debug)]
597pub struct TestOrderStubs;
598
599impl TestOrderStubs {
600    /// # Panics
601    ///
602    /// Panics if applying the accepted event via `new_order.apply(...)` fails.
603    #[must_use]
604    pub fn make_accepted_order(order: &OrderAny) -> OrderAny {
605        let mut new_order = order.clone();
606        let accepted_event = TestOrderEventStubs::accepted(
607            &new_order,
608            AccountId::from("SIM-001"),
609            VenueOrderId::from("V-001"),
610        );
611        new_order.apply(accepted_event).unwrap();
612        new_order
613    }
614
615    /// # Panics
616    ///
617    /// Panics if applying the filled event via `accepted_order.apply(...)` fails.
618    #[must_use]
619    pub fn make_filled_order(
620        order: &OrderAny,
621        instrument: &InstrumentAny,
622        liquidity_side: LiquiditySide,
623    ) -> OrderAny {
624        let mut accepted_order = Self::make_accepted_order(order);
625        let fill = OrderFilledTestBuilder::new(&accepted_order, instrument)
626            .liquidity_side(liquidity_side)
627            .build();
628        accepted_order.apply(fill).unwrap();
629        accepted_order
630    }
631}
632
633#[derive(Debug)]
634pub struct TestOrdersGenerator {
635    order_type: OrderType,
636    venue_instruments: HashMap<Venue, u32>,
637    orders_per_instrument: u32,
638}
639
640impl TestOrdersGenerator {
641    #[must_use]
642    pub fn new(order_type: OrderType) -> Self {
643        Self {
644            order_type,
645            venue_instruments: HashMap::new(),
646            orders_per_instrument: 5,
647        }
648    }
649
650    pub fn set_orders_per_instrument(&mut self, total_orders: u32) {
651        self.orders_per_instrument = total_orders;
652    }
653
654    pub fn add_venue_and_total_instruments(&mut self, venue: Venue, total_instruments: u32) {
655        self.venue_instruments.insert(venue, total_instruments);
656    }
657
658    fn generate_order(&self, instrument_id: InstrumentId, client_order_id_index: u32) -> OrderAny {
659        let client_order_id =
660            ClientOrderId::from(format!("O-{instrument_id}-{client_order_id_index}"));
661        OrderTestBuilder::new(self.order_type)
662            .quantity(Quantity::from("1"))
663            .price(Price::from("1"))
664            .instrument_id(instrument_id)
665            .client_order_id(client_order_id)
666            .build()
667    }
668
669    #[must_use]
670    pub fn build(&self) -> Vec<OrderAny> {
671        let mut orders = Vec::new();
672
673        for (venue, total_instruments) in &self.venue_instruments {
674            for i in 0..*total_instruments {
675                let instrument_id = InstrumentId::from(format!("SYMBOL-{i}.{venue}"));
676                for order_index in 0..self.orders_per_instrument {
677                    let order = self.generate_order(instrument_id, order_index);
678                    orders.push(order);
679                }
680            }
681        }
682        orders
683    }
684}
685
686#[must_use]
687pub fn create_order_list_sample(
688    total_venues: u8,
689    total_instruments: u32,
690    orders_per_instrument: u32,
691) -> Vec<OrderAny> {
692    // Create Limit orders list from order generator with spec:
693    // x venues * x instruments * x orders per instrument
694    let mut order_generator = TestOrdersGenerator::new(OrderType::Limit);
695
696    for i in 0..total_venues {
697        let venue = Venue::from(format!("VENUE-{i}"));
698        order_generator.add_venue_and_total_instruments(venue, total_instruments);
699    }
700    order_generator.set_orders_per_instrument(orders_per_instrument);
701
702    order_generator.build()
703}
704
705#[cfg(test)]
706mod tests {
707    use rstest::rstest;
708
709    use super::*;
710    use crate::instruments::stubs::audusd_sim;
711
712    #[rstest]
713    fn preserves_legacy_fill_defaults() {
714        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
715        let order = OrderTestBuilder::new(OrderType::Market)
716            .instrument_id(instrument.id())
717            .quantity(Quantity::from(1))
718            .build();
719        let OrderEventAny::Filled(fill) = OrderFilledTestBuilder::new(&order, &instrument).build()
720        else {
721            panic!("expected OrderFilled event");
722        };
723
724        assert_eq!(fill.position_id, Some(PositionId::new("1")));
725        assert_eq!(fill.commission, Some(Money::from("2 USD")));
726        assert_eq!(fill.liquidity_side, LiquiditySide::Maker);
727    }
728
729    #[rstest]
730    fn can_omit_position_id_and_commission() {
731        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
732        let order = OrderTestBuilder::new(OrderType::Market)
733            .instrument_id(instrument.id())
734            .quantity(Quantity::from(1))
735            .build();
736        let OrderEventAny::Filled(fill) = OrderFilledTestBuilder::new(&order, &instrument)
737            .without_position_id()
738            .without_commission()
739            .build()
740        else {
741            panic!("expected OrderFilled event");
742        };
743
744        assert_eq!(fill.position_id, None);
745        assert_eq!(fill.commission, None);
746    }
747}