Skip to main content

nautilus_model/data/
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
16//! Type stubs to facilitate testing.
17
18use std::sync::Arc;
19
20use nautilus_core::{Params, UnixNanos};
21use rstest::fixture;
22use serde::{Deserialize, Serialize};
23
24use super::{
25    Bar, BarSpecification, BarType, CustomData, CustomDataTrait, DEPTH10_LEN, DataType, HasTsInit,
26    InstrumentStatus, OrderBookDelta, OrderBookDeltas, OrderBookDepth10, QuoteTick, TradeTick,
27    close::InstrumentClose, register_custom_data_json,
28};
29use crate::{
30    data::order::BookOrder,
31    enums::{
32        AggregationSource, AggressorSide, BarAggregation, BookAction, InstrumentCloseType,
33        MarketStatusAction, OrderSide, PriceType,
34    },
35    identifiers::{InstrumentId, Symbol, TradeId, Venue},
36    types::{Price, Quantity},
37};
38
39impl Default for QuoteTick {
40    /// Creates a new default [`QuoteTick`] instance for testing.
41    fn default() -> Self {
42        Self {
43            instrument_id: InstrumentId::from("AUDUSD.SIM"),
44            bid_price: Price::from("1.00000"),
45            ask_price: Price::from("1.00000"),
46            bid_size: Quantity::from(100_000),
47            ask_size: Quantity::from(100_000),
48            ts_event: UnixNanos::default(),
49            ts_init: UnixNanos::default(),
50        }
51    }
52}
53
54impl Default for TradeTick {
55    /// Creates a new default [`TradeTick`] instance for testing.
56    fn default() -> Self {
57        Self {
58            instrument_id: InstrumentId::from("AUDUSD.SIM"),
59            price: Price::from("1.00000"),
60            size: Quantity::from(100_000),
61            aggressor_side: AggressorSide::Buyer,
62            trade_id: TradeId::new("123456789"),
63            ts_event: UnixNanos::default(),
64            ts_init: UnixNanos::default(),
65        }
66    }
67}
68
69impl Default for Bar {
70    /// Creates a new default [`Bar`] instance for testing.
71    fn default() -> Self {
72        Self {
73            bar_type: BarType::from("AUDUSD.SIM-1-MINUTE-LAST-INTERNAL"),
74            open: Price::from("1.00010"),
75            high: Price::from("1.00020"),
76            low: Price::from("1.00000"),
77            close: Price::from("1.00010"),
78            volume: Quantity::from(100_000),
79            ts_event: UnixNanos::default(),
80            ts_init: UnixNanos::default(),
81        }
82    }
83}
84
85#[fixture]
86pub fn stub_delta() -> OrderBookDelta {
87    let instrument_id = InstrumentId::from("AAPL.XNAS");
88    let action = BookAction::Add;
89    let price = Price::from("100.00");
90    let size = Quantity::from("10");
91    let side = OrderSide::Buy;
92    let order_id = 123_456;
93    let flags = 0;
94    let sequence = 1;
95    let ts_event = 1;
96    let ts_init = 2;
97
98    let order = BookOrder::new(side, price, size, order_id);
99    OrderBookDelta::new(
100        instrument_id,
101        action,
102        order,
103        flags,
104        sequence,
105        ts_event.into(),
106        ts_init.into(),
107    )
108}
109
110#[fixture]
111pub fn stub_deltas() -> OrderBookDeltas {
112    let instrument_id = InstrumentId::from("AAPL.XNAS");
113    let flags = 32; // Snapshot flag
114    let sequence = 0;
115    let ts_event = 1;
116    let ts_init = 2;
117
118    let delta0 = OrderBookDelta::clear(instrument_id, sequence, ts_event.into(), ts_init.into());
119    let delta1 = OrderBookDelta::new(
120        instrument_id,
121        BookAction::Add,
122        BookOrder::new(
123            OrderSide::Sell,
124            Price::from("102.00"),
125            Quantity::from("300"),
126            1,
127        ),
128        flags,
129        sequence,
130        ts_event.into(),
131        ts_init.into(),
132    );
133    let delta2 = OrderBookDelta::new(
134        instrument_id,
135        BookAction::Add,
136        BookOrder::new(
137            OrderSide::Sell,
138            Price::from("101.00"),
139            Quantity::from("200"),
140            2,
141        ),
142        flags,
143        sequence,
144        ts_event.into(),
145        ts_init.into(),
146    );
147    let delta3 = OrderBookDelta::new(
148        instrument_id,
149        BookAction::Add,
150        BookOrder::new(
151            OrderSide::Sell,
152            Price::from("100.00"),
153            Quantity::from("100"),
154            3,
155        ),
156        flags,
157        sequence,
158        ts_event.into(),
159        ts_init.into(),
160    );
161    let delta4 = OrderBookDelta::new(
162        instrument_id,
163        BookAction::Add,
164        BookOrder::new(
165            OrderSide::Buy,
166            Price::from("99.00"),
167            Quantity::from("100"),
168            4,
169        ),
170        flags,
171        sequence,
172        ts_event.into(),
173        ts_init.into(),
174    );
175    let delta5 = OrderBookDelta::new(
176        instrument_id,
177        BookAction::Add,
178        BookOrder::new(
179            OrderSide::Buy,
180            Price::from("98.00"),
181            Quantity::from("200"),
182            5,
183        ),
184        flags,
185        sequence,
186        ts_event.into(),
187        ts_init.into(),
188    );
189    let delta6 = OrderBookDelta::new(
190        instrument_id,
191        BookAction::Add,
192        BookOrder::new(
193            OrderSide::Buy,
194            Price::from("97.00"),
195            Quantity::from("300"),
196            6,
197        ),
198        flags,
199        sequence,
200        ts_event.into(),
201        ts_init.into(),
202    );
203
204    let deltas = vec![delta0, delta1, delta2, delta3, delta4, delta5, delta6];
205
206    OrderBookDeltas::new(instrument_id, deltas)
207}
208
209#[fixture]
210pub fn stub_depth10() -> OrderBookDepth10 {
211    let instrument_id = InstrumentId::from("AAPL.XNAS");
212    let flags = 0;
213    let sequence = 0;
214    let ts_event = 1;
215    let ts_init = 2;
216
217    let mut bids: [BookOrder; DEPTH10_LEN] = [BookOrder::default(); DEPTH10_LEN];
218    let mut asks: [BookOrder; DEPTH10_LEN] = [BookOrder::default(); DEPTH10_LEN];
219
220    // Create bids
221    let mut price = 99.00;
222    let mut quantity = 100.0;
223
224    for (i, bid) in bids.iter_mut().enumerate() {
225        *bid = BookOrder::new(
226            OrderSide::Buy,
227            Price::new(price, 2),
228            Quantity::new(quantity, 0),
229            (i + 1) as u64,
230        );
231
232        price -= 1.0;
233        quantity += 100.0;
234    }
235
236    // Create asks
237    let mut price = 100.00;
238    let mut quantity = 100.0;
239
240    for (i, ask) in asks.iter_mut().enumerate() {
241        *ask = BookOrder::new(
242            OrderSide::Sell,
243            Price::new(price, 2),
244            Quantity::new(quantity, 0),
245            (i + 11) as u64,
246        );
247
248        price += 1.0;
249        quantity += 100.0;
250    }
251
252    let bid_counts: [u32; DEPTH10_LEN] = [1; DEPTH10_LEN];
253    let ask_counts: [u32; DEPTH10_LEN] = [1; DEPTH10_LEN];
254
255    OrderBookDepth10::new(
256        instrument_id,
257        bids,
258        asks,
259        bid_counts,
260        ask_counts,
261        flags,
262        sequence,
263        ts_event.into(),
264        ts_init.into(),
265    )
266}
267
268#[fixture]
269pub fn stub_book_order() -> BookOrder {
270    let price = Price::from("100.00");
271    let size = Quantity::from("10");
272    let side = OrderSide::Buy;
273    let order_id = 123_456;
274
275    BookOrder::new(side, price, size, order_id)
276}
277
278#[fixture]
279pub fn quote_ethusdt_binance() -> QuoteTick {
280    QuoteTick {
281        instrument_id: InstrumentId::from("ETHUSDT-PERP.BINANCE"),
282        bid_price: Price::from("10000.0000"),
283        ask_price: Price::from("10001.0000"),
284        bid_size: Quantity::from("1.00000000"),
285        ask_size: Quantity::from("1.00000000"),
286        ts_event: UnixNanos::default(),
287        ts_init: UnixNanos::from(1),
288    }
289}
290
291#[fixture]
292pub fn quote_audusd() -> QuoteTick {
293    QuoteTick {
294        instrument_id: InstrumentId::from("AUD/USD.SIM"),
295        bid_price: Price::from("100.0000"),
296        ask_price: Price::from("101.0000"),
297        bid_size: Quantity::from("1.00000000"),
298        ask_size: Quantity::from("1.00000000"),
299        ts_event: UnixNanos::default(),
300        ts_init: UnixNanos::from(1),
301    }
302}
303
304#[fixture]
305pub fn stub_trade_ethusdt_buyer() -> TradeTick {
306    TradeTick {
307        instrument_id: InstrumentId::from("ETHUSDT-PERP.BINANCE"),
308        price: Price::from("10000.0000"),
309        size: Quantity::from("1.00000000"),
310        aggressor_side: AggressorSide::Buyer,
311        trade_id: TradeId::new("123456789"),
312        ts_event: UnixNanos::default(),
313        ts_init: UnixNanos::from(1),
314    }
315}
316
317#[fixture]
318pub fn stub_bar() -> Bar {
319    let instrument_id = InstrumentId {
320        symbol: Symbol::new("AUD/USD"),
321        venue: Venue::new("SIM"),
322    };
323    let bar_spec = BarSpecification::new(1, BarAggregation::Minute, PriceType::Bid);
324    let bar_type = BarType::Standard {
325        instrument_id,
326        spec: bar_spec,
327        aggregation_source: AggregationSource::External,
328    };
329    Bar {
330        bar_type,
331        open: Price::from("1.00002"),
332        high: Price::from("1.00004"),
333        low: Price::from("1.00001"),
334        close: Price::from("1.00003"),
335        volume: Quantity::from("100000"),
336        ts_event: UnixNanos::default(),
337        ts_init: UnixNanos::from(1),
338    }
339}
340
341#[fixture]
342pub fn stub_instrument_status() -> InstrumentStatus {
343    let instrument_id = InstrumentId::from("MSFT.XNAS");
344    InstrumentStatus::new(
345        instrument_id,
346        MarketStatusAction::Trading,
347        UnixNanos::from(1),
348        UnixNanos::from(2),
349        None,
350        None,
351        None,
352        None,
353        None,
354    )
355}
356
357#[fixture]
358pub fn stub_instrument_close() -> InstrumentClose {
359    let instrument_id = InstrumentId::from("MSFT.XNAS");
360    InstrumentClose::new(
361        instrument_id,
362        Price::from("100.50"),
363        InstrumentCloseType::EndOfSession,
364        UnixNanos::from(1),
365        UnixNanos::from(2),
366    )
367}
368
369#[derive(Debug)]
370pub struct OrderBookDeltaTestBuilder {
371    instrument_id: InstrumentId,
372    action: Option<BookAction>,
373    book_order: Option<BookOrder>,
374    flags: Option<u8>,
375    sequence: Option<u64>,
376    ts_event: Option<UnixNanos>,
377    ts_init: Option<UnixNanos>,
378}
379
380impl OrderBookDeltaTestBuilder {
381    #[must_use]
382    pub fn new(instrument_id: InstrumentId) -> Self {
383        Self {
384            instrument_id,
385            action: None,
386            book_order: None,
387            flags: None,
388            sequence: None,
389            ts_event: None,
390            ts_init: None,
391        }
392    }
393
394    pub fn book_action(&mut self, action: BookAction) -> &mut Self {
395        self.action = Some(action);
396        self
397    }
398
399    fn get_book_action(&self) -> BookAction {
400        self.action.unwrap_or(BookAction::Add)
401    }
402
403    pub fn book_order(&mut self, book_order: BookOrder) -> &mut Self {
404        self.book_order = Some(book_order);
405        self
406    }
407
408    fn get_book_order(&self) -> BookOrder {
409        self.book_order.unwrap_or(BookOrder::new(
410            OrderSide::Sell,
411            Price::from("1500.00"),
412            Quantity::from("1"),
413            1,
414        ))
415    }
416
417    pub fn flags(&mut self, flags: u8) -> &mut Self {
418        self.flags = Some(flags);
419        self
420    }
421
422    fn get_flags(&self) -> u8 {
423        self.flags.unwrap_or(0)
424    }
425
426    pub fn sequence(&mut self, sequence: u64) -> &mut Self {
427        self.sequence = Some(sequence);
428        self
429    }
430
431    fn get_sequence(&self) -> u64 {
432        self.sequence.unwrap_or(1)
433    }
434
435    pub fn ts_event(&mut self, ts_event: UnixNanos) -> &mut Self {
436        self.ts_event = Some(ts_event);
437        self
438    }
439
440    pub fn ts_init(&mut self, ts_init: UnixNanos) -> &mut Self {
441        self.ts_init = Some(ts_init);
442        self
443    }
444
445    #[must_use]
446    pub fn build(&self) -> OrderBookDelta {
447        OrderBookDelta::new(
448            self.instrument_id,
449            self.get_book_action(),
450            self.get_book_order(),
451            self.get_flags(),
452            self.get_sequence(),
453            self.ts_event.unwrap_or(UnixNanos::from(1)),
454            self.ts_init.unwrap_or(UnixNanos::from(2)),
455        )
456    }
457}
458
459/// Stub custom data type for integration tests (e.g. Redis cache).
460#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
461pub struct StubCustomData {
462    pub ts_init: UnixNanos,
463    pub value: i64,
464}
465
466impl HasTsInit for StubCustomData {
467    fn ts_init(&self) -> UnixNanos {
468        self.ts_init
469    }
470}
471
472impl CustomDataTrait for StubCustomData {
473    fn type_name(&self) -> &'static str {
474        "StubCustomData"
475    }
476    fn as_any(&self) -> &dyn std::any::Any {
477        self
478    }
479    fn ts_event(&self) -> UnixNanos {
480        self.ts_init
481    }
482    fn to_json(&self) -> anyhow::Result<String> {
483        Ok(serde_json::to_string(self)?)
484    }
485    fn clone_arc(&self) -> Arc<dyn CustomDataTrait> {
486        Arc::new(self.clone())
487    }
488    fn eq_arc(&self, other: &dyn CustomDataTrait) -> bool {
489        if let Some(o) = other.as_any().downcast_ref::<Self>() {
490            self == o
491        } else {
492            false
493        }
494    }
495
496    fn type_name_static() -> &'static str {
497        "StubCustomData"
498    }
499    fn from_json(value: serde_json::Value) -> anyhow::Result<Arc<dyn CustomDataTrait>> {
500        let parsed: Self = serde_json::from_value(value)?;
501        Ok(Arc::new(parsed))
502    }
503}
504
505/// Registers `StubCustomData` for JSON roundtrip; call once before tests that persist custom data.
506pub fn ensure_stub_custom_data_registered() {
507    let _ = register_custom_data_json::<StubCustomData>();
508}
509
510/// Builds a `CustomData` stub for tests (e.g. Redis add/load).
511#[must_use]
512pub fn stub_custom_data(
513    ts_init: u64,
514    value: i64,
515    metadata: Option<Params>,
516    identifier: Option<String>,
517) -> CustomData {
518    ensure_stub_custom_data_registered();
519    let inner = StubCustomData {
520        ts_init: UnixNanos::from(ts_init),
521        value,
522    };
523    let data_type = DataType::new("StubCustomData", metadata, identifier);
524    CustomData::new(Arc::new(inner), data_type)
525}