Skip to main content

nautilus_testkit/
common.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::{
17    fs::File,
18    path::{Path, PathBuf},
19    sync::OnceLock,
20};
21
22use nautilus_core::paths::get_test_data_path;
23use nautilus_model::{
24    data::OrderBookDelta,
25    instruments::{InstrumentAny, stubs::equity_aapl_itch},
26};
27use nautilus_serialization::arrow::DecodeFromRecordBatch;
28use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
29
30use crate::files::ensure_file_exists_or_download_http;
31
32/// Returns the full path to the test data file at the specified relative `path` within the standard test data directory.
33///
34/// # Panics
35///
36/// Panics if the computed path cannot be represented as a valid UTF-8 string.
37#[must_use]
38pub fn get_test_data_file_path(path: &str) -> String {
39    get_test_data_path()
40        .join(path)
41        .to_str()
42        .unwrap()
43        .to_string()
44}
45
46/// Returns the full path to the Nautilus-specific test data file given by `filename`, within the configured precision directory ("64-bit" or "128-bit").
47///
48/// # Panics
49///
50/// Panics if the computed path cannot be represented as a valid UTF-8 string.
51#[must_use]
52#[allow(unused_mut)]
53pub fn get_nautilus_test_data_file_path(filename: &str) -> String {
54    let mut path = get_test_data_path().join("nautilus");
55
56    #[cfg(feature = "high-precision")]
57    {
58        path = path.join("128-bit");
59    }
60    #[cfg(not(feature = "high-precision"))]
61    {
62        path = path.join("64-bit");
63    }
64
65    path.join(filename).to_str().unwrap().to_string()
66}
67
68/// Returns the path to the checksums file for large test data files.
69#[must_use]
70pub fn get_test_data_large_checksums_filepath() -> PathBuf {
71    get_test_data_path().join("large").join("checksums.json")
72}
73
74/// Ensures that the specified test data file exists locally by downloading it if necessary, using the provided `url`.
75///
76/// # Panics
77///
78/// Panics if the download or checksum verification fails, or if the resulting path cannot be represented as a valid UTF-8 string.
79#[must_use]
80pub fn ensure_test_data_exists(filename: &str, url: &str) -> PathBuf {
81    let filepath = get_test_data_path().join("large").join(filename);
82    let checksums_filepath = get_test_data_large_checksums_filepath();
83    ensure_file_exists_or_download_http(&filepath, url, Some(&checksums_filepath), None).unwrap();
84    filepath
85}
86
87/// Ensures the NASDAQ ITCH AAPL deltas Parquet file exists locally, downloading from R2 if necessary.
88///
89/// # Panics
90///
91/// Panics if the download or checksum verification fails.
92#[must_use]
93pub fn ensure_itch_aapl_deltas_parquet() -> PathBuf {
94    ensure_test_data_exists(
95        "itch_AAPL.XNAS_2019-01-30_deltas.parquet",
96        "https://test-data.nautechsystems.io/large/itch_AAPL.XNAS_2019-01-30_deltas.parquet",
97    )
98}
99
100/// Ensures the Tardis Deribit BTC-PERPETUAL deltas Parquet file exists locally, downloading from R2 if necessary.
101///
102/// # Panics
103///
104/// Panics if the download or checksum verification fails.
105#[must_use]
106pub fn ensure_tardis_deribit_deltas_parquet() -> PathBuf {
107    ensure_test_data_exists(
108        "tardis_BTC-PERPETUAL.DERIBIT_2020-04-01_deltas.parquet",
109        "https://test-data.nautechsystems.io/large/tardis_BTC-PERPETUAL.DERIBIT_2020-04-01_deltas.parquet",
110    )
111}
112
113/// Returns the path to the Tardis Deribit incremental book L2 test data.
114#[must_use]
115pub fn get_tardis_deribit_book_l2_path() -> PathBuf {
116    get_test_data_path()
117        .join("tardis")
118        .join("deribit_incremental_book_L2_BTC-PERPETUAL.csv")
119}
120
121/// Returns the path to the Tardis Binance Futures book snapshot (depth 5) test data.
122#[must_use]
123pub fn get_tardis_binance_snapshot5_path() -> PathBuf {
124    get_test_data_path()
125        .join("tardis")
126        .join("binance-futures_book_snapshot_5_BTCUSDT.csv")
127}
128
129/// Returns the path to the Tardis Binance Futures book snapshot (depth 25) test data.
130#[must_use]
131pub fn get_tardis_binance_snapshot25_path() -> PathBuf {
132    get_test_data_path()
133        .join("tardis")
134        .join("binance-futures_book_snapshot_25_BTCUSDT.csv")
135}
136
137/// Returns the path to the Tardis Huobi quotes test data.
138#[must_use]
139pub fn get_tardis_huobi_quotes_path() -> PathBuf {
140    get_test_data_path()
141        .join("tardis")
142        .join("huobi-dm-swap_quotes_BTC-USD.csv")
143}
144
145/// Returns the path to the Tardis Bitmex trades test data.
146#[must_use]
147pub fn get_tardis_bitmex_trades_path() -> PathBuf {
148    get_test_data_path()
149        .join("tardis")
150        .join("bitmex_trades_XBTUSD.csv")
151}
152
153/// Returns an AAPL equity instrument with ITCH-compatible precision
154/// (price_precision=4, price_increment=0.0001).
155#[must_use]
156pub fn itch_aapl_equity() -> InstrumentAny {
157    InstrumentAny::Equity(equity_aapl_itch())
158}
159
160/// Loads ITCH AAPL order book deltas from the parquet test dataset.
161///
162/// Downloads the file on first access. Pass `limit` to subsample.
163#[must_use]
164pub fn load_itch_aapl_deltas(limit: Option<usize>) -> Vec<OrderBookDelta> {
165    static PATH: OnceLock<PathBuf> = OnceLock::new();
166    let filepath = PATH.get_or_init(ensure_itch_aapl_deltas_parquet);
167    load_deltas_from_parquet(filepath, limit)
168}
169
170/// Loads Tardis Deribit BTC-PERPETUAL order book deltas from the parquet test dataset.
171///
172/// Downloads the file on first access. Pass `limit` to subsample.
173#[must_use]
174pub fn load_tardis_deribit_deltas(limit: Option<usize>) -> Vec<OrderBookDelta> {
175    static PATH: OnceLock<PathBuf> = OnceLock::new();
176    let filepath = PATH.get_or_init(ensure_tardis_deribit_deltas_parquet);
177    load_deltas_from_parquet(filepath, limit)
178}
179
180fn load_deltas_from_parquet(filepath: &Path, limit: Option<usize>) -> Vec<OrderBookDelta> {
181    let file = File::open(filepath).unwrap();
182    let mut builder = ParquetRecordBatchReaderBuilder::try_new(file).unwrap();
183    let metadata = builder.schema().metadata().clone();
184
185    if let Some(limit) = limit {
186        builder = builder.with_limit(limit);
187    }
188    let reader = builder.build().unwrap();
189
190    let mut deltas = Vec::new();
191    for batch_result in reader {
192        let batch = batch_result.unwrap();
193        let batch_deltas = OrderBookDelta::decode_batch(&metadata, batch).unwrap();
194        deltas.extend(batch_deltas);
195    }
196    deltas
197}