Skip to main content

nautilus_tardis/
config.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 parquet::basic::{Compression, ZstdLevel};
17use serde::{Deserialize, Serialize};
18
19use super::machine::types::{ReplayNormalizedRequestOptions, StreamNormalizedRequestOptions};
20
21/// Determines the output format for Tardis `book_snapshot_*` messages.
22#[derive(Debug, Clone, Default, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum BookSnapshotOutput {
25    /// Convert book snapshots to `OrderBookDeltas` and write to `order_book_deltas/`.
26    #[default]
27    Deltas,
28    /// Convert book snapshots to `OrderBookDepth10` and write to `order_book_depths/`.
29    Depth10,
30}
31
32/// Determines the compression codec for Parquet files written by Tardis replay.
33#[derive(Debug, Clone, Default, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35pub enum ParquetCompression {
36    /// Use Zstandard compression with level 3.
37    #[default]
38    Zstd,
39    /// Use Snappy compression.
40    Snappy,
41    /// Write uncompressed Parquet files.
42    Uncompressed,
43}
44
45impl ParquetCompression {
46    /// Converts the replay config compression value to a Parquet compression value.
47    ///
48    /// # Panics
49    ///
50    /// Panics if the hard-coded Zstandard level 3 is rejected by the Parquet crate.
51    #[must_use]
52    pub fn as_parquet_compression(&self) -> Compression {
53        match self {
54            Self::Zstd => {
55                let level = ZstdLevel::try_new(3).expect("zstd level 3 is valid");
56                Compression::ZSTD(level)
57            }
58            Self::Snappy => Compression::SNAPPY,
59            Self::Uncompressed => Compression::UNCOMPRESSED,
60        }
61    }
62}
63
64/// Provides a configuration for a Tardis Machine -> Nautilus data -> Parquet replay run.
65#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
66#[serde(deny_unknown_fields)]
67pub struct TardisReplayConfig {
68    /// The Tardis Machine websocket url.
69    pub tardis_ws_url: Option<String>,
70    /// If symbols should be normalized with Nautilus conventions.
71    pub normalize_symbols: Option<bool>,
72    /// The output directory for writing Nautilus format Parquet files.
73    pub output_path: Option<String>,
74    /// The Tardis Machine replay options.
75    #[builder(default)]
76    #[serde(default)]
77    pub options: Vec<ReplayNormalizedRequestOptions>,
78    /// Optional proxy URL for the Tardis HTTP API client.
79    /// The Tardis Machine WebSocket transport does not yet support proxying.
80    pub proxy_url: Option<String>,
81    /// The output format for `book_snapshot_*` messages.
82    ///
83    /// - `deltas`: Convert to `OrderBookDeltas` and write to `order_book_deltas/` (default).
84    /// - `depth10`: Convert to `OrderBookDepth10` and write to `order_book_depths/`.
85    pub book_snapshot_output: Option<BookSnapshotOutput>,
86    /// If best bid/offer fields from Tardis `option_summary` messages should emit `QuoteTick`.
87    pub extract_bbo_as_quotes: Option<bool>,
88    /// The compression codec for written data files.
89    ///
90    /// - `zstd`: Use Zstandard compression level 3 (default).
91    /// - `snappy`: Use Snappy compression.
92    /// - `uncompressed`: Write uncompressed Parquet files.
93    pub compression: Option<ParquetCompression>,
94}
95
96/// Configuration for the Tardis data client.
97#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
98#[serde(default, deny_unknown_fields)]
99#[cfg_attr(
100    feature = "python",
101    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.tardis", from_py_object)
102)]
103#[cfg_attr(
104    feature = "python",
105    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.tardis")
106)]
107pub struct TardisDataClientConfig {
108    /// Tardis API key for HTTP instrument fetching.
109    /// Falls back to `TARDIS_API_KEY` env var if not set.
110    pub api_key: Option<String>,
111    /// Tardis Machine Server WebSocket URL.
112    /// Falls back to `TARDIS_MACHINE_WS_URL` env var if not set.
113    pub tardis_ws_url: Option<String>,
114    /// Optional proxy URL for the Tardis HTTP API client.
115    /// The Tardis Machine WebSocket transport does not yet support proxying.
116    pub proxy_url: Option<String>,
117    /// Whether to normalize symbols to Nautilus conventions.
118    #[builder(default = true)]
119    pub normalize_symbols: bool,
120    /// Output format for `book_snapshot_*` messages.
121    #[builder(default)]
122    pub book_snapshot_output: BookSnapshotOutput,
123    /// Whether to emit `QuoteTick` from Tardis `option_summary` best bid/offer fields.
124    #[builder(default)]
125    pub extract_bbo_as_quotes: bool,
126    /// Replay options defining exchanges, symbols, date ranges, and data types.
127    /// When non-empty the client connects to `ws-replay-normalized`.
128    #[builder(default)]
129    pub options: Vec<ReplayNormalizedRequestOptions>,
130    /// Live stream options defining exchanges, symbols, and data types.
131    /// When non-empty (and `options` is empty) the client connects to
132    /// `ws-stream-normalized` with automatic reconnection.
133    #[builder(default)]
134    pub stream_options: Vec<StreamNormalizedRequestOptions>,
135}
136
137impl Default for TardisDataClientConfig {
138    fn default() -> Self {
139        Self::builder().build()
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use rstest::rstest;
146
147    use super::*;
148
149    #[rstest]
150    fn test_default_config_values() {
151        let config = TardisDataClientConfig::default();
152        assert!(config.api_key.is_none());
153        assert!(config.tardis_ws_url.is_none());
154        assert!(config.proxy_url.is_none());
155        assert!(config.normalize_symbols);
156        assert!(matches!(
157            config.book_snapshot_output,
158            BookSnapshotOutput::Deltas
159        ));
160        assert!(!config.extract_bbo_as_quotes);
161        assert!(config.options.is_empty());
162        assert!(config.stream_options.is_empty());
163    }
164
165    #[rstest]
166    fn test_book_snapshot_output_default_is_deltas() {
167        assert!(matches!(
168            BookSnapshotOutput::default(),
169            BookSnapshotOutput::Deltas
170        ));
171    }
172
173    #[rstest]
174    fn test_book_snapshot_output_serde_roundtrip_deltas() {
175        let json = serde_json::to_string(&BookSnapshotOutput::Deltas).unwrap();
176        assert_eq!(json, "\"deltas\"");
177
178        let deserialized: BookSnapshotOutput = serde_json::from_str(&json).unwrap();
179        assert!(matches!(deserialized, BookSnapshotOutput::Deltas));
180    }
181
182    #[rstest]
183    fn test_book_snapshot_output_serde_roundtrip_depth10() {
184        let json = serde_json::to_string(&BookSnapshotOutput::Depth10).unwrap();
185        assert_eq!(json, "\"depth10\"");
186
187        let deserialized: BookSnapshotOutput = serde_json::from_str(&json).unwrap();
188        assert!(matches!(deserialized, BookSnapshotOutput::Depth10));
189    }
190
191    #[rstest]
192    fn test_parquet_compression_default_is_zstd() {
193        assert!(matches!(
194            ParquetCompression::default(),
195            ParquetCompression::Zstd
196        ));
197        assert!(matches!(
198            ParquetCompression::default().as_parquet_compression(),
199            Compression::ZSTD(_)
200        ));
201    }
202
203    #[rstest]
204    fn test_parquet_compression_serde_roundtrip() {
205        let cases = [
206            (ParquetCompression::Zstd, "\"zstd\""),
207            (ParquetCompression::Snappy, "\"snappy\""),
208            (ParquetCompression::Uncompressed, "\"uncompressed\""),
209        ];
210
211        for (compression, expected_json) in cases {
212            let json = serde_json::to_string(&compression).unwrap();
213            assert_eq!(json, expected_json);
214
215            let deserialized: ParquetCompression = serde_json::from_str(&json).unwrap();
216            assert_eq!(
217                compression.as_parquet_compression(),
218                deserialized.as_parquet_compression()
219            );
220        }
221    }
222
223    #[rstest]
224    fn test_data_client_config_toml_minimal() {
225        let config: TardisDataClientConfig = toml::from_str(
226            r#"
227normalize_symbols = false
228book_snapshot_output = "depth10"
229"#,
230        )
231        .unwrap();
232
233        assert!(!config.normalize_symbols);
234        assert!(matches!(
235            config.book_snapshot_output,
236            BookSnapshotOutput::Depth10
237        ));
238        assert!(!config.extract_bbo_as_quotes);
239        assert!(config.options.is_empty());
240    }
241
242    #[rstest]
243    fn test_replay_config_omits_options_uses_empty_default() {
244        let config: TardisReplayConfig = toml::from_str(
245            r#"
246tardis_ws_url = "wss://example.com"
247normalize_symbols = false
248"#,
249        )
250        .unwrap();
251
252        assert!(config.options.is_empty());
253        assert_eq!(config.tardis_ws_url.as_deref(), Some("wss://example.com"));
254        assert_eq!(config.normalize_symbols, Some(false));
255        assert_eq!(config.extract_bbo_as_quotes, None);
256    }
257
258    #[rstest]
259    fn test_replay_config_deserializes_compression() {
260        let json = r#"{
261            "tardis_ws_url": null,
262            "normalize_symbols": true,
263            "output_path": null,
264            "options": [],
265            "proxy_url": null,
266            "book_snapshot_output": "depth10",
267            "extract_bbo_as_quotes": true,
268            "compression": "zstd"
269        }"#;
270
271        let config: TardisReplayConfig = serde_json::from_str(json).unwrap();
272
273        assert!(matches!(
274            config.book_snapshot_output,
275            Some(BookSnapshotOutput::Depth10)
276        ));
277        assert_eq!(config.extract_bbo_as_quotes, Some(true));
278        assert!(matches!(config.compression, Some(ParquetCompression::Zstd)));
279    }
280}