Skip to main content

nautilus_tardis/python/
csv.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::{fmt::Debug, path::PathBuf, time::Duration};
17
18use nautilus_core::python::to_pyvalue_err;
19use nautilus_model::{
20    data::{Data, FundingRateUpdate, OrderBookDelta, OrderBookDepth, QuoteTick, TradeTick},
21    identifiers::InstrumentId,
22};
23use pyo3::prelude::*;
24
25use crate::csv::{
26    convert::{TardisOptionsChainCSVConverterConfig, convert_options_chain_csv},
27    load::{
28        load_deltas, load_depth_from_snapshot5, load_depth_from_snapshot25, load_funding_rates,
29        load_options_chain, load_quotes, load_trades,
30    },
31    stream::{
32        stream_batched_deltas, stream_deltas, stream_depth_from_snapshot5,
33        stream_depth_from_snapshot25, stream_funding_rates, stream_options_chain, stream_quotes,
34        stream_trades,
35    },
36};
37
38macro_rules! impl_tardis_stream_iterator {
39    ($struct_name:ident, $data_type:ty, $type_name:expr) => {
40        #[pyclass(unsendable)]
41        #[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.tardis")]
42        pub struct $struct_name {
43            stream: Box<dyn Iterator<Item = anyhow::Result<Vec<$data_type>>>>,
44        }
45
46        impl Debug for $struct_name {
47            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48                write!(f, "{} {{ stream: ... }}", $type_name)
49            }
50        }
51
52        #[pymethods]
53        #[pyo3_stub_gen::derive::gen_stub_pymethods]
54        impl $struct_name {
55            const fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
56                slf
57            }
58
59            fn __next__(&mut self) -> PyResult<Option<Vec<$data_type>>> {
60                match self.stream.next() {
61                    Some(Ok(chunk)) => Ok(Some(chunk)),
62                    Some(Err(e)) => Err(to_pyvalue_err(e)),
63                    None => Ok(None),
64                }
65            }
66        }
67    };
68}
69
70fn options_chain_data_to_pyobject(py: Python<'_>, data: Data) -> PyResult<Py<PyAny>> {
71    match data {
72        Data::Quote(quote) => Py::new(py, quote).map(|value| value.into_any()),
73        Data::OptionGreeks(greeks) => Py::new(py, greeks).map(|value| value.into_any()),
74        data => Err(to_pyvalue_err(format!(
75            "Unsupported options_chain data type: {data:?}"
76        ))),
77    }
78}
79
80/// # Errors
81///
82/// Returns a Python error if loading or parsing the CSV file fails.
83#[pyfunction(name = "load_tardis_deltas")]
84#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.adapters.tardis")]
85#[pyo3(signature = (filepath, price_precision=None, size_precision=None, instrument_id=None, limit=None))]
86pub fn py_load_tardis_deltas(
87    filepath: PathBuf,
88    price_precision: Option<u8>,
89    size_precision: Option<u8>,
90    instrument_id: Option<InstrumentId>,
91    limit: Option<usize>,
92) -> PyResult<Vec<OrderBookDelta>> {
93    load_deltas(
94        filepath,
95        price_precision,
96        size_precision,
97        instrument_id,
98        limit,
99    )
100    .map_err(to_pyvalue_err)
101}
102
103/// # Errors
104///
105/// Returns a Python error if loading or parsing the CSV file fails.
106#[pyfunction(name = "load_tardis_depth_from_snapshot5")]
107#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.adapters.tardis")]
108#[pyo3(signature = (filepath, price_precision=None, size_precision=None, instrument_id=None, limit=None))]
109pub fn py_load_tardis_depth_from_snapshot5(
110    filepath: PathBuf,
111    price_precision: Option<u8>,
112    size_precision: Option<u8>,
113    instrument_id: Option<InstrumentId>,
114    limit: Option<usize>,
115) -> PyResult<Vec<OrderBookDepth>> {
116    load_depth_from_snapshot5(
117        filepath,
118        price_precision,
119        size_precision,
120        instrument_id,
121        limit,
122    )
123    .map_err(to_pyvalue_err)
124}
125
126/// # Errors
127///
128/// Returns a Python error if loading or parsing the CSV file fails.
129#[pyfunction(name = "load_tardis_depth_from_snapshot25")]
130#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.adapters.tardis")]
131#[pyo3(signature = (filepath, price_precision=None, size_precision=None, instrument_id=None, limit=None))]
132pub fn py_load_tardis_depth_from_snapshot25(
133    filepath: PathBuf,
134    price_precision: Option<u8>,
135    size_precision: Option<u8>,
136    instrument_id: Option<InstrumentId>,
137    limit: Option<usize>,
138) -> PyResult<Vec<OrderBookDepth>> {
139    load_depth_from_snapshot25(
140        filepath,
141        price_precision,
142        size_precision,
143        instrument_id,
144        limit,
145    )
146    .map_err(to_pyvalue_err)
147}
148
149/// # Errors
150///
151/// Returns a Python error if loading or parsing the CSV file fails.
152#[pyfunction(name = "load_tardis_quotes")]
153#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.adapters.tardis")]
154#[pyo3(signature = (filepath, price_precision=None, size_precision=None, instrument_id=None, limit=None))]
155pub fn py_load_tardis_quotes(
156    filepath: PathBuf,
157    price_precision: Option<u8>,
158    size_precision: Option<u8>,
159    instrument_id: Option<InstrumentId>,
160    limit: Option<usize>,
161) -> PyResult<Vec<QuoteTick>> {
162    load_quotes(
163        filepath,
164        price_precision,
165        size_precision,
166        instrument_id,
167        limit,
168    )
169    .map_err(to_pyvalue_err)
170}
171
172/// # Errors
173///
174/// Returns a Python error if loading or parsing the CSV file fails.
175#[pyfunction(name = "load_tardis_trades")]
176#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.adapters.tardis")]
177#[pyo3(signature = (filepath, price_precision=None, size_precision=None, instrument_id=None, limit=None))]
178pub fn py_load_tardis_trades(
179    filepath: PathBuf,
180    price_precision: Option<u8>,
181    size_precision: Option<u8>,
182    instrument_id: Option<InstrumentId>,
183    limit: Option<usize>,
184) -> PyResult<Vec<TradeTick>> {
185    load_trades(
186        filepath,
187        price_precision,
188        size_precision,
189        instrument_id,
190        limit,
191    )
192    .map_err(to_pyvalue_err)
193}
194
195/// # Errors
196///
197/// Returns a Python error if loading or parsing the CSV file fails.
198#[pyfunction(name = "load_tardis_funding_rates")]
199#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.adapters.tardis")]
200#[pyo3(signature = (filepath, instrument_id=None, limit=None))]
201pub fn py_load_tardis_funding_rates(
202    filepath: PathBuf,
203    instrument_id: Option<InstrumentId>,
204    limit: Option<usize>,
205) -> PyResult<Vec<FundingRateUpdate>> {
206    load_funding_rates(filepath, instrument_id, limit).map_err(to_pyvalue_err)
207}
208
209/// # Errors
210///
211/// Returns a Python error if loading or parsing the CSV file fails.
212#[pyfunction(name = "load_tardis_options_chain")]
213#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.adapters.tardis")]
214#[pyo3(signature = (filepath, underlyings=None, price_precision=None, size_precision=None, limit=None))]
215pub fn py_load_tardis_options_chain(
216    py: Python<'_>,
217    filepath: PathBuf,
218    underlyings: Option<Vec<String>>,
219    price_precision: Option<u8>,
220    size_precision: Option<u8>,
221    limit: Option<usize>,
222) -> PyResult<Vec<Py<PyAny>>> {
223    load_options_chain(
224        filepath,
225        underlyings,
226        price_precision,
227        size_precision,
228        limit,
229    )
230    .map_err(to_pyvalue_err)?
231    .into_iter()
232    .map(|data| options_chain_data_to_pyobject(py, data))
233    .collect()
234}
235
236/// Converts Tardis `options_chain` CSV files into a Nautilus catalog.
237///
238/// # Errors
239///
240/// Returns a Python error if parsing, instrument derivation, or catalog writing fails.
241#[pyfunction(name = "convert_tardis_options_chain_csv")]
242#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.adapters.tardis")]
243#[pyo3(signature = (filepaths, catalog_path, underlyings=None, snapshot_interval_ms=None, extract_bbo_as_quotes=true, write_instruments=true, price_precision=None, size_precision=None))]
244#[allow(
245    clippy::too_many_arguments,
246    reason = "PyO3 exposes these keyword arguments as function parameters"
247)]
248pub fn py_convert_tardis_options_chain_csv(
249    py: Python<'_>,
250    filepaths: Vec<PathBuf>,
251    catalog_path: PathBuf,
252    underlyings: Option<Vec<String>>,
253    snapshot_interval_ms: Option<u64>,
254    extract_bbo_as_quotes: bool,
255    write_instruments: bool,
256    price_precision: Option<u8>,
257    size_precision: Option<u8>,
258) -> PyResult<()> {
259    let config = TardisOptionsChainCSVConverterConfig {
260        filepaths,
261        catalog_path,
262        underlyings,
263        snapshot_interval: snapshot_interval_ms.map(Duration::from_millis),
264        extract_bbo_as_quotes,
265        write_instruments,
266        price_precision,
267        size_precision,
268    };
269
270    py.detach(|| convert_options_chain_csv(&config))
271        .map_err(to_pyvalue_err)
272}
273
274impl_tardis_stream_iterator!(
275    TardisDeltaStreamIterator,
276    OrderBookDelta,
277    "TardisDeltaStreamIterator"
278);
279
280/// Streams order book deltas from a Tardis CSV file.
281///
282/// # Errors
283///
284/// Returns a Python `ValueError` if `chunk_size` is outside `[1, 1_000_000]`, or if loading or parsing
285/// the CSV file fails.
286#[pyfunction(name = "stream_tardis_deltas")]
287#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.adapters.tardis")]
288#[pyo3(signature = (filepath, chunk_size=100_000, price_precision=None, size_precision=None, instrument_id=None, limit=None))]
289pub fn py_stream_tardis_deltas(
290    filepath: PathBuf,
291    chunk_size: usize,
292    price_precision: Option<u8>,
293    size_precision: Option<u8>,
294    instrument_id: Option<InstrumentId>,
295    limit: Option<usize>,
296) -> PyResult<TardisDeltaStreamIterator> {
297    let stream = stream_deltas(
298        filepath,
299        chunk_size,
300        price_precision,
301        size_precision,
302        instrument_id,
303        limit,
304    )
305    .map_err(to_pyvalue_err)?;
306
307    Ok(TardisDeltaStreamIterator {
308        stream: Box::new(stream),
309    })
310}
311
312#[pyclass(unsendable)]
313#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.tardis")]
314pub struct TardisBatchedDeltasStreamIterator {
315    stream: Box<dyn Iterator<Item = anyhow::Result<Vec<Py<PyAny>>>>>,
316}
317
318impl Debug for TardisBatchedDeltasStreamIterator {
319    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320        write!(f, "TardisBatchedDeltasStreamIterator {{ stream: ... }}")
321    }
322}
323
324#[pymethods]
325#[pyo3_stub_gen::derive::gen_stub_pymethods]
326impl TardisBatchedDeltasStreamIterator {
327    const fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
328        slf
329    }
330
331    fn __next__(&mut self) -> PyResult<Option<Vec<Py<PyAny>>>> {
332        match self.stream.next() {
333            Some(Ok(batch)) => Ok(Some(batch)),
334            Some(Err(e)) => Err(to_pyvalue_err(e)),
335            None => Ok(None),
336        }
337    }
338}
339
340/// Streams batched order book deltas from a Tardis CSV file.
341///
342/// # Errors
343///
344/// Returns a Python `ValueError` if `chunk_size` is outside `[1, 1_000_000]`, or if loading or parsing
345/// the CSV file fails.
346#[pyfunction(name = "stream_tardis_batched_deltas")]
347#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.adapters.tardis")]
348#[pyo3(signature = (filepath, chunk_size=100_000, price_precision=None, size_precision=None, instrument_id=None, limit=None))]
349pub fn py_stream_tardis_batched_deltas(
350    filepath: PathBuf,
351    chunk_size: usize,
352    price_precision: Option<u8>,
353    size_precision: Option<u8>,
354    instrument_id: Option<InstrumentId>,
355    limit: Option<usize>,
356) -> PyResult<TardisBatchedDeltasStreamIterator> {
357    let stream = stream_batched_deltas(
358        filepath,
359        chunk_size,
360        price_precision,
361        size_precision,
362        instrument_id,
363        limit,
364    )
365    .map_err(to_pyvalue_err)?;
366
367    Ok(TardisBatchedDeltasStreamIterator {
368        stream: Box::new(stream),
369    })
370}
371
372impl_tardis_stream_iterator!(
373    TardisQuoteStreamIterator,
374    QuoteTick,
375    "TardisQuoteStreamIterator"
376);
377
378/// Streams quote ticks from a Tardis CSV file.
379///
380/// # Errors
381///
382/// Returns a Python `ValueError` if `chunk_size` is outside `[1, 1_000_000]`, or if loading or parsing
383/// the CSV file fails.
384#[pyfunction(name = "stream_tardis_quotes")]
385#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.adapters.tardis")]
386#[pyo3(signature = (filepath, chunk_size=100_000, price_precision=None, size_precision=None, instrument_id=None, limit=None))]
387pub fn py_stream_tardis_quotes(
388    filepath: PathBuf,
389    chunk_size: usize,
390    price_precision: Option<u8>,
391    size_precision: Option<u8>,
392    instrument_id: Option<InstrumentId>,
393    limit: Option<usize>,
394) -> PyResult<TardisQuoteStreamIterator> {
395    let stream = stream_quotes(
396        filepath,
397        chunk_size,
398        price_precision,
399        size_precision,
400        instrument_id,
401        limit,
402    )
403    .map_err(to_pyvalue_err)?;
404
405    Ok(TardisQuoteStreamIterator {
406        stream: Box::new(stream),
407    })
408}
409
410#[pyclass(unsendable)]
411#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.tardis")]
412pub struct TardisOptionsChainStreamIterator {
413    stream: Box<dyn Iterator<Item = anyhow::Result<Vec<Data>>>>,
414}
415
416impl Debug for TardisOptionsChainStreamIterator {
417    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
418        write!(f, "TardisOptionsChainStreamIterator {{ stream: ... }}")
419    }
420}
421
422#[pymethods]
423#[pyo3_stub_gen::derive::gen_stub_pymethods]
424impl TardisOptionsChainStreamIterator {
425    const fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
426        slf
427    }
428
429    fn __next__(&mut self, py: Python<'_>) -> PyResult<Option<Vec<Py<PyAny>>>> {
430        match self.stream.next() {
431            Some(Ok(chunk)) => chunk
432                .into_iter()
433                .map(|data| options_chain_data_to_pyobject(py, data))
434                .collect::<PyResult<Vec<_>>>()
435                .map(Some),
436            Some(Err(e)) => Err(to_pyvalue_err(e)),
437            None => Ok(None),
438        }
439    }
440}
441
442/// Streams Tardis options chain rows from a CSV file.
443///
444/// # Errors
445///
446/// Returns a Python `ValueError` if `chunk_size` is outside `[1, 1_000_000]`, or if loading or parsing
447/// the CSV file fails.
448#[pyfunction(name = "stream_tardis_options_chain")]
449#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.adapters.tardis")]
450#[pyo3(signature = (filepath, chunk_size=100_000, underlyings=None, price_precision=None, size_precision=None, limit=None))]
451pub fn py_stream_tardis_options_chain(
452    filepath: PathBuf,
453    chunk_size: usize,
454    underlyings: Option<Vec<String>>,
455    price_precision: Option<u8>,
456    size_precision: Option<u8>,
457    limit: Option<usize>,
458) -> PyResult<TardisOptionsChainStreamIterator> {
459    let stream = stream_options_chain(
460        filepath,
461        chunk_size,
462        underlyings,
463        price_precision,
464        size_precision,
465        limit,
466    )
467    .map_err(to_pyvalue_err)?;
468
469    Ok(TardisOptionsChainStreamIterator {
470        stream: Box::new(stream),
471    })
472}
473
474impl_tardis_stream_iterator!(
475    TardisTradeStreamIterator,
476    TradeTick,
477    "TardisTradeStreamIterator"
478);
479
480/// Streams trade ticks from a Tardis CSV file.
481///
482/// # Errors
483///
484/// Returns a Python `ValueError` if `chunk_size` is outside `[1, 1_000_000]`, or if loading or parsing
485/// the CSV file fails.
486#[pyfunction(name = "stream_tardis_trades")]
487#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.adapters.tardis")]
488#[pyo3(signature = (filepath, chunk_size=100_000, price_precision=None, size_precision=None, instrument_id=None, limit=None))]
489pub fn py_stream_tardis_trades(
490    filepath: PathBuf,
491    chunk_size: usize,
492    price_precision: Option<u8>,
493    size_precision: Option<u8>,
494    instrument_id: Option<InstrumentId>,
495    limit: Option<usize>,
496) -> PyResult<TardisTradeStreamIterator> {
497    let stream = stream_trades(
498        filepath,
499        chunk_size,
500        price_precision,
501        size_precision,
502        instrument_id,
503        limit,
504    )
505    .map_err(to_pyvalue_err)?;
506
507    Ok(TardisTradeStreamIterator {
508        stream: Box::new(stream),
509    })
510}
511
512impl_tardis_stream_iterator!(
513    TardisDepthStreamIterator,
514    OrderBookDepth,
515    "TardisDepthStreamIterator"
516);
517
518/// Streams order book depth from a Tardis snapshot5 CSV file.
519///
520/// # Errors
521///
522/// Returns a Python `ValueError` if `chunk_size` is outside `[1, 1_000_000]`, or if loading or parsing
523/// the CSV file fails.
524#[pyfunction(name = "stream_tardis_depth_from_snapshot5")]
525#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.adapters.tardis")]
526#[pyo3(signature = (filepath, chunk_size=100_000, price_precision=None, size_precision=None, instrument_id=None, limit=None))]
527pub fn py_stream_tardis_depth_from_snapshot5(
528    filepath: PathBuf,
529    chunk_size: usize,
530    price_precision: Option<u8>,
531    size_precision: Option<u8>,
532    instrument_id: Option<InstrumentId>,
533    limit: Option<usize>,
534) -> PyResult<TardisDepthStreamIterator> {
535    let stream = stream_depth_from_snapshot5(
536        filepath,
537        chunk_size,
538        price_precision,
539        size_precision,
540        instrument_id,
541        limit,
542    )
543    .map_err(to_pyvalue_err)?;
544
545    Ok(TardisDepthStreamIterator {
546        stream: Box::new(stream),
547    })
548}
549
550/// Streams order book depth from a Tardis snapshot25 CSV file.
551///
552/// # Errors
553///
554/// Returns a Python `ValueError` if `chunk_size` is outside `[1, 1_000_000]`, or if loading or parsing
555/// the CSV file fails.
556#[pyfunction(name = "stream_tardis_depth_from_snapshot25")]
557#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.adapters.tardis")]
558#[pyo3(signature = (filepath, chunk_size=100_000, price_precision=None, size_precision=None, instrument_id=None, limit=None))]
559pub fn py_stream_tardis_depth_from_snapshot25(
560    filepath: PathBuf,
561    chunk_size: usize,
562    price_precision: Option<u8>,
563    size_precision: Option<u8>,
564    instrument_id: Option<InstrumentId>,
565    limit: Option<usize>,
566) -> PyResult<TardisDepthStreamIterator> {
567    let stream = stream_depth_from_snapshot25(
568        filepath,
569        chunk_size,
570        price_precision,
571        size_precision,
572        instrument_id,
573        limit,
574    )
575    .map_err(to_pyvalue_err)?;
576
577    Ok(TardisDepthStreamIterator {
578        stream: Box::new(stream),
579    })
580}
581
582impl_tardis_stream_iterator!(
583    TardisFundingRateStreamIterator,
584    FundingRateUpdate,
585    "TardisFundingRateStreamIterator"
586);
587
588/// Streams funding rate updates from a Tardis derivative ticker CSV file.
589///
590/// # Errors
591///
592/// Returns a Python `ValueError` if `chunk_size` is outside `[1, 1_000_000]`, or if loading or parsing
593/// the CSV file fails.
594#[pyfunction(name = "stream_tardis_funding_rates")]
595#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.adapters.tardis")]
596#[pyo3(signature = (filepath, chunk_size=100_000, instrument_id=None, limit=None))]
597pub fn py_stream_tardis_funding_rates(
598    filepath: PathBuf,
599    chunk_size: usize,
600    instrument_id: Option<InstrumentId>,
601    limit: Option<usize>,
602) -> PyResult<TardisFundingRateStreamIterator> {
603    let stream =
604        stream_funding_rates(filepath, chunk_size, instrument_id, limit).map_err(to_pyvalue_err)?;
605
606    Ok(TardisFundingRateStreamIterator {
607        stream: Box::new(stream),
608    })
609}