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, str::FromStr};
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::{OrderAccepted, OrderCanceled, OrderEventAny, OrderFilled, OrderSubmitted},
30    identifiers::{
31        AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, TraderId, Venue,
32        VenueOrderId,
33    },
34    instruments::{Instrument, InstrumentAny},
35    orders::{Order, OrderTestBuilder},
36    stubs::TestDefault,
37    types::{Money, Price, Quantity},
38};
39
40impl TestDefault for LimitOrder {
41    /// Creates a new test default [`LimitOrder`] instance.
42    fn test_default() -> Self {
43        Self::new(
44            TraderId::test_default(),
45            StrategyId::test_default(),
46            InstrumentId::test_default(),
47            ClientOrderId::test_default(),
48            OrderSide::Buy,
49            Quantity::from(100_000),
50            Price::from("1.00000"),
51            TimeInForce::Gtc,
52            None,
53            false,
54            false,
55            false,
56            None,
57            None,
58            None,
59            None,
60            None,
61            None,
62            None,
63            None,
64            None,
65            None,
66            None,
67            UUID4::default(),
68            UnixNanos::default(),
69        )
70    }
71}
72
73impl TestDefault for LimitIfTouchedOrder {
74    /// Creates a new test default [`LimitIfTouchedOrder`] instance.
75    fn test_default() -> Self {
76        Self::new(
77            TraderId::test_default(),
78            StrategyId::test_default(),
79            InstrumentId::test_default(),
80            ClientOrderId::test_default(),
81            OrderSide::Buy,
82            Quantity::from(100_000),
83            Price::from("1.00000"),
84            Price::from("1.00000"),
85            TriggerType::BidAsk,
86            TimeInForce::Gtc,
87            None,
88            false,
89            false,
90            false,
91            None,
92            None,
93            None,
94            None,
95            None,
96            None,
97            None,
98            None,
99            None,
100            None,
101            None,
102            UUID4::default(),
103            UnixNanos::default(),
104        )
105    }
106}
107
108impl TestDefault for MarketOrder {
109    /// Creates a new test default [`MarketOrder`] instance.
110    fn test_default() -> Self {
111        Self::new(
112            TraderId::test_default(),
113            StrategyId::test_default(),
114            InstrumentId::test_default(),
115            ClientOrderId::test_default(),
116            OrderSide::Buy,
117            Quantity::from(100_000),
118            TimeInForce::Day,
119            UUID4::default(),
120            UnixNanos::default(),
121            false,
122            false,
123            None,
124            None,
125            None,
126            None,
127            None,
128            None,
129            None,
130            None,
131        )
132    }
133}
134
135impl TestDefault for MarketIfTouchedOrder {
136    /// Creates a new test default [`MarketIfTouchedOrder`] instance.
137    fn test_default() -> Self {
138        Self::new(
139            TraderId::test_default(),
140            StrategyId::test_default(),
141            InstrumentId::test_default(),
142            ClientOrderId::test_default(),
143            OrderSide::Buy,
144            Quantity::from(100_000),
145            Price::from("1.00000"),
146            TriggerType::BidAsk,
147            TimeInForce::Gtc,
148            None,
149            false,
150            false,
151            None,
152            None,
153            None,
154            None,
155            None,
156            None,
157            None,
158            None,
159            None,
160            None,
161            UUID4::default(),
162            UnixNanos::default(),
163        )
164    }
165}
166
167impl TestDefault for MarketToLimitOrder {
168    /// Creates a new test default [`MarketToLimitOrder`] instance.
169    fn test_default() -> Self {
170        Self::new(
171            TraderId::test_default(),
172            StrategyId::test_default(),
173            InstrumentId::test_default(),
174            ClientOrderId::test_default(),
175            OrderSide::Buy,
176            Quantity::from(100_000),
177            TimeInForce::Gtc,
178            None,
179            false,
180            false,
181            false,
182            None,
183            None,
184            None,
185            None,
186            None,
187            None,
188            None,
189            None,
190            None,
191            UUID4::default(),
192            UnixNanos::default(),
193        )
194    }
195}
196
197impl TestDefault for StopLimitOrder {
198    /// Creates a new test default [`StopLimitOrder`] instance.
199    fn test_default() -> Self {
200        Self::new(
201            TraderId::test_default(),
202            StrategyId::test_default(),
203            InstrumentId::test_default(),
204            ClientOrderId::test_default(),
205            OrderSide::Buy,
206            Quantity::from(100_000),
207            Price::from("1.00000"),
208            Price::from("1.00000"),
209            TriggerType::BidAsk,
210            TimeInForce::Gtc,
211            None,
212            false,
213            false,
214            false,
215            None,
216            None,
217            None,
218            None,
219            None,
220            None,
221            None,
222            None,
223            None,
224            None,
225            None,
226            UUID4::default(),
227            UnixNanos::default(),
228        )
229    }
230}
231
232impl TestDefault for StopMarketOrder {
233    /// Creates a new test default [`StopMarketOrder`] instance.
234    fn test_default() -> Self {
235        Self::new(
236            TraderId::test_default(),
237            StrategyId::test_default(),
238            InstrumentId::test_default(),
239            ClientOrderId::test_default(),
240            OrderSide::Buy,
241            Quantity::from(100_000),
242            Price::from("1.00000"),
243            TriggerType::BidAsk,
244            TimeInForce::Gtc,
245            None,
246            false,
247            false,
248            None,
249            None,
250            None,
251            None,
252            None,
253            None,
254            None,
255            None,
256            None,
257            None,
258            None,
259            UUID4::default(),
260            UnixNanos::default(),
261        )
262    }
263}
264
265impl TestDefault for TrailingStopLimitOrder {
266    /// Creates a new test default [`TrailingStopLimitOrder`] instance.
267    fn test_default() -> Self {
268        Self::new(
269            TraderId::test_default(),
270            StrategyId::test_default(),
271            InstrumentId::test_default(),
272            ClientOrderId::test_default(),
273            OrderSide::Buy,
274            Quantity::from(100_000),
275            Price::from("1.00000"),
276            Price::from("1.00000"),
277            TriggerType::BidAsk,
278            dec!(0.001),
279            dec!(0.001),
280            TrailingOffsetType::Price,
281            TimeInForce::Gtc,
282            None,
283            false,
284            false,
285            false,
286            None,
287            None,
288            None,
289            None,
290            None,
291            None,
292            None,
293            None,
294            None,
295            None,
296            None,
297            UUID4::default(),
298            UnixNanos::default(),
299        )
300    }
301}
302
303impl TestDefault for TrailingStopMarketOrder {
304    /// Creates a new test default [`TrailingStopMarketOrder`] instance.
305    fn test_default() -> Self {
306        Self::new(
307            TraderId::test_default(),
308            StrategyId::test_default(),
309            InstrumentId::test_default(),
310            ClientOrderId::test_default(),
311            OrderSide::Buy,
312            Quantity::from(100_000),
313            Price::from("1.00000"),
314            TriggerType::BidAsk,
315            dec!(0.001),
316            TrailingOffsetType::Price,
317            TimeInForce::Gtc,
318            None,
319            false,
320            false,
321            None,
322            None,
323            None,
324            None,
325            None,
326            None,
327            None,
328            None,
329            None,
330            None,
331            None,
332            UUID4::default(),
333            UnixNanos::default(),
334        )
335    }
336}
337
338#[derive(Debug)]
339pub struct TestOrderEventStubs;
340
341impl TestOrderEventStubs {
342    #[must_use]
343    pub fn submitted(order: &OrderAny, account_id: AccountId) -> OrderEventAny {
344        let event = OrderSubmitted::new(
345            order.trader_id(),
346            order.strategy_id(),
347            order.instrument_id(),
348            order.client_order_id(),
349            account_id,
350            UUID4::new(),
351            UnixNanos::default(),
352            UnixNanos::default(),
353        );
354        OrderEventAny::Submitted(event)
355    }
356
357    #[must_use]
358    pub fn accepted(
359        order: &OrderAny,
360        account_id: AccountId,
361        venue_order_id: VenueOrderId,
362    ) -> OrderEventAny {
363        let event = OrderAccepted::new(
364            order.trader_id(),
365            order.strategy_id(),
366            order.instrument_id(),
367            order.client_order_id(),
368            venue_order_id,
369            account_id,
370            UUID4::new(),
371            UnixNanos::default(),
372            UnixNanos::default(),
373            false,
374        );
375        OrderEventAny::Accepted(event)
376    }
377
378    #[must_use]
379    pub fn canceled(
380        order: &OrderAny,
381        account_id: AccountId,
382        venue_order_id: Option<VenueOrderId>,
383    ) -> OrderEventAny {
384        let event = OrderCanceled::new(
385            order.trader_id(),
386            order.strategy_id(),
387            order.instrument_id(),
388            order.client_order_id(),
389            UUID4::new(),
390            UnixNanos::default(),
391            UnixNanos::default(),
392            false, // reconciliation
393            venue_order_id,
394            Some(account_id),
395        );
396        OrderEventAny::Canceled(event)
397    }
398
399    /// # Panics
400    ///
401    /// Panics if parsing the fallback price string fails or unwrapping default values fails.
402    #[expect(clippy::too_many_arguments)]
403    pub fn filled(
404        order: &OrderAny,
405        instrument: &InstrumentAny,
406        trade_id: Option<TradeId>,
407        position_id: Option<PositionId>,
408        last_px: Option<Price>,
409        last_qty: Option<Quantity>,
410        liquidity_side: Option<LiquiditySide>,
411        commission: Option<Money>,
412        ts_filled_ns: Option<UnixNanos>,
413        account_id: Option<AccountId>,
414    ) -> OrderEventAny {
415        let venue_order_id = order
416            .venue_order_id()
417            .unwrap_or_else(VenueOrderId::test_default);
418        let account_id = account_id
419            .or(order.account_id())
420            .unwrap_or(AccountId::from("SIM-001"));
421        let trade_id = trade_id.unwrap_or(TradeId::new(
422            order.client_order_id().as_str().replace('O', "E").as_str(),
423        ));
424        let liquidity_side = liquidity_side.unwrap_or(LiquiditySide::Maker);
425        let event = UUID4::new();
426        let position_id = position_id
427            .or_else(|| order.position_id())
428            .unwrap_or(PositionId::new("1"));
429        let commission = commission.unwrap_or(Money::from("2 USD"));
430        let last_px = last_px.unwrap_or(Price::from_str("1.0").unwrap());
431        let last_qty = last_qty.unwrap_or(order.quantity());
432        let event = OrderFilled::new(
433            order.trader_id(),
434            order.strategy_id(),
435            instrument.id(),
436            order.client_order_id(),
437            venue_order_id,
438            account_id,
439            trade_id,
440            order.order_side(),
441            order.order_type(),
442            last_qty,
443            last_px,
444            instrument.quote_currency(),
445            liquidity_side,
446            event,
447            ts_filled_ns.unwrap_or_default(),
448            UnixNanos::default(),
449            false,
450            Some(position_id),
451            Some(commission),
452        );
453        OrderEventAny::Filled(event)
454    }
455}
456
457#[derive(Debug)]
458pub struct TestOrderStubs;
459
460impl TestOrderStubs {
461    /// # Panics
462    ///
463    /// Panics if applying the accepted event via `new_order.apply(...)` fails.
464    #[must_use]
465    pub fn make_accepted_order(order: &OrderAny) -> OrderAny {
466        let mut new_order = order.clone();
467        let accepted_event = TestOrderEventStubs::accepted(
468            &new_order,
469            AccountId::from("SIM-001"),
470            VenueOrderId::from("V-001"),
471        );
472        new_order.apply(accepted_event).unwrap();
473        new_order
474    }
475
476    /// # Panics
477    ///
478    /// Panics if applying the filled event via `accepted_order.apply(...)` fails.
479    #[must_use]
480    pub fn make_filled_order(
481        order: &OrderAny,
482        instrument: &InstrumentAny,
483        liquidity_side: LiquiditySide,
484    ) -> OrderAny {
485        let mut accepted_order = Self::make_accepted_order(order);
486        let fill = TestOrderEventStubs::filled(
487            &accepted_order,
488            instrument,
489            None,
490            None,
491            None,
492            None,
493            Some(liquidity_side),
494            None,
495            None,
496            None,
497        );
498        accepted_order.apply(fill).unwrap();
499        accepted_order
500    }
501}
502
503#[derive(Debug)]
504pub struct TestOrdersGenerator {
505    order_type: OrderType,
506    venue_instruments: HashMap<Venue, u32>,
507    orders_per_instrument: u32,
508}
509
510impl TestOrdersGenerator {
511    #[must_use]
512    pub fn new(order_type: OrderType) -> Self {
513        Self {
514            order_type,
515            venue_instruments: HashMap::new(),
516            orders_per_instrument: 5,
517        }
518    }
519
520    pub fn set_orders_per_instrument(&mut self, total_orders: u32) {
521        self.orders_per_instrument = total_orders;
522    }
523
524    pub fn add_venue_and_total_instruments(&mut self, venue: Venue, total_instruments: u32) {
525        self.venue_instruments.insert(venue, total_instruments);
526    }
527
528    fn generate_order(&self, instrument_id: InstrumentId, client_order_id_index: u32) -> OrderAny {
529        let client_order_id =
530            ClientOrderId::from(format!("O-{instrument_id}-{client_order_id_index}"));
531        OrderTestBuilder::new(self.order_type)
532            .quantity(Quantity::from("1"))
533            .price(Price::from("1"))
534            .instrument_id(instrument_id)
535            .client_order_id(client_order_id)
536            .build()
537    }
538
539    #[must_use]
540    pub fn build(&self) -> Vec<OrderAny> {
541        let mut orders = Vec::new();
542
543        for (venue, total_instruments) in &self.venue_instruments {
544            for i in 0..*total_instruments {
545                let instrument_id = InstrumentId::from(format!("SYMBOL-{i}.{venue}"));
546                for order_index in 0..self.orders_per_instrument {
547                    let order = self.generate_order(instrument_id, order_index);
548                    orders.push(order);
549                }
550            }
551        }
552        orders
553    }
554}
555
556#[must_use]
557pub fn create_order_list_sample(
558    total_venues: u8,
559    total_instruments: u32,
560    orders_per_instrument: u32,
561) -> Vec<OrderAny> {
562    // Create Limit orders list from order generator with spec:
563    // x venues * x instruments * x orders per instrument
564    let mut order_generator = TestOrdersGenerator::new(OrderType::Limit);
565
566    for i in 0..total_venues {
567        let venue = Venue::from(format!("VENUE-{i}"));
568        order_generator.add_venue_and_total_instruments(venue, total_instruments);
569    }
570    order_generator.set_orders_per_instrument(orders_per_instrument);
571
572    order_generator.build()
573}