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.adapters.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
137#[cfg(feature = "python")]
138nautilus_core::impl_pyo3_config_getters!(TardisDataClientConfig {
139    tardis_ws_url: Option<String>,
140    normalize_symbols: bool,
141    extract_bbo_as_quotes: bool,
142    options: Vec<ReplayNormalizedRequestOptions>,
143    stream_options: Vec<StreamNormalizedRequestOptions>,
144});
145
146impl Default for TardisDataClientConfig {
147    fn default() -> Self {
148        Self::builder().build()
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use rstest::rstest;
155
156    use super::*;
157
158    #[rstest]
159    fn test_default_config_values() {
160        let config = TardisDataClientConfig::default();
161        assert!(config.api_key.is_none());
162        assert!(config.tardis_ws_url.is_none());
163        assert!(config.proxy_url.is_none());
164        assert!(config.normalize_symbols);
165        assert!(matches!(
166            config.book_snapshot_output,
167            BookSnapshotOutput::Deltas
168        ));
169        assert!(!config.extract_bbo_as_quotes);
170        assert!(config.options.is_empty());
171        assert!(config.stream_options.is_empty());
172    }
173
174    #[rstest]
175    fn test_book_snapshot_output_default_is_deltas() {
176        assert!(matches!(
177            BookSnapshotOutput::default(),
178            BookSnapshotOutput::Deltas
179        ));
180    }
181
182    #[rstest]
183    fn test_book_snapshot_output_serde_roundtrip_deltas() {
184        let json = serde_json::to_string(&BookSnapshotOutput::Deltas).unwrap();
185        assert_eq!(json, "\"deltas\"");
186
187        let deserialized: BookSnapshotOutput = serde_json::from_str(&json).unwrap();
188        assert!(matches!(deserialized, BookSnapshotOutput::Deltas));
189    }
190
191    #[rstest]
192    fn test_book_snapshot_output_serde_roundtrip_depth10() {
193        let json = serde_json::to_string(&BookSnapshotOutput::Depth10).unwrap();
194        assert_eq!(json, "\"depth10\"");
195
196        let deserialized: BookSnapshotOutput = serde_json::from_str(&json).unwrap();
197        assert!(matches!(deserialized, BookSnapshotOutput::Depth10));
198    }
199
200    #[rstest]
201    fn test_parquet_compression_default_is_zstd() {
202        assert!(matches!(
203            ParquetCompression::default(),
204            ParquetCompression::Zstd
205        ));
206        assert!(matches!(
207            ParquetCompression::default().as_parquet_compression(),
208            Compression::ZSTD(_)
209        ));
210    }
211
212    #[rstest]
213    fn test_parquet_compression_serde_roundtrip() {
214        let cases = [
215            (ParquetCompression::Zstd, "\"zstd\""),
216            (ParquetCompression::Snappy, "\"snappy\""),
217            (ParquetCompression::Uncompressed, "\"uncompressed\""),
218        ];
219
220        for (compression, expected_json) in cases {
221            let json = serde_json::to_string(&compression).unwrap();
222            assert_eq!(json, expected_json);
223
224            let deserialized: ParquetCompression = serde_json::from_str(&json).unwrap();
225            assert_eq!(
226                compression.as_parquet_compression(),
227                deserialized.as_parquet_compression()
228            );
229        }
230    }
231
232    #[rstest]
233    fn test_data_client_config_toml_minimal() {
234        let config: TardisDataClientConfig = toml::from_str(
235            r#"
236normalize_symbols = false
237book_snapshot_output = "depth10"
238"#,
239        )
240        .unwrap();
241
242        assert!(!config.normalize_symbols);
243        assert!(matches!(
244            config.book_snapshot_output,
245            BookSnapshotOutput::Depth10
246        ));
247        assert!(!config.extract_bbo_as_quotes);
248        assert!(config.options.is_empty());
249    }
250
251    #[rstest]
252    fn test_replay_config_omits_options_uses_empty_default() {
253        let config: TardisReplayConfig = toml::from_str(
254            r#"
255tardis_ws_url = "wss://example.com"
256normalize_symbols = false
257"#,
258        )
259        .unwrap();
260
261        assert!(config.options.is_empty());
262        assert_eq!(config.tardis_ws_url.as_deref(), Some("wss://example.com"));
263        assert_eq!(config.normalize_symbols, Some(false));
264        assert_eq!(config.extract_bbo_as_quotes, None);
265    }
266
267    #[rstest]
268    fn test_replay_config_deserializes_compression() {
269        let json = r#"{
270            "tardis_ws_url": null,
271            "normalize_symbols": true,
272            "output_path": null,
273            "options": [],
274            "proxy_url": null,
275            "book_snapshot_output": "depth10",
276            "extract_bbo_as_quotes": true,
277            "compression": "zstd"
278        }"#;
279
280        let config: TardisReplayConfig = serde_json::from_str(json).unwrap();
281
282        assert!(matches!(
283            config.book_snapshot_output,
284            Some(BookSnapshotOutput::Depth10)
285        ));
286        assert_eq!(config.extract_bbo_as_quotes, Some(true));
287        assert!(matches!(config.compression, Some(ParquetCompression::Zstd)));
288    }
289}