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