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    types::fixed::PRECISION_BYTES,
27};
28use nautilus_serialization::arrow::DecodeFromRecordBatch;
29use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
30
31/// Returns the full path to the test data file at the specified relative `path` within the standard test data directory.
32///
33/// # Panics
34///
35/// Panics if the computed path cannot be represented as a valid UTF-8 string.
36#[must_use]
37pub fn get_test_data_file_path(path: &str) -> String {
38    get_test_data_path()
39        .join(path)
40        .to_str()
41        .unwrap()
42        .to_string()
43}
44
45/// Returns the full path to the Nautilus-specific test data file given by `filename`, within the configured precision directory ("64-bit" or "128-bit").
46///
47/// # Panics
48///
49/// Panics if the computed path cannot be represented as a valid UTF-8 string.
50#[must_use]
51pub fn get_nautilus_test_data_file_path(filename: &str) -> String {
52    let precision_directory = format!("{}-bit", PRECISION_BYTES * 8);
53    let path = get_test_data_path()
54        .join("nautilus")
55        .join(precision_directory);
56
57    path.join(filename).to_str().unwrap().to_string()
58}
59
60/// Returns the path to the checksums file for large test data files.
61#[must_use]
62pub fn get_test_data_large_checksums_filepath() -> PathBuf {
63    get_test_data_path().join("large").join("checksums.json")
64}
65
66/// Returns the path to a large test data file that is already present locally.
67///
68/// # Panics
69///
70/// Panics if the file is missing, with the command to prepare test data.
71#[must_use]
72pub fn ensure_test_data_exists(filename: &str) -> PathBuf {
73    let filepath = get_test_data_path().join("large").join(filename);
74    assert!(
75        filepath.is_file(),
76        "Missing test data file: {}. Run `cargo run --locked -p nautilus-testkit --bin prepare-test-data` before testing.",
77        filepath.display(),
78    );
79    filepath
80}
81
82/// Returns the path to the local NASDAQ ITCH AAPL deltas Parquet file.
83///
84/// # Panics
85///
86/// Panics if the file is missing, with the command to prepare test data.
87#[must_use]
88pub fn ensure_itch_aapl_deltas_parquet() -> PathBuf {
89    ensure_test_data_exists("itch_AAPL.XNAS_2019-01-30_deltas.parquet")
90}
91
92/// Returns the path to the local Tardis Deribit BTC-PERPETUAL deltas Parquet file.
93///
94/// # Panics
95///
96/// Panics if the file is missing, with the command to prepare test data.
97#[must_use]
98pub fn ensure_tardis_deribit_deltas_parquet() -> PathBuf {
99    ensure_test_data_exists("tardis_BTC-PERPETUAL.DERIBIT_2020-04-01_deltas.parquet")
100}
101
102/// Returns the path to the local HISTDATA EURUSD.SIM quotes Parquet file.
103///
104/// # Panics
105///
106/// Panics if the file is missing, with the command to prepare test data.
107#[must_use]
108pub fn ensure_histdata_eurusd_quotes_parquet() -> PathBuf {
109    ensure_test_data_exists("histdata_EURUSD.SIM_2020-01_quotes.parquet")
110}
111
112/// Returns the path to the local HISTDATA EURUSD.SIM instrument Parquet file.
113///
114/// # Panics
115///
116/// Panics if the file is missing, with the command to prepare test data.
117#[must_use]
118pub fn ensure_histdata_eurusd_instrument_parquet() -> PathBuf {
119    ensure_test_data_exists("histdata_EURUSD.SIM_2020-01_instrument.parquet")
120}
121
122/// Returns the path to the Tardis Deribit incremental book L2 test data.
123#[must_use]
124pub fn get_tardis_deribit_book_l2_path() -> PathBuf {
125    get_test_data_path()
126        .join("tardis")
127        .join("deribit_incremental_book_L2_BTC-PERPETUAL.csv")
128}
129
130/// Returns the path to the Tardis Binance Futures book snapshot (depth 5) test data.
131#[must_use]
132pub fn get_tardis_binance_snapshot5_path() -> PathBuf {
133    get_test_data_path()
134        .join("tardis")
135        .join("binance-futures_book_snapshot_5_BTCUSDT.csv")
136}
137
138/// Returns the path to the Tardis Binance Futures book snapshot (depth 25) test data.
139#[must_use]
140pub fn get_tardis_binance_snapshot25_path() -> PathBuf {
141    get_test_data_path()
142        .join("tardis")
143        .join("binance-futures_book_snapshot_25_BTCUSDT.csv")
144}
145
146/// Returns the path to the Tardis Huobi quotes test data.
147#[must_use]
148pub fn get_tardis_huobi_quotes_path() -> PathBuf {
149    get_test_data_path()
150        .join("tardis")
151        .join("huobi-dm-swap_quotes_BTC-USD.csv")
152}
153
154/// Returns the path to the Tardis Bitmex trades test data.
155#[must_use]
156pub fn get_tardis_bitmex_trades_path() -> PathBuf {
157    get_test_data_path()
158        .join("tardis")
159        .join("bitmex_trades_XBTUSD.csv")
160}
161
162/// Returns an AAPL equity instrument with ITCH-compatible precision
163/// (`price_precision=4`, `price_increment=0.0001`).
164#[must_use]
165pub fn itch_aapl_equity() -> InstrumentAny {
166    InstrumentAny::Equity(equity_aapl_itch())
167}
168
169/// Loads ITCH AAPL order book deltas from the parquet test dataset.
170///
171/// Requires prepared local test data. Pass `limit` to subsample.
172#[must_use]
173pub fn load_itch_aapl_deltas(limit: Option<usize>) -> Vec<OrderBookDelta> {
174    static PATH: OnceLock<PathBuf> = OnceLock::new();
175    let filepath = PATH.get_or_init(ensure_itch_aapl_deltas_parquet);
176    load_deltas_from_parquet(filepath, limit)
177}
178
179/// Loads Tardis Deribit BTC-PERPETUAL order book deltas from the parquet test dataset.
180///
181/// Requires prepared local test data. Pass `limit` to subsample.
182#[must_use]
183pub fn load_tardis_deribit_deltas(limit: Option<usize>) -> Vec<OrderBookDelta> {
184    static PATH: OnceLock<PathBuf> = OnceLock::new();
185    let filepath = PATH.get_or_init(ensure_tardis_deribit_deltas_parquet);
186    load_deltas_from_parquet(filepath, limit)
187}
188
189fn load_deltas_from_parquet(filepath: &Path, limit: Option<usize>) -> Vec<OrderBookDelta> {
190    let file = File::open(filepath).unwrap();
191    let mut builder = ParquetRecordBatchReaderBuilder::try_new(file).unwrap();
192    let metadata = builder.schema().metadata().clone();
193
194    if let Some(limit) = limit {
195        builder = builder.with_limit(limit);
196    }
197    let reader = builder.build().unwrap();
198
199    let mut deltas = Vec::new();
200
201    for batch_result in reader {
202        let batch = batch_result.unwrap();
203        let batch_deltas = OrderBookDelta::decode_batch(&metadata, batch).unwrap();
204        deltas.extend(batch_deltas);
205    }
206    deltas
207}
208
209#[cfg(test)]
210mod tests {
211    use rstest::rstest;
212    use tempfile::TempDir;
213
214    use super::*;
215
216    #[rstest]
217    #[case::file("file")]
218    #[case::missing("missing")]
219    #[case::directory("directory")]
220    fn test_ensure_test_data_exists(#[case] state: &str) {
221        let directory = TempDir::new().unwrap();
222        let filepath = directory.path().join("fixture.parquet");
223        if state == "file" {
224            std::fs::write(&filepath, "local fixture").unwrap();
225        } else if state == "directory" {
226            std::fs::create_dir(&filepath).unwrap();
227        }
228
229        // The absolute path isolates this test without changing the shared test data root
230        let result =
231            std::panic::catch_unwind(|| ensure_test_data_exists(filepath.to_str().unwrap()));
232
233        if state == "file" {
234            assert_eq!(result.unwrap(), filepath);
235            assert_eq!(std::fs::read_to_string(&filepath).unwrap(), "local fixture");
236        } else {
237            let panic = result.unwrap_err().downcast::<String>().unwrap();
238            assert_eq!(
239                *panic,
240                format!(
241                    "Missing test data file: {}. Run `cargo run --locked -p nautilus-testkit --bin prepare-test-data` before testing.",
242                    filepath.display(),
243                ),
244            );
245            assert_eq!(filepath.exists(), state == "directory");
246        }
247    }
248}