nautilus_persistence/python/catalog.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::collections::HashMap;
17
18use nautilus_core::{UnixNanos, python::to_pytype_err};
19use nautilus_model::{
20 data::{
21 Bar, Data, IndexPriceUpdate, InstrumentStatus, MarkPriceUpdate, OptionGreeks,
22 OrderBookDelta, OrderBookDepth10, QuoteTick, TradeTick, close::InstrumentClose,
23 },
24 python::instruments::{instrument_any_to_pyobject, pyobject_to_instrument_any},
25};
26use pyo3::{exceptions::PyIOError, prelude::*, types::PyList};
27
28use crate::backend::catalog::ParquetDataCatalog;
29
30/// Converts a single `Data` variant into a Python object for returning from catalog methods.
31#[allow(
32 clippy::match_wildcard_for_single_variants,
33 reason = "Data::Defi appears through nautilus-model feature unification"
34)]
35fn data_to_pyobject(py: Python<'_>, item: Data) -> PyResult<Py<PyAny>> {
36 match item {
37 Data::Quote(quote) => Py::new(py, quote).map(pyo3::Py::into_any),
38 Data::Trade(trade) => Py::new(py, trade).map(pyo3::Py::into_any),
39 Data::Bar(bar) => Py::new(py, bar).map(pyo3::Py::into_any),
40 Data::Delta(delta) => Py::new(py, delta).map(pyo3::Py::into_any),
41 Data::Deltas(deltas) => Py::new(py, (*deltas).clone()).map(pyo3::Py::into_any),
42 Data::Depth10(depth) => Py::new(py, *depth).map(pyo3::Py::into_any),
43 Data::IndexPriceUpdate(price) => Py::new(py, price).map(pyo3::Py::into_any),
44 Data::MarkPriceUpdate(price) => Py::new(py, price).map(pyo3::Py::into_any),
45 Data::FundingRateUpdate(funding) => Py::new(py, funding).map(pyo3::Py::into_any),
46 Data::OptionGreeks(greeks) => Py::new(py, greeks).map(pyo3::Py::into_any),
47 Data::InstrumentStatus(status) => Py::new(py, status).map(pyo3::Py::into_any),
48 Data::InstrumentClose(close) => Py::new(py, close).map(pyo3::Py::into_any),
49 Data::Custom(custom) => Py::new(py, custom).map(pyo3::Py::into_any),
50 #[cfg(feature = "defi")]
51 Data::Defi(_) => Err(to_pytype_err("Unsupported Data::Defi variant")),
52 #[allow(unreachable_patterns)]
53 _ => Err(to_pytype_err("Unsupported Data variant")),
54 }
55}
56
57/// A catalog for writing data to Parquet files.
58#[pyclass(
59 name = "ParquetDataCatalog",
60 module = "nautilus_trader.core.nautilus_pyo3.persistence"
61)]
62#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.persistence")]
63pub struct PyParquetDataCatalog {
64 inner: ParquetDataCatalog,
65}
66
67#[pymethods]
68#[pyo3_stub_gen::derive::gen_stub_pymethods]
69impl PyParquetDataCatalog {
70 /// Create a new `ParquetCatalog` with the given base path and optional parameters.
71 ///
72 /// # Parameters
73 ///
74 /// - `base_path`: The base path for the catalog
75 /// - `storage_options`: Optional storage configuration for cloud backends
76 /// - `batch_size`: Optional batch size for processing (default: 5000)
77 /// - `compression`: Optional compression type (0=UNCOMPRESSED, 1=SNAPPY, 2=GZIP, 3=LZO, 4=BROTLI, 5=LZ4, 6=ZSTD)
78 /// - `max_row_group_size`: Optional maximum row group size (default: 5000)
79 ///
80 /// # Panics
81 ///
82 /// Panics if the underlying [`ParquetDataCatalog`] cannot be created.
83 #[new]
84 #[pyo3(signature = (base_path, storage_options=None, batch_size=None, compression=None, max_row_group_size=None))]
85 #[must_use]
86 pub fn new(
87 base_path: &str,
88 storage_options: Option<HashMap<String, String>>,
89 batch_size: Option<usize>,
90 compression: Option<u8>,
91 max_row_group_size: Option<usize>,
92 ) -> Self {
93 let compression = compression.map(|c| match c {
94 0 => parquet::basic::Compression::UNCOMPRESSED,
95 // For GZIP, LZO, BROTLI, LZ4, ZSTD we need to use the default level
96 // since we can't pass the level parameter through PyO3
97 2 => {
98 let level = parquet::basic::GzipLevel::default();
99 parquet::basic::Compression::GZIP(level)
100 }
101 3 => parquet::basic::Compression::LZO,
102 4 => {
103 let level = parquet::basic::BrotliLevel::default();
104 parquet::basic::Compression::BROTLI(level)
105 }
106 5 => parquet::basic::Compression::LZ4,
107 6 => {
108 let level = parquet::basic::ZstdLevel::default();
109 parquet::basic::Compression::ZSTD(level)
110 }
111 _ => parquet::basic::Compression::SNAPPY,
112 });
113
114 // Convert HashMap to AHashMap for internal use
115 let storage_options = storage_options.map(|m| m.into_iter().collect());
116
117 Self {
118 inner: ParquetDataCatalog::from_uri(
119 base_path,
120 storage_options,
121 batch_size,
122 compression,
123 max_row_group_size,
124 )
125 .expect("Failed to create ParquetDataCatalog"),
126 }
127 }
128
129 // TODO: Cannot pass mixed data across pyo3 as a single type
130 // pub fn write_data(mut slf: PyRefMut<'_, Self>, data_type: NautilusDataType, data: Vec<Data>) {}
131
132 /// Write quote tick data to Parquet files.
133 ///
134 /// # Parameters
135 ///
136 /// - `data`: Vector of quote ticks to write
137 /// - `start`: Optional start timestamp override (nanoseconds since Unix epoch)
138 /// - `end`: Optional end timestamp override (nanoseconds since Unix epoch)
139 ///
140 /// # Returns
141 ///
142 /// Returns the path of the created file as a string.
143 #[pyo3(signature = (data, start=None, end=None, skip_disjoint_check=false))]
144 pub fn write_quote_ticks(
145 &self,
146 data: Vec<QuoteTick>,
147 start: Option<u64>,
148 end: Option<u64>,
149 skip_disjoint_check: bool,
150 ) -> PyResult<String> {
151 let start_nanos = start.map(UnixNanos::from);
152 let end_nanos = end.map(UnixNanos::from);
153 let data = data.into_boxed_slice();
154
155 self.inner
156 .write_to_parquet(
157 data.as_ref(),
158 start_nanos,
159 end_nanos,
160 Some(skip_disjoint_check),
161 )
162 .map(|path| path.to_string_lossy().to_string())
163 .map_err(|e| PyIOError::new_err(format!("Failed to write quote ticks: {e}")))
164 }
165
166 /// Write trade tick data to Parquet files.
167 ///
168 /// # Parameters
169 ///
170 /// - `data`: Vector of trade ticks to write
171 /// - `start`: Optional start timestamp override (nanoseconds since Unix epoch)
172 /// - `end`: Optional end timestamp override (nanoseconds since Unix epoch)
173 ///
174 /// # Returns
175 ///
176 /// Returns the path of the created file as a string.
177 #[pyo3(signature = (data, start=None, end=None, skip_disjoint_check=false))]
178 pub fn write_trade_ticks(
179 &self,
180 data: Vec<TradeTick>,
181 start: Option<u64>,
182 end: Option<u64>,
183 skip_disjoint_check: bool,
184 ) -> PyResult<String> {
185 let start_nanos = start.map(UnixNanos::from);
186 let end_nanos = end.map(UnixNanos::from);
187 let data = data.into_boxed_slice();
188
189 self.inner
190 .write_to_parquet(
191 data.as_ref(),
192 start_nanos,
193 end_nanos,
194 Some(skip_disjoint_check),
195 )
196 .map(|path| path.to_string_lossy().to_string())
197 .map_err(|e| PyIOError::new_err(format!("Failed to write trade ticks: {e}")))
198 }
199
200 /// Write order book delta data to Parquet files.
201 ///
202 /// # Parameters
203 ///
204 /// - `data`: Vector of order book deltas to write
205 /// - `start`: Optional start timestamp override (nanoseconds since Unix epoch)
206 /// - `end`: Optional end timestamp override (nanoseconds since Unix epoch)
207 ///
208 /// # Returns
209 ///
210 /// Returns the path of the created file as a string.
211 #[pyo3(signature = (data, start=None, end=None, skip_disjoint_check=false))]
212 pub fn write_order_book_deltas(
213 &self,
214 data: Vec<OrderBookDelta>,
215 start: Option<u64>,
216 end: Option<u64>,
217 skip_disjoint_check: bool,
218 ) -> PyResult<String> {
219 let start_nanos = start.map(UnixNanos::from);
220 let end_nanos = end.map(UnixNanos::from);
221 let data = data.into_boxed_slice();
222
223 self.inner
224 .write_to_parquet(
225 data.as_ref(),
226 start_nanos,
227 end_nanos,
228 Some(skip_disjoint_check),
229 )
230 .map(|path| path.to_string_lossy().to_string())
231 .map_err(|e| PyIOError::new_err(format!("Failed to write order book deltas: {e}")))
232 }
233
234 /// Write bar data to Parquet files.
235 ///
236 /// # Parameters
237 ///
238 /// - `data`: Vector of bars to write
239 /// - `start`: Optional start timestamp override (nanoseconds since Unix epoch)
240 /// - `end`: Optional end timestamp override (nanoseconds since Unix epoch)
241 ///
242 /// # Returns
243 ///
244 /// Returns the path of the created file as a string.
245 #[pyo3(signature = (data, start=None, end=None, skip_disjoint_check=false))]
246 pub fn write_bars(
247 &self,
248 data: Vec<Bar>,
249 start: Option<u64>,
250 end: Option<u64>,
251 skip_disjoint_check: bool,
252 ) -> PyResult<String> {
253 let start_nanos = start.map(UnixNanos::from);
254 let end_nanos = end.map(UnixNanos::from);
255 let data = data.into_boxed_slice();
256
257 self.inner
258 .write_to_parquet(
259 data.as_ref(),
260 start_nanos,
261 end_nanos,
262 Some(skip_disjoint_check),
263 )
264 .map(|path| path.to_string_lossy().to_string())
265 .map_err(|e| PyIOError::new_err(format!("Failed to write bars: {e}")))
266 }
267
268 /// Write order book depth data to Parquet files.
269 ///
270 /// # Parameters
271 ///
272 /// - `data`: Vector of order book depths to write
273 /// - `start`: Optional start timestamp override (nanoseconds since Unix epoch)
274 /// - `end`: Optional end timestamp override (nanoseconds since Unix epoch)
275 ///
276 /// # Returns
277 ///
278 /// Returns the path of the created file as a string.
279 #[pyo3(signature = (data, start=None, end=None, skip_disjoint_check=false))]
280 pub fn write_order_book_depths(
281 &self,
282 data: Vec<OrderBookDepth10>,
283 start: Option<u64>,
284 end: Option<u64>,
285 skip_disjoint_check: bool,
286 ) -> PyResult<String> {
287 let start_nanos = start.map(UnixNanos::from);
288 let end_nanos = end.map(UnixNanos::from);
289 let data = data.into_boxed_slice();
290
291 self.inner
292 .write_to_parquet(
293 data.as_ref(),
294 start_nanos,
295 end_nanos,
296 Some(skip_disjoint_check),
297 )
298 .map(|path| path.to_string_lossy().to_string())
299 .map_err(|e| PyIOError::new_err(format!("Failed to write order book depths: {e}")))
300 }
301
302 /// Write mark price update data to Parquet files.
303 ///
304 /// # Parameters
305 ///
306 /// - `data`: Vector of mark price updates to write
307 /// - `start`: Optional start timestamp override (nanoseconds since Unix epoch)
308 /// - `end`: Optional end timestamp override (nanoseconds since Unix epoch)
309 ///
310 /// # Returns
311 ///
312 /// Returns the path of the created file as a string.
313 #[pyo3(signature = (data, start=None, end=None, skip_disjoint_check=false))]
314 pub fn write_mark_price_updates(
315 &self,
316 data: Vec<MarkPriceUpdate>,
317 start: Option<u64>,
318 end: Option<u64>,
319 skip_disjoint_check: bool,
320 ) -> PyResult<String> {
321 let start_nanos = start.map(UnixNanos::from);
322 let end_nanos = end.map(UnixNanos::from);
323 let data = data.into_boxed_slice();
324
325 self.inner
326 .write_to_parquet(
327 data.as_ref(),
328 start_nanos,
329 end_nanos,
330 Some(skip_disjoint_check),
331 )
332 .map(|path| path.to_string_lossy().to_string())
333 .map_err(|e| PyIOError::new_err(format!("Failed to write mark price updates: {e}")))
334 }
335
336 /// Write index price update data to Parquet files.
337 ///
338 /// # Parameters
339 ///
340 /// - `data`: Vector of index price updates to write
341 /// - `start`: Optional start timestamp override (nanoseconds since Unix epoch)
342 /// - `end`: Optional end timestamp override (nanoseconds since Unix epoch)
343 ///
344 /// # Returns
345 ///
346 /// Returns the path of the created file as a string.
347 #[pyo3(signature = (data, start=None, end=None, skip_disjoint_check=false))]
348 pub fn write_index_price_updates(
349 &self,
350 data: Vec<IndexPriceUpdate>,
351 start: Option<u64>,
352 end: Option<u64>,
353 skip_disjoint_check: bool,
354 ) -> PyResult<String> {
355 let start_nanos = start.map(UnixNanos::from);
356 let end_nanos = end.map(UnixNanos::from);
357 let data = data.into_boxed_slice();
358
359 self.inner
360 .write_to_parquet(
361 data.as_ref(),
362 start_nanos,
363 end_nanos,
364 Some(skip_disjoint_check),
365 )
366 .map(|path| path.to_string_lossy().to_string())
367 .map_err(|e| PyIOError::new_err(format!("Failed to write index price updates: {e}")))
368 }
369
370 /// Write option greeks data to Parquet files.
371 ///
372 /// # Parameters
373 ///
374 /// - `data`: Vector of option greeks to write
375 /// - `start`: Optional start timestamp override (nanoseconds since Unix epoch)
376 /// - `end`: Optional end timestamp override (nanoseconds since Unix epoch)
377 ///
378 /// # Returns
379 ///
380 /// Returns the path of the created file as a string.
381 #[pyo3(signature = (data, start=None, end=None, skip_disjoint_check=false))]
382 pub fn write_option_greeks(
383 &self,
384 data: Vec<OptionGreeks>,
385 start: Option<u64>,
386 end: Option<u64>,
387 skip_disjoint_check: bool,
388 ) -> PyResult<String> {
389 let start_nanos = start.map(UnixNanos::from);
390 let end_nanos = end.map(UnixNanos::from);
391 let data = data.into_boxed_slice();
392
393 self.inner
394 .write_to_parquet(
395 data.as_ref(),
396 start_nanos,
397 end_nanos,
398 Some(skip_disjoint_check),
399 )
400 .map(|path| path.to_string_lossy().to_string())
401 .map_err(|e| PyIOError::new_err(format!("Failed to write option greeks: {e}")))
402 }
403
404 /// Write instruments to Parquet files in the catalog.
405 ///
406 /// Instruments are stored under `data/instruments/{instrument_id}/` using timestamp-ranged
407 /// parquet file names, allowing multiple historical versions of the same instrument to be
408 /// written across separate calls.
409 ///
410 /// # Parameters
411 ///
412 /// - `data`: A Python list of instrument objects (e.g. `CurrencyPair`, Equity).
413 ///
414 /// # Returns
415 ///
416 /// Returns a list of written file paths.
417 #[pyo3(signature = (data))]
418 pub fn write_instruments(&self, data: &Bound<'_, PyAny>) -> PyResult<Vec<String>> {
419 let py = data.py();
420 let list = data.cast::<PyList>()?;
421 let mut instruments = Vec::with_capacity(list.len());
422 for item in list.iter() {
423 let py_item: Py<PyAny> = item.unbind();
424 let instrument = pyobject_to_instrument_any(py, py_item)?;
425 instruments.push(instrument);
426 }
427 self.inner
428 .write_instruments(instruments)
429 .map(|paths| {
430 paths
431 .into_iter()
432 .map(|p| p.to_string_lossy().to_string())
433 .collect()
434 })
435 .map_err(|e| PyIOError::new_err(format!("Failed to write instruments: {e}")))
436 }
437
438 /// Query instruments from the catalog.
439 ///
440 /// # Parameters
441 ///
442 /// - `instrument_ids`: Optional list of instrument IDs to filter by. If `None`, returns all instruments.
443 /// - `start`: Optional inclusive lower bound for `ts_init` filtering.
444 /// - `end`: Optional inclusive upper bound for `ts_init` filtering.
445 ///
446 /// # Returns
447 ///
448 /// Returns a list of instrument objects (e.g. `CurrencyPair`, Equity).
449 #[pyo3(signature = (instrument_ids=None, start=None, end=None))]
450 #[expect(clippy::needless_pass_by_value)]
451 pub fn instruments(
452 &self,
453 instrument_ids: Option<Vec<String>>,
454 start: Option<u64>,
455 end: Option<u64>,
456 ) -> PyResult<Vec<Py<PyAny>>> {
457 let rust_instruments = self
458 .inner
459 .query_instruments_filtered(
460 instrument_ids.as_deref(),
461 start.map(UnixNanos::from),
462 end.map(UnixNanos::from),
463 )
464 .map_err(|e| PyIOError::new_err(format!("Failed to query instruments: {e}")))?;
465 Python::attach(|py| {
466 rust_instruments
467 .into_iter()
468 .map(|inst| instrument_any_to_pyobject(py, inst))
469 .collect()
470 })
471 }
472
473 /// Extend file names in the catalog with additional timestamp information.
474 ///
475 /// # Parameters
476 ///
477 /// - `data_cls`: The data class name
478 /// - `instrument_id`: Optional instrument ID filter
479 /// - `start`: Start timestamp (nanoseconds since Unix epoch)
480 /// - `end`: End timestamp (nanoseconds since Unix epoch)
481 #[pyo3(signature = (data_cls, instrument_id=None, *, start, end))]
482 #[expect(clippy::needless_pass_by_value)]
483 pub fn extend_file_name(
484 &self,
485 data_cls: &str,
486 instrument_id: Option<String>,
487 start: u64,
488 end: u64,
489 ) -> PyResult<()> {
490 let start_nanos = UnixNanos::from(start);
491 let end_nanos = UnixNanos::from(end);
492
493 self.inner
494 .extend_file_name(data_cls, instrument_id.as_deref(), start_nanos, end_nanos)
495 .map_err(|e| PyIOError::new_err(format!("Failed to extend file name: {e}")))
496 }
497
498 /// Consolidate all data files in the catalog within the specified time range.
499 ///
500 /// # Parameters
501 ///
502 /// - `start`: Optional start timestamp (nanoseconds since Unix epoch)
503 /// - `end`: Optional end timestamp (nanoseconds since Unix epoch)
504 /// - `ensure_contiguous_files`: Optional flag to ensure files are contiguous
505 /// - `deduplicate`: Optional flag to deduplicate rows when combining files
506 #[pyo3(signature = (start=None, end=None, ensure_contiguous_files=None, deduplicate=None))]
507 pub fn consolidate_catalog(
508 &self,
509 start: Option<u64>,
510 end: Option<u64>,
511 ensure_contiguous_files: Option<bool>,
512 deduplicate: Option<bool>,
513 ) -> PyResult<()> {
514 let start_nanos = start.map(UnixNanos::from);
515 let end_nanos = end.map(UnixNanos::from);
516
517 self.inner
518 .consolidate_catalog(start_nanos, end_nanos, ensure_contiguous_files, deduplicate)
519 .map_err(|e| PyIOError::new_err(format!("Failed to consolidate catalog: {e}")))
520 }
521
522 /// Consolidate data files for a specific data type within the specified time range.
523 ///
524 /// # Parameters
525 ///
526 /// - `type_name`: The data type name to consolidate
527 /// - `instrument_id`: Optional instrument ID filter
528 /// - `start`: Optional start timestamp (nanoseconds since Unix epoch)
529 /// - `end`: Optional end timestamp (nanoseconds since Unix epoch)
530 /// - `ensure_contiguous_files`: Optional flag to ensure files are contiguous
531 /// - `deduplicate`: Optional flag to deduplicate rows when combining files
532 #[pyo3(signature = (type_name, instrument_id=None, start=None, end=None, ensure_contiguous_files=None, deduplicate=None))]
533 #[expect(clippy::needless_pass_by_value)]
534 pub fn consolidate_data(
535 &self,
536 type_name: &str,
537 instrument_id: Option<String>,
538 start: Option<u64>,
539 end: Option<u64>,
540 ensure_contiguous_files: Option<bool>,
541 deduplicate: Option<bool>,
542 ) -> PyResult<()> {
543 let start_nanos = start.map(UnixNanos::from);
544 let end_nanos = end.map(UnixNanos::from);
545
546 self.inner
547 .consolidate_data(
548 type_name,
549 instrument_id.as_deref(),
550 start_nanos,
551 end_nanos,
552 ensure_contiguous_files,
553 deduplicate,
554 )
555 .map_err(|e| PyIOError::new_err(format!("Failed to consolidate data: {e}")))
556 }
557
558 /// Consolidate all data files in the catalog by splitting them into fixed time periods.
559 ///
560 /// This method identifies all leaf directories in the catalog that contain parquet files
561 /// and consolidates them by period. A leaf directory is one that contains files but no subdirectories.
562 /// This is a convenience method that effectively calls `consolidate_data_by_period` for all data types
563 /// and instrument IDs in the catalog.
564 ///
565 /// # Parameters
566 ///
567 /// - `period_nanos`: Optional period duration for consolidation in nanoseconds. Default is 1 day (86400000000000).
568 /// Examples: 3600000000000 (1 hour), 604800000000000 (7 days), 1800000000000 (30 minutes)
569 /// - `start`: Optional start timestamp for the consolidation range (nanoseconds since Unix epoch)
570 /// - `end`: Optional end timestamp for the consolidation range (nanoseconds since Unix epoch)
571 /// - `ensure_contiguous_files`: Optional flag to control file naming strategy
572 #[pyo3(signature = (period_nanos=None, start=None, end=None, ensure_contiguous_files=None))]
573 pub fn consolidate_catalog_by_period(
574 &mut self,
575 period_nanos: Option<u64>,
576 start: Option<u64>,
577 end: Option<u64>,
578 ensure_contiguous_files: Option<bool>,
579 ) -> PyResult<()> {
580 let start_nanos = start.map(UnixNanos::from);
581 let end_nanos = end.map(UnixNanos::from);
582
583 self.inner
584 .consolidate_catalog_by_period(
585 period_nanos,
586 start_nanos,
587 end_nanos,
588 ensure_contiguous_files,
589 )
590 .map_err(|e| {
591 PyIOError::new_err(format!("Failed to consolidate catalog by period: {e}"))
592 })
593 }
594
595 /// Consolidate data files by splitting them into fixed time periods.
596 ///
597 /// This method queries data by period and writes consolidated files immediately,
598 /// using efficient period-based consolidation logic. When start/end boundaries intersect existing files,
599 /// the function automatically splits those files to preserve all data.
600 ///
601 /// # Parameters
602 ///
603 /// - `type_name`: The data type directory name (e.g., "quotes", "trades", "bars")
604 /// - `identifier`: Optional instrument ID to consolidate. If None, consolidates all instruments
605 /// - `period_nanos`: Optional period duration for consolidation in nanoseconds. Default is 1 day (86400000000000).
606 /// Examples: 3600000000000 (1 hour), 604800000000000 (7 days), 1800000000000 (30 minutes)
607 /// - `start`: Optional start timestamp for consolidation range (nanoseconds since Unix epoch)
608 /// - `end`: Optional end timestamp for consolidation range (nanoseconds since Unix epoch)
609 /// - `ensure_contiguous_files`: Optional flag to control file naming strategy
610 #[pyo3(signature = (type_name, identifier=None, period_nanos=None, start=None, end=None, ensure_contiguous_files=None))]
611 #[expect(clippy::needless_pass_by_value)]
612 pub fn consolidate_data_by_period(
613 &mut self,
614 type_name: &str,
615 identifier: Option<String>,
616 period_nanos: Option<u64>,
617 start: Option<u64>,
618 end: Option<u64>,
619 ensure_contiguous_files: Option<bool>,
620 ) -> PyResult<()> {
621 let start_nanos = start.map(UnixNanos::from);
622 let end_nanos = end.map(UnixNanos::from);
623
624 self.inner
625 .consolidate_data_by_period(
626 type_name,
627 identifier.as_deref(),
628 period_nanos,
629 start_nanos,
630 end_nanos,
631 ensure_contiguous_files,
632 )
633 .map_err(|e| PyIOError::new_err(format!("Failed to consolidate data by period: {e}")))
634 }
635
636 /// Reset all catalog file names to their canonical form.
637 pub fn reset_all_file_names(&self) -> PyResult<()> {
638 self.inner
639 .reset_all_file_names()
640 .map_err(|e| PyIOError::new_err(format!("Failed to reset catalog file names: {e}")))
641 }
642
643 /// Reset data file names for a specific data class to their canonical form.
644 ///
645 /// # Parameters
646 ///
647 /// - `data_cls`: The data class name
648 /// - `instrument_id`: Optional instrument ID filter
649 #[pyo3(signature = (data_cls, instrument_id=None))]
650 #[expect(clippy::needless_pass_by_value)]
651 pub fn reset_data_file_names(
652 &self,
653 data_cls: &str,
654 instrument_id: Option<String>,
655 ) -> PyResult<()> {
656 self.inner
657 .reset_data_file_names(data_cls, instrument_id.as_deref())
658 .map_err(|e| PyIOError::new_err(format!("Failed to reset data file names: {e}")))
659 }
660
661 /// Delete data within a specified time range across the entire catalog.
662 ///
663 /// This method identifies all leaf directories in the catalog that contain parquet files
664 /// and deletes data within the specified time range from each directory. A leaf directory
665 /// is one that contains files but no subdirectories. This is a convenience method that
666 /// effectively calls `delete_data_range` for all data types and instrument IDs in the catalog.
667 ///
668 /// # Parameters
669 ///
670 /// - `start`: Optional start timestamp for the deletion range (nanoseconds since Unix epoch)
671 /// - `end`: Optional end timestamp for the deletion range (nanoseconds since Unix epoch)
672 ///
673 /// # Notes
674 ///
675 /// - This operation permanently removes data and cannot be undone
676 /// - The deletion process handles file intersections intelligently by splitting files
677 /// when they partially overlap with the deletion range
678 /// - Files completely within the deletion range are removed entirely
679 /// - Files partially overlapping the deletion range are split to preserve data outside the range
680 /// - This method is useful for bulk data cleanup operations across the entire catalog
681 /// - Empty directories are not automatically removed after deletion
682 #[pyo3(signature = (start=None, end=None))]
683 pub fn delete_catalog_range(&mut self, start: Option<u64>, end: Option<u64>) -> PyResult<()> {
684 let start_nanos = start.map(UnixNanos::from);
685 let end_nanos = end.map(UnixNanos::from);
686
687 self.inner
688 .delete_catalog_range(start_nanos, end_nanos)
689 .map_err(|e| PyIOError::new_err(format!("Failed to delete catalog range: {e}")))
690 }
691
692 /// Delete data within a specified time range for a specific data type and instrument.
693 ///
694 /// This method identifies all parquet files that intersect with the specified time range
695 /// and handles them appropriately:
696 /// - Files completely within the range are deleted
697 /// - Files partially overlapping the range are split to preserve data outside the range
698 /// - The original intersecting files are removed after processing
699 ///
700 /// # Parameters
701 ///
702 /// - `type_name`: The data type directory name (e.g., "quotes", "trades", "bars")
703 /// - `instrument_id`: Optional instrument ID to delete data for. If None, deletes data across all instruments
704 /// - `start`: Optional start timestamp for the deletion range (nanoseconds since Unix epoch)
705 /// - `end`: Optional end timestamp for the deletion range (nanoseconds since Unix epoch)
706 ///
707 /// # Notes
708 ///
709 /// - This operation permanently removes data and cannot be undone
710 /// - Files that partially overlap the deletion range are split to preserve data outside the range
711 /// - The method ensures data integrity by using atomic operations where possible
712 /// - Empty directories are not automatically removed after deletion
713 #[pyo3(signature = (type_name, instrument_id=None, start=None, end=None))]
714 #[expect(clippy::needless_pass_by_value)]
715 pub fn delete_data_range(
716 &mut self,
717 type_name: &str,
718 instrument_id: Option<String>,
719 start: Option<u64>,
720 end: Option<u64>,
721 ) -> PyResult<()> {
722 let start_nanos = start.map(UnixNanos::from);
723 let end_nanos = end.map(UnixNanos::from);
724
725 self.inner
726 .delete_data_range(type_name, instrument_id.as_deref(), start_nanos, end_nanos)
727 .map_err(|e| PyIOError::new_err(format!("Failed to delete data range: {e}")))
728 }
729
730 /// Write custom data to Parquet files.
731 ///
732 /// Requires `CustomData` wrappers. Callers must wrap raw custom objects in
733 /// `CustomData(data_type=DataType(cls, metadata=...), data=...)` before writing.
734 #[pyo3(signature = (data, start=None, end=None, skip_disjoint_check=false))]
735 pub fn write_custom_data(
736 &self,
737 _py: Python<'_>,
738 data: Vec<Bound<'_, PyAny>>,
739 start: Option<u64>,
740 end: Option<u64>,
741 skip_disjoint_check: bool,
742 ) -> PyResult<String> {
743 use nautilus_model::data::CustomData;
744
745 let mut custom_items: Vec<CustomData> = Vec::with_capacity(data.len());
746 for obj in data {
747 let custom = obj.extract::<CustomData>().map_err(|_| {
748 to_pytype_err(
749 "write_custom_data requires CustomData wrappers; wrap with CustomData(data_type=DataType(cls, metadata=...), data=...)",
750 )
751 })?;
752 custom_items.push(custom);
753 }
754
755 let start_nanos = start.map(UnixNanos::from);
756 let end_nanos = end.map(UnixNanos::from);
757
758 self.inner
759 .write_custom_data_batch(
760 custom_items,
761 start_nanos,
762 end_nanos,
763 Some(skip_disjoint_check),
764 )
765 .map(|path| path.to_string_lossy().to_string())
766 .map_err(|e| PyIOError::new_err(format!("Failed to write custom data: {e}")))
767 }
768
769 /// List all instrument IDs available in the catalog for a given data type.
770 pub fn list_instruments(&self, data_type: &str) -> PyResult<Vec<String>> {
771 self.inner
772 .list_instruments(data_type)
773 .map_err(|e| PyIOError::new_err(format!("Failed to list instruments: {e}")))
774 }
775
776 /// List all Parquet files in the catalog for a given data type and instrument.
777 pub fn list_parquet_files(
778 &self,
779 data_type: &str,
780 instrument_id: &str,
781 ) -> PyResult<Vec<String>> {
782 let directory = format!("data/{data_type}/{instrument_id}");
783 self.inner
784 .list_parquet_files(&directory)
785 .map_err(|e| PyIOError::new_err(format!("Failed to list parquet files: {e}")))
786 }
787
788 /// Query files in the catalog matching the specified criteria.
789 ///
790 /// # Parameters
791 ///
792 /// - `data_cls`: The data class name to query
793 /// - `identifiers`: Optional list of identifiers to filter by. Can be `instrument_id` strings
794 /// (e.g., "EUR/USD.SIM") or `bar_type` strings (e.g., "EUR/USD.SIM-1-MINUTE-LAST-EXTERNAL").
795 /// For bars, partial matching is supported.
796 /// - `start`: Optional start timestamp (nanoseconds since Unix epoch)
797 /// - `end`: Optional end timestamp (nanoseconds since Unix epoch)
798 ///
799 /// # Returns
800 ///
801 /// Returns a list of file paths matching the criteria.
802 #[pyo3(signature = (data_cls, identifiers=None, start=None, end=None))]
803 pub fn query_files(
804 &self,
805 data_cls: &str,
806 identifiers: Option<Vec<String>>,
807 start: Option<u64>,
808 end: Option<u64>,
809 ) -> PyResult<Vec<String>> {
810 let start_nanos = start.map(UnixNanos::from);
811 let end_nanos = end.map(UnixNanos::from);
812
813 self.inner
814 .query_files(data_cls, identifiers, start_nanos, end_nanos)
815 .map_err(|e| PyIOError::new_err(format!("Failed to query files list: {e}")))
816 }
817
818 /// Get missing time intervals for a data request.
819 ///
820 /// # Parameters
821 ///
822 /// - `start`: Start timestamp (nanoseconds since Unix epoch)
823 /// - `end`: End timestamp (nanoseconds since Unix epoch)
824 /// - `data_cls`: The data class name
825 /// - `instrument_id`: Optional instrument ID filter
826 ///
827 /// # Returns
828 ///
829 /// Returns a list of (start, end) timestamp tuples representing missing intervals.
830 #[pyo3(signature = (start, end, data_cls, instrument_id=None))]
831 #[expect(clippy::needless_pass_by_value)]
832 pub fn get_missing_intervals_for_request(
833 &self,
834 start: u64,
835 end: u64,
836 data_cls: &str,
837 instrument_id: Option<String>,
838 ) -> PyResult<Vec<(u64, u64)>> {
839 self.inner
840 .get_missing_intervals_for_request(start, end, data_cls, instrument_id.as_deref())
841 .map_err(|e| PyIOError::new_err(format!("Failed to get missing intervals: {e}")))
842 }
843
844 /// Query the first timestamp for a specific data class and instrument.
845 ///
846 /// # Parameters
847 ///
848 /// - `data_cls`: The data class name
849 /// - `instrument_id`: Optional instrument ID filter
850 ///
851 /// # Returns
852 ///
853 /// Returns the first timestamp as nanoseconds since Unix epoch, or None if no data exists.
854 #[pyo3(signature = (data_cls, instrument_id=None))]
855 #[expect(clippy::needless_pass_by_value)]
856 pub fn query_first_timestamp(
857 &self,
858 data_cls: &str,
859 instrument_id: Option<String>,
860 ) -> PyResult<Option<u64>> {
861 self.inner
862 .query_first_timestamp(data_cls, instrument_id.as_deref())
863 .map_err(|e| PyIOError::new_err(format!("Failed to query first timestamp: {e}")))
864 }
865
866 /// Query the last timestamp for a specific data class and instrument.
867 ///
868 /// # Parameters
869 ///
870 /// - `data_cls`: The data class name
871 /// - `instrument_id`: Optional instrument ID filter
872 ///
873 /// # Returns
874 ///
875 /// Returns the last timestamp as nanoseconds since Unix epoch, or None if no data exists.
876 #[pyo3(signature = (data_cls, instrument_id=None))]
877 #[expect(clippy::needless_pass_by_value)]
878 pub fn query_last_timestamp(
879 &self,
880 data_cls: &str,
881 instrument_id: Option<String>,
882 ) -> PyResult<Option<u64>> {
883 self.inner
884 .query_last_timestamp(data_cls, instrument_id.as_deref())
885 .map_err(|e| PyIOError::new_err(format!("Failed to query last timestamp: {e}")))
886 }
887
888 /// Get time intervals covered by data for a specific data class and instrument.
889 ///
890 /// # Parameters
891 ///
892 /// - `data_cls`: The data class name
893 /// - `instrument_id`: Optional instrument ID filter
894 ///
895 /// # Returns
896 ///
897 /// Returns a list of (start, end) timestamp tuples representing covered intervals.
898 #[pyo3(signature = (data_cls, instrument_id=None))]
899 #[expect(clippy::needless_pass_by_value)]
900 pub fn get_intervals(
901 &self,
902 data_cls: &str,
903 instrument_id: Option<String>,
904 ) -> PyResult<Vec<(u64, u64)>> {
905 self.inner
906 .get_intervals(data_cls, instrument_id.as_deref())
907 .map_err(|e| PyIOError::new_err(format!("Failed to get intervals: {e}")))
908 }
909
910 /// Query Parquet files for data matching the given criteria.
911 #[pyo3(signature = (data_type, identifiers=None, start=None, end=None, where_clause=None, files=None, optimize_file_loading=true))]
912 #[expect(
913 clippy::too_many_arguments,
914 clippy::too_many_lines,
915 reason = "PyO3 query binding mirrors the Python catalog API"
916 )]
917 pub fn query(
918 &mut self,
919 py: Python<'_>,
920 data_type: &str,
921 identifiers: Option<Vec<String>>,
922 start: Option<u64>,
923 end: Option<u64>,
924 where_clause: Option<&str>,
925 files: Option<Vec<String>>,
926 optimize_file_loading: bool,
927 ) -> PyResult<Vec<Py<PyAny>>> {
928 let start_nanos = start.map(UnixNanos::from);
929 let end_nanos = end.map(UnixNanos::from);
930
931 let data = match data_type {
932 "quotes" => {
933 let ticks = self
934 .inner
935 .query_typed_data::<QuoteTick>(
936 identifiers,
937 start_nanos,
938 end_nanos,
939 where_clause,
940 files,
941 optimize_file_loading,
942 )
943 .map_err(|e| PyIOError::new_err(format!("Query failed: {e}")))?;
944 ticks.into_iter().map(Data::from).collect()
945 }
946 "trades" => {
947 let ticks = self
948 .inner
949 .query_typed_data::<TradeTick>(
950 identifiers,
951 start_nanos,
952 end_nanos,
953 where_clause,
954 files,
955 optimize_file_loading,
956 )
957 .map_err(|e| PyIOError::new_err(format!("Query failed: {e}")))?;
958 ticks.into_iter().map(Data::from).collect()
959 }
960 "bars" => {
961 let bars = self
962 .inner
963 .query_typed_data::<Bar>(
964 identifiers,
965 start_nanos,
966 end_nanos,
967 where_clause,
968 files,
969 optimize_file_loading,
970 )
971 .map_err(|e| PyIOError::new_err(format!("Query failed: {e}")))?;
972 bars.into_iter().map(Data::from).collect()
973 }
974 "order_book_deltas" => {
975 let deltas = self
976 .inner
977 .query_typed_data::<OrderBookDelta>(
978 identifiers,
979 start_nanos,
980 end_nanos,
981 where_clause,
982 files,
983 optimize_file_loading,
984 )
985 .map_err(|e| PyIOError::new_err(format!("Query failed: {e}")))?;
986 deltas.into_iter().map(Data::from).collect()
987 }
988 "order_book_depths" => {
989 let depths = self
990 .inner
991 .query_typed_data::<OrderBookDepth10>(
992 identifiers,
993 start_nanos,
994 end_nanos,
995 where_clause,
996 files,
997 optimize_file_loading,
998 )
999 .map_err(|e| PyIOError::new_err(format!("Query failed: {e}")))?;
1000 depths.into_iter().map(Data::from).collect()
1001 }
1002 "index_prices" => {
1003 let prices = self
1004 .inner
1005 .query_typed_data::<IndexPriceUpdate>(
1006 identifiers,
1007 start_nanos,
1008 end_nanos,
1009 where_clause,
1010 files,
1011 optimize_file_loading,
1012 )
1013 .map_err(|e| PyIOError::new_err(format!("Query failed: {e}")))?;
1014 prices.into_iter().map(Data::from).collect()
1015 }
1016 "mark_prices" => {
1017 let prices = self
1018 .inner
1019 .query_typed_data::<MarkPriceUpdate>(
1020 identifiers,
1021 start_nanos,
1022 end_nanos,
1023 where_clause,
1024 files,
1025 optimize_file_loading,
1026 )
1027 .map_err(|e| PyIOError::new_err(format!("Query failed: {e}")))?;
1028 prices.into_iter().map(Data::from).collect()
1029 }
1030 "option_greeks" => {
1031 let greeks = self
1032 .inner
1033 .query_typed_data::<OptionGreeks>(
1034 identifiers,
1035 start_nanos,
1036 end_nanos,
1037 where_clause,
1038 files,
1039 optimize_file_loading,
1040 )
1041 .map_err(|e| PyIOError::new_err(format!("Query failed: {e}")))?;
1042 greeks.into_iter().map(Data::from).collect()
1043 }
1044 "instrument_status" => {
1045 let statuses = self
1046 .inner
1047 .query_typed_data::<InstrumentStatus>(
1048 identifiers,
1049 start_nanos,
1050 end_nanos,
1051 where_clause,
1052 files,
1053 optimize_file_loading,
1054 )
1055 .map_err(|e| PyIOError::new_err(format!("Query failed: {e}")))?;
1056 statuses.into_iter().map(Data::from).collect()
1057 }
1058 "instrument_closes" => {
1059 let closes = self
1060 .inner
1061 .query_typed_data::<InstrumentClose>(
1062 identifiers,
1063 start_nanos,
1064 end_nanos,
1065 where_clause,
1066 files,
1067 optimize_file_loading,
1068 )
1069 .map_err(|e| PyIOError::new_err(format!("Query failed: {e}")))?;
1070 closes.into_iter().map(Data::from).collect()
1071 }
1072 _ => py
1073 .detach(|| {
1074 self.inner.query_custom_data_dynamic(
1075 data_type,
1076 identifiers.as_deref(),
1077 start_nanos,
1078 end_nanos,
1079 where_clause,
1080 files.clone(),
1081 optimize_file_loading,
1082 )
1083 })
1084 .map_err(|e| PyIOError::new_err(format!("Query failed: {e}")))?,
1085 };
1086
1087 let mut python_objects = Vec::new();
1088 for item in data {
1089 python_objects.push(data_to_pyobject(py, item)?);
1090 }
1091 Ok(python_objects)
1092 }
1093
1094 /// Query quote tick data from Parquet files.
1095 ///
1096 /// # Parameters
1097 ///
1098 /// - `identifiers`: Optional list of identifiers to filter by. Can be `instrument_id` strings
1099 /// (e.g., "EUR/USD.SIM") or `bar_type` strings (e.g., "EUR/USD.SIM-1-MINUTE-LAST-EXTERNAL").
1100 /// For bars, partial matching is supported.
1101 /// - `start`: Optional start timestamp (nanoseconds since Unix epoch)
1102 /// - `end`: Optional end timestamp (nanoseconds since Unix epoch)
1103 /// - `where_clause`: Optional SQL WHERE clause for additional filtering
1104 ///
1105 /// # Returns
1106 ///
1107 /// Returns a vector of `QuoteTick` objects matching the query criteria.
1108 #[pyo3(signature = (identifiers=None, start=None, end=None, where_clause=None))]
1109 pub fn query_quote_ticks(
1110 &mut self,
1111 identifiers: Option<Vec<String>>,
1112 start: Option<u64>,
1113 end: Option<u64>,
1114 where_clause: Option<&str>,
1115 ) -> PyResult<Vec<QuoteTick>> {
1116 let start_nanos = start.map(UnixNanos::from);
1117 let end_nanos = end.map(UnixNanos::from);
1118
1119 self.inner
1120 .query_typed_data::<QuoteTick>(
1121 identifiers,
1122 start_nanos,
1123 end_nanos,
1124 where_clause,
1125 None,
1126 true, // optimize_file_loading=true for directory-based registration (default)
1127 )
1128 .map_err(|e| PyIOError::new_err(format!("Failed to query data: {e}")))
1129 }
1130
1131 /// Query trade tick data from Parquet files.
1132 ///
1133 /// # Parameters
1134 ///
1135 /// - `identifiers`: Optional list of identifiers to filter by. Can be `instrument_id` strings
1136 /// (e.g., "EUR/USD.SIM") or `bar_type` strings (e.g., "EUR/USD.SIM-1-MINUTE-LAST-EXTERNAL").
1137 /// For bars, partial matching is supported.
1138 /// - `start`: Optional start timestamp (nanoseconds since Unix epoch)
1139 /// - `end`: Optional end timestamp (nanoseconds since Unix epoch)
1140 /// - `where_clause`: Optional SQL WHERE clause for additional filtering
1141 ///
1142 /// # Returns
1143 ///
1144 /// Returns a vector of `TradeTick` objects matching the query criteria.
1145 #[pyo3(signature = (identifiers=None, start=None, end=None, where_clause=None))]
1146 pub fn query_trade_ticks(
1147 &mut self,
1148 identifiers: Option<Vec<String>>,
1149 start: Option<u64>,
1150 end: Option<u64>,
1151 where_clause: Option<&str>,
1152 ) -> PyResult<Vec<TradeTick>> {
1153 let start_nanos = start.map(UnixNanos::from);
1154 let end_nanos = end.map(UnixNanos::from);
1155
1156 self.inner
1157 .query_typed_data::<TradeTick>(
1158 identifiers,
1159 start_nanos,
1160 end_nanos,
1161 where_clause,
1162 None,
1163 true, // optimize_file_loading=true for directory-based registration (default)
1164 )
1165 .map_err(|e| PyIOError::new_err(format!("Failed to query data: {e}")))
1166 }
1167
1168 /// Query order book delta data from Parquet files.
1169 ///
1170 /// # Parameters
1171 ///
1172 /// - `identifiers`: Optional list of identifiers to filter by. Can be `instrument_id` strings
1173 /// (e.g., "EUR/USD.SIM") or `bar_type` strings (e.g., "EUR/USD.SIM-1-MINUTE-LAST-EXTERNAL").
1174 /// For bars, partial matching is supported.
1175 /// - `start`: Optional start timestamp (nanoseconds since Unix epoch)
1176 /// - `end`: Optional end timestamp (nanoseconds since Unix epoch)
1177 /// - `where_clause`: Optional SQL WHERE clause for additional filtering
1178 ///
1179 /// # Returns
1180 ///
1181 /// Returns a vector of `OrderBookDelta` objects matching the query criteria.
1182 #[pyo3(signature = (identifiers=None, start=None, end=None, where_clause=None))]
1183 pub fn query_order_book_deltas(
1184 &mut self,
1185 identifiers: Option<Vec<String>>,
1186 start: Option<u64>,
1187 end: Option<u64>,
1188 where_clause: Option<&str>,
1189 ) -> PyResult<Vec<OrderBookDelta>> {
1190 let start_nanos = start.map(UnixNanos::from);
1191 let end_nanos = end.map(UnixNanos::from);
1192
1193 self.inner
1194 .query_typed_data::<OrderBookDelta>(
1195 identifiers,
1196 start_nanos,
1197 end_nanos,
1198 where_clause,
1199 None,
1200 true, // optimize_file_loading=true for directory-based registration (default)
1201 )
1202 .map_err(|e| PyIOError::new_err(format!("Failed to query data: {e}")))
1203 }
1204
1205 /// Query bar data from Parquet files.
1206 ///
1207 /// # Parameters
1208 ///
1209 /// - `identifiers`: Optional list of identifiers to filter by. Can be `instrument_id` strings
1210 /// (e.g., "EUR/USD.SIM") or `bar_type` strings (e.g., "EUR/USD.SIM-1-MINUTE-LAST-EXTERNAL").
1211 /// For bars, partial matching is supported (e.g., "EUR/USD.SIM" will match all bar types for that instrument).
1212 /// - `start`: Optional start timestamp (nanoseconds since Unix epoch)
1213 /// - `end`: Optional end timestamp (nanoseconds since Unix epoch)
1214 /// - `where_clause`: Optional SQL WHERE clause for additional filtering
1215 ///
1216 /// # Returns
1217 ///
1218 /// Returns a vector of Bar objects matching the query criteria.
1219 #[pyo3(signature = (identifiers=None, start=None, end=None, where_clause=None))]
1220 pub fn query_bars(
1221 &mut self,
1222 identifiers: Option<Vec<String>>,
1223 start: Option<u64>,
1224 end: Option<u64>,
1225 where_clause: Option<&str>,
1226 ) -> PyResult<Vec<Bar>> {
1227 let start_nanos = start.map(UnixNanos::from);
1228 let end_nanos = end.map(UnixNanos::from);
1229
1230 self.inner
1231 .query_typed_data::<Bar>(
1232 identifiers,
1233 start_nanos,
1234 end_nanos,
1235 where_clause,
1236 None,
1237 true, // optimize_file_loading=true for directory-based registration (default)
1238 )
1239 .map_err(|e| PyIOError::new_err(format!("Failed to query data: {e}")))
1240 }
1241
1242 /// Query order book depth data from Parquet files.
1243 ///
1244 /// # Parameters
1245 ///
1246 /// - `instrument_ids`: Optional list of instrument IDs to filter by
1247 /// - `start`: Optional start timestamp (nanoseconds since Unix epoch)
1248 /// - `end`: Optional end timestamp (nanoseconds since Unix epoch)
1249 /// - `where_clause`: Optional SQL WHERE clause for additional filtering
1250 ///
1251 /// # Returns
1252 ///
1253 /// Returns a vector of `OrderBookDepth10` objects matching the query criteria.
1254 #[pyo3(signature = (instrument_ids=None, start=None, end=None, where_clause=None))]
1255 pub fn query_order_book_depths(
1256 &mut self,
1257 instrument_ids: Option<Vec<String>>,
1258 start: Option<u64>,
1259 end: Option<u64>,
1260 where_clause: Option<&str>,
1261 ) -> PyResult<Vec<OrderBookDepth10>> {
1262 let start_nanos = start.map(UnixNanos::from);
1263 let end_nanos = end.map(UnixNanos::from);
1264
1265 self.inner
1266 .query_typed_data::<OrderBookDepth10>(
1267 instrument_ids,
1268 start_nanos,
1269 end_nanos,
1270 where_clause,
1271 None,
1272 true, // optimize_file_loading=true for directory-based registration (default)
1273 )
1274 .map_err(|e| PyIOError::new_err(format!("Failed to query data: {e}")))
1275 }
1276
1277 /// Query mark price update data from Parquet files.
1278 ///
1279 /// # Parameters
1280 ///
1281 /// - `instrument_ids`: Optional list of instrument IDs to filter by
1282 /// - `start`: Optional start timestamp (nanoseconds since Unix epoch)
1283 /// - `end`: Optional end timestamp (nanoseconds since Unix epoch)
1284 /// - `where_clause`: Optional SQL WHERE clause for additional filtering
1285 ///
1286 /// # Returns
1287 ///
1288 /// Returns a vector of `MarkPriceUpdate` objects matching the query criteria.
1289 #[pyo3(signature = (instrument_ids=None, start=None, end=None, where_clause=None))]
1290 pub fn query_mark_price_updates(
1291 &mut self,
1292 instrument_ids: Option<Vec<String>>,
1293 start: Option<u64>,
1294 end: Option<u64>,
1295 where_clause: Option<&str>,
1296 ) -> PyResult<Vec<MarkPriceUpdate>> {
1297 let start_nanos = start.map(UnixNanos::from);
1298 let end_nanos = end.map(UnixNanos::from);
1299
1300 self.inner
1301 .query_typed_data::<MarkPriceUpdate>(
1302 instrument_ids,
1303 start_nanos,
1304 end_nanos,
1305 where_clause,
1306 None,
1307 true, // optimize_file_loading=true for directory-based registration (default)
1308 )
1309 .map_err(|e| PyIOError::new_err(format!("Failed to query data: {e}")))
1310 }
1311
1312 /// Query index price update data from Parquet files.
1313 ///
1314 /// # Parameters
1315 ///
1316 /// - `instrument_ids`: Optional list of instrument IDs to filter by
1317 /// - `start`: Optional start timestamp (nanoseconds since Unix epoch)
1318 /// - `end`: Optional end timestamp (nanoseconds since Unix epoch)
1319 /// - `where_clause`: Optional SQL WHERE clause for additional filtering
1320 ///
1321 /// # Returns
1322 ///
1323 /// Returns a vector of `IndexPriceUpdate` objects matching the query criteria.
1324 #[pyo3(signature = (instrument_ids=None, start=None, end=None, where_clause=None))]
1325 pub fn query_index_price_updates(
1326 &mut self,
1327 instrument_ids: Option<Vec<String>>,
1328 start: Option<u64>,
1329 end: Option<u64>,
1330 where_clause: Option<&str>,
1331 ) -> PyResult<Vec<IndexPriceUpdate>> {
1332 let start_nanos = start.map(UnixNanos::from);
1333 let end_nanos = end.map(UnixNanos::from);
1334
1335 self.inner
1336 .query_typed_data::<IndexPriceUpdate>(
1337 instrument_ids,
1338 start_nanos,
1339 end_nanos,
1340 where_clause,
1341 None,
1342 true, // optimize_file_loading=true for directory-based registration (default)
1343 )
1344 .map_err(|e| PyIOError::new_err(format!("Failed to query data: {e}")))
1345 }
1346
1347 /// Query option greeks data from Parquet files.
1348 #[pyo3(signature = (instrument_ids=None, start=None, end=None, where_clause=None))]
1349 pub fn query_option_greeks(
1350 &mut self,
1351 instrument_ids: Option<Vec<String>>,
1352 start: Option<u64>,
1353 end: Option<u64>,
1354 where_clause: Option<&str>,
1355 ) -> PyResult<Vec<OptionGreeks>> {
1356 let start_nanos = start.map(UnixNanos::from);
1357 let end_nanos = end.map(UnixNanos::from);
1358
1359 self.inner
1360 .query_typed_data::<OptionGreeks>(
1361 instrument_ids,
1362 start_nanos,
1363 end_nanos,
1364 where_clause,
1365 None,
1366 true,
1367 )
1368 .map_err(|e| PyIOError::new_err(format!("Failed to query data: {e}")))
1369 }
1370
1371 /// List all data types available in the catalog.
1372 ///
1373 /// # Returns
1374 ///
1375 /// Returns a list of data type names (as directory stems) in the catalog.
1376 pub fn list_data_types(&self) -> PyResult<Vec<String>> {
1377 self.inner
1378 .list_data_types()
1379 .map_err(|e| PyIOError::new_err(format!("Failed to list data types: {e}")))
1380 }
1381
1382 /// List all live run IDs available in the catalog.
1383 ///
1384 /// # Returns
1385 ///
1386 /// Returns a list of live run IDs (as directory stems) in the catalog.
1387 pub fn list_live_runs(&self) -> PyResult<Vec<String>> {
1388 self.inner
1389 .list_live_runs()
1390 .map_err(|e| PyIOError::new_err(format!("Failed to list live runs: {e}")))
1391 }
1392
1393 /// List all backtest run IDs available in the catalog.
1394 ///
1395 /// # Returns
1396 ///
1397 /// Returns a list of backtest run IDs (as directory stems) in the catalog.
1398 pub fn list_backtest_runs(&self) -> PyResult<Vec<String>> {
1399 self.inner
1400 .list_backtest_runs()
1401 .map_err(|e| PyIOError::new_err(format!("Failed to list backtest runs: {e}")))
1402 }
1403
1404 /// List all backtest run instances available in the catalog.
1405 pub fn list_backtests(&self) -> PyResult<Vec<String>> {
1406 self.inner
1407 .list_backtest_runs()
1408 .map_err(|e| PyIOError::new_err(format!("Failed to list backtests: {e}")))
1409 }
1410
1411 /// Read data from a live run instance.
1412 ///
1413 /// # Parameters
1414 ///
1415 /// - `instance_id`: The ID of the live run instance
1416 ///
1417 /// # Returns
1418 ///
1419 /// Returns a list of data objects from the live run, sorted by timestamp.
1420 #[pyo3(signature = (instance_id))]
1421 pub fn read_live_run(&self, py: Python<'_>, instance_id: &str) -> PyResult<Vec<Py<PyAny>>> {
1422 let data = self
1423 .inner
1424 .read_live_run(instance_id)
1425 .map_err(|e| PyIOError::new_err(format!("Failed to read live run: {e}")))?;
1426
1427 let mut python_objects = Vec::new();
1428 for item in data {
1429 python_objects.push(data_to_pyobject(py, item)?);
1430 }
1431 Ok(python_objects)
1432 }
1433
1434 /// Read data from a backtest run instance.
1435 ///
1436 /// # Parameters
1437 ///
1438 /// - `instance_id`: The ID of the backtest run instance
1439 ///
1440 /// # Returns
1441 ///
1442 /// Returns a list of data objects from the backtest run, sorted by timestamp.
1443 #[pyo3(signature = (instance_id))]
1444 pub fn read_backtest(&self, py: Python<'_>, instance_id: &str) -> PyResult<Vec<Py<PyAny>>> {
1445 let data = self
1446 .inner
1447 .read_backtest(instance_id)
1448 .map_err(|e| PyIOError::new_err(format!("Failed to read backtest: {e}")))?;
1449
1450 let mut python_objects = Vec::new();
1451 for item in data {
1452 python_objects.push(data_to_pyobject(py, item)?);
1453 }
1454 Ok(python_objects)
1455 }
1456
1457 /// Convert stream data from feather files to parquet files.
1458 ///
1459 /// This method reads data from feather files generated during a backtest or live run
1460 /// and writes it to the catalog in parquet format. It's useful for converting temporary
1461 /// stream data into a more permanent and queryable format.
1462 ///
1463 /// # Parameters
1464 ///
1465 /// - `instance_id`: The ID of the backtest or live run instance
1466 /// - `data_cls`: The data class name (e.g., "quotes", "trades", "bars")
1467 /// - `subdirectory`: Optional subdirectory containing the feather files. Either "backtest" or "live" (default: "backtest")
1468 /// - `identifiers`: Optional list of identifiers to filter by (instrument IDs or bar types)
1469 /// - `use_ts_event_for_ts_init`: If true, replaces the `ts_init` column with `ts_event` column values before deserializing
1470 ///
1471 /// # Returns
1472 ///
1473 /// Returns nothing on success.
1474 ///
1475 /// # Examples
1476 ///
1477 /// ```python
1478 /// # Convert backtest stream data to parquet
1479 /// catalog.convert_stream_to_data(
1480 /// "instance-123",
1481 /// "quotes",
1482 /// subdirectory="backtest"
1483 /// )
1484 ///
1485 /// # Convert live run data with identifier filtering
1486 /// catalog.convert_stream_to_data(
1487 /// "instance-456",
1488 /// "trades",
1489 /// subdirectory="live",
1490 /// identifiers=["EUR/USD.SIM"]
1491 /// )
1492 /// ```
1493 #[pyo3(signature = (instance_id, data_cls, subdirectory=None, identifiers=None, use_ts_event_for_ts_init=false))]
1494 #[expect(clippy::needless_pass_by_value)]
1495 pub fn convert_stream_to_data(
1496 &mut self,
1497 instance_id: &str,
1498 data_cls: &str,
1499 subdirectory: Option<&str>,
1500 identifiers: Option<Vec<String>>,
1501 use_ts_event_for_ts_init: bool,
1502 ) -> PyResult<()> {
1503 let subdir = subdirectory.unwrap_or("backtest");
1504
1505 match self.inner.convert_stream_to_data(
1506 instance_id,
1507 data_cls,
1508 Some(subdir),
1509 identifiers.as_deref(),
1510 use_ts_event_for_ts_init,
1511 ) {
1512 Ok(()) => Ok(()),
1513 Err(e) => Err(PyIOError::new_err(format!(
1514 "Failed to convert stream to data: {e}"
1515 ))),
1516 }
1517 }
1518
1519 /// Query custom data from Parquet files.
1520 #[pyo3(signature = (type_name, identifiers=None, start=None, end=None, where_clause=None))]
1521 #[expect(clippy::needless_pass_by_value)]
1522 pub fn query_custom_data(
1523 &mut self,
1524 py: Python<'_>,
1525 type_name: &str,
1526 identifiers: Option<Vec<String>>,
1527 start: Option<u64>,
1528 end: Option<u64>,
1529 where_clause: Option<&str>,
1530 ) -> PyResult<Vec<Py<PyAny>>> {
1531 let start_nanos = start.map(UnixNanos::from);
1532 let end_nanos = end.map(UnixNanos::from);
1533
1534 let data = py
1535 .detach(|| {
1536 self.inner.query_custom_data_dynamic(
1537 type_name,
1538 identifiers.as_deref(),
1539 start_nanos,
1540 end_nanos,
1541 where_clause,
1542 None,
1543 true,
1544 )
1545 })
1546 .map_err(|e| PyIOError::new_err(format!("Failed to query custom data: {e}")))?;
1547
1548 let mut python_objects = Vec::new();
1549
1550 for item in data {
1551 let py_obj: Py<PyAny> = match item {
1552 Data::Custom(custom) => Py::new(py, custom.clone())?.into_any(),
1553 _ => return Err(PyIOError::new_err("Expected custom data")),
1554 };
1555 python_objects.push(py_obj);
1556 }
1557 Ok(python_objects)
1558 }
1559}