1use std::{io::Cursor, sync::Arc};
17
18use arrow::{
19 datatypes::Schema,
20 ffi_stream::FFI_ArrowArrayStream,
21 ipc::{reader::StreamReader, writer::StreamWriter},
22 record_batch::{RecordBatch, RecordBatchIterator},
23};
24use nautilus_core::python::{to_pyruntime_err, to_pytype_err, to_pyvalue_err};
25use nautilus_model::{
26 data::{
27 Bar, FundingRateUpdate, IndexPriceUpdate, InstrumentStatus, MarkPriceUpdate, OptionGreeks,
28 OrderBookDelta, OrderBookDepth, QuoteTick, TradeTick,
29 close::InstrumentClose,
30 greeks::{GreeksData, PortfolioGreeks},
31 },
32 python::data::{
33 pyobjects_to_bars, pyobjects_to_book_deltas, pyobjects_to_index_prices,
34 pyobjects_to_instrument_closes, pyobjects_to_instrument_statuses, pyobjects_to_mark_prices,
35 pyobjects_to_option_greeks, pyobjects_to_quotes, pyobjects_to_trades,
36 },
37};
38use pyo3::{
39 conversion::IntoPyObjectExt,
40 prelude::*,
41 types::{PyBytes, PyCapsule, PyType},
42};
43
44use crate::arrow::{
45 ArrowSchemaProvider, DecodeFromRecordBatch, DecodeTypedFromRecordBatch, EncodeToRecordBatch,
46 bars_to_arrow_record_batch_bytes, book_deltas_to_arrow_record_batch_bytes,
47 book_depths_to_arrow_record_batch_bytes, encode_batch_with_metadata,
48 index_prices_to_arrow_record_batch_bytes, instrument_closes_to_arrow_record_batch_bytes,
49 instrument_status_to_arrow_record_batch_bytes, mark_prices_to_arrow_record_batch_bytes,
50 option_greeks_to_arrow_record_batch_bytes, quotes_to_arrow_record_batch_bytes,
51 trades_to_arrow_record_batch_bytes,
52};
53
54pub fn arrow_record_batch_to_pybytes(py: Python, batch: &RecordBatch) -> PyResult<Py<PyBytes>> {
60 arrow_record_batches_to_pybytes(py, &batch.schema(), std::slice::from_ref(batch))
61}
62
63pub fn arrow_record_batches_to_pybytes(
69 py: Python,
70 schema: &Schema,
71 batches: &[RecordBatch],
72) -> PyResult<Py<PyBytes>> {
73 let mut cursor = Cursor::new(Vec::new());
74 {
75 let mut writer = StreamWriter::try_new(&mut cursor, schema).map_err(to_pyruntime_err)?;
76
77 for batch in batches {
78 writer.write(batch).map_err(to_pyruntime_err)?;
79 }
80
81 writer.finish().map_err(to_pyruntime_err)?;
82 }
83
84 let buffer = cursor.into_inner();
85 let pybytes = PyBytes::new(py, &buffer);
86
87 Ok(pybytes.into())
88}
89
90pub fn arrow_record_batches_to_pyarrow_stream(
96 py: Python<'_>,
97 schema: &Schema,
98 batches: Vec<RecordBatch>,
99) -> PyResult<Py<PyAny>> {
100 let reader = RecordBatchIterator::new(batches.into_iter().map(Ok), Arc::new(schema.clone()));
101 let stream = FFI_ArrowArrayStream::new(Box::new(reader));
102
103 let capsule = PyCapsule::new_with_value(py, stream, c"arrow_array_stream")?;
105 Ok(capsule.into_any().unbind())
106}
107
108#[pyfunction]
114#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.serialization")]
115pub fn get_arrow_schema_map(py: Python<'_>, cls: &Bound<'_, PyType>) -> PyResult<Py<PyAny>> {
116 let cls_str: String = cls.getattr("__name__")?.extract()?;
117 let result_map = match cls_str.as_str() {
118 stringify!(OrderBookDelta) => OrderBookDelta::get_schema_map(),
119 stringify!(OrderBookDepth) => OrderBookDepth::get_schema_map(),
120 stringify!(QuoteTick) => QuoteTick::get_schema_map(),
121 stringify!(TradeTick) => TradeTick::get_schema_map(),
122 stringify!(Bar) => Bar::get_schema_map(),
123 stringify!(MarkPriceUpdate) => MarkPriceUpdate::get_schema_map(),
124 stringify!(IndexPriceUpdate) => IndexPriceUpdate::get_schema_map(),
125 stringify!(FundingRateUpdate) => FundingRateUpdate::get_schema_map(),
126 stringify!(InstrumentStatus) => InstrumentStatus::get_schema_map(),
127 stringify!(OptionGreeks) => OptionGreeks::get_schema_map(),
128 stringify!(InstrumentClose) => InstrumentClose::get_schema_map(),
129 stringify!(GreeksData) => GreeksData::get_schema_map(),
130 stringify!(PortfolioGreeks) => PortfolioGreeks::get_schema_map(),
131 _ => {
132 return Err(to_pytype_err(format!(
133 "Arrow schema for `{cls_str}` is not currently implemented in Rust."
134 )));
135 }
136 };
137
138 result_map.into_py_any(py)
139}
140
141#[pyfunction]
147#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.serialization")]
148pub fn get_arrow_schema_bytes(py: Python<'_>, cls: &Bound<'_, PyType>) -> PyResult<Py<PyBytes>> {
149 let cls_str: String = cls.getattr("__name__")?.extract()?;
150 let schema = match cls_str.as_str() {
151 stringify!(OrderBookDelta) => OrderBookDelta::get_schema(None),
152 stringify!(OrderBookDepth) => OrderBookDepth::get_schema(None),
153 stringify!(QuoteTick) => QuoteTick::get_schema(None),
154 stringify!(TradeTick) => TradeTick::get_schema(None),
155 stringify!(Bar) => Bar::get_schema(None),
156 stringify!(MarkPriceUpdate) => MarkPriceUpdate::get_schema(None),
157 stringify!(IndexPriceUpdate) => IndexPriceUpdate::get_schema(None),
158 stringify!(FundingRateUpdate) => FundingRateUpdate::get_schema(None),
159 stringify!(InstrumentStatus) => InstrumentStatus::get_schema(None),
160 stringify!(OptionGreeks) => OptionGreeks::get_schema(None),
161 stringify!(InstrumentClose) => InstrumentClose::get_schema(None),
162 stringify!(GreeksData) => GreeksData::get_schema(None),
163 stringify!(PortfolioGreeks) => PortfolioGreeks::get_schema(None),
164 _ => {
165 return Err(to_pytype_err(format!(
166 "Arrow schema for `{cls_str}` is not currently implemented in Rust."
167 )));
168 }
169 };
170
171 arrow_record_batches_to_pybytes(py, &schema, &[])
172}
173
174#[pyfunction]
184#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.serialization")]
185#[expect(clippy::missing_panics_doc)] pub fn pyobjects_to_arrow_record_batch_bytes(
187 py: Python,
188 data: Vec<Bound<'_, PyAny>>,
189) -> PyResult<Py<PyBytes>> {
190 if data.is_empty() {
191 return Err(to_pyvalue_err("Empty data"));
192 }
193
194 let data_type: String = data
195 .first()
196 .unwrap() .getattr("__class__")?
198 .getattr("__name__")?
199 .extract()?;
200
201 match data_type.as_str() {
202 stringify!(OrderBookDelta) => {
203 let deltas = pyobjects_to_book_deltas(data)?;
204 py_book_deltas_to_arrow_record_batch_bytes(py, deltas)
205 }
206 stringify!(OrderBookDepth) => {
207 let depth_snapshots: Vec<OrderBookDepth> = data
208 .into_iter()
209 .map(|obj| obj.extract::<OrderBookDepth>().map_err(Into::into))
210 .collect::<PyResult<Vec<OrderBookDepth>>>()?;
211 py_book_depths_to_arrow_record_batch_bytes(py, depth_snapshots)
212 }
213 stringify!(QuoteTick) => {
214 let quotes = pyobjects_to_quotes(data)?;
215 py_quotes_to_arrow_record_batch_bytes(py, quotes)
216 }
217 stringify!(TradeTick) => {
218 let trades = pyobjects_to_trades(data)?;
219 py_trades_to_arrow_record_batch_bytes(py, trades)
220 }
221 stringify!(Bar) => {
222 let bars = pyobjects_to_bars(data)?;
223 py_bars_to_arrow_record_batch_bytes(py, bars)
224 }
225 stringify!(MarkPriceUpdate) => {
226 let updates = pyobjects_to_mark_prices(data)?;
227 py_mark_prices_to_arrow_record_batch_bytes(py, updates)
228 }
229 stringify!(IndexPriceUpdate) => {
230 let index_prices = pyobjects_to_index_prices(data)?;
231 py_index_prices_to_arrow_record_batch_bytes(py, index_prices)
232 }
233 stringify!(InstrumentStatus) => {
234 let statuses = pyobjects_to_instrument_statuses(data)?;
235 py_instrument_status_to_arrow_record_batch_bytes(py, statuses)
236 }
237 stringify!(OptionGreeks) => {
238 let greeks = pyobjects_to_option_greeks(data)?;
239 py_option_greeks_to_arrow_record_batch_bytes(py, greeks)
240 }
241 stringify!(InstrumentClose) => {
242 let closes = pyobjects_to_instrument_closes(data)?;
243 py_instrument_closes_to_arrow_record_batch_bytes(py, closes)
244 }
245 stringify!(GreeksData) => encode_py_records::<GreeksData>(py, data),
246 stringify!(PortfolioGreeks) => encode_py_records::<PortfolioGreeks>(py, data),
247 _ => Err(to_pyvalue_err(format!(
248 "unsupported data type: {data_type}"
249 ))),
250 }
251}
252
253fn encode_py_records<T>(py: Python<'_>, data: Vec<Bound<'_, PyAny>>) -> PyResult<Py<PyBytes>>
254where
255 T: EncodeToRecordBatch + pyo3::PyClass + Clone,
256{
257 let values = data
258 .into_iter()
259 .map(|obj| {
260 obj.extract::<PyRef<'_, T>>()
261 .map(|value| (*value).clone())
262 .map_err(PyErr::from)
263 })
264 .collect::<PyResult<Vec<_>>>()?;
265
266 let batch = encode_batch_with_metadata(&values).map_err(to_pyvalue_err)?;
267 arrow_record_batch_to_pybytes(py, &batch)
268}
269
270#[pyfunction(name = "book_deltas_to_arrow_record_batch_bytes")]
279#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.serialization")]
280#[expect(clippy::needless_pass_by_value)]
281pub fn py_book_deltas_to_arrow_record_batch_bytes(
282 py: Python,
283 data: Vec<OrderBookDelta>,
284) -> PyResult<Py<PyBytes>> {
285 match book_deltas_to_arrow_record_batch_bytes(&data) {
286 Ok(batch) => arrow_record_batch_to_pybytes(py, &batch),
287 Err(e) => Err(to_pyvalue_err(e)),
288 }
289}
290
291#[pyfunction(name = "book_depths_to_arrow_record_batch_bytes")]
300#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.serialization")]
301#[expect(clippy::needless_pass_by_value)]
302pub fn py_book_depths_to_arrow_record_batch_bytes(
303 py: Python,
304 data: Vec<OrderBookDepth>,
305) -> PyResult<Py<PyBytes>> {
306 match book_depths_to_arrow_record_batch_bytes(&data) {
307 Ok(batch) => arrow_record_batch_to_pybytes(py, &batch),
308 Err(e) => Err(to_pyvalue_err(e)),
309 }
310}
311
312#[pyfunction(name = "quotes_to_arrow_record_batch_bytes")]
321#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.serialization")]
322#[expect(clippy::needless_pass_by_value)]
323pub fn py_quotes_to_arrow_record_batch_bytes(
324 py: Python,
325 data: Vec<QuoteTick>,
326) -> PyResult<Py<PyBytes>> {
327 match quotes_to_arrow_record_batch_bytes(&data) {
328 Ok(batch) => arrow_record_batch_to_pybytes(py, &batch),
329 Err(e) => Err(to_pyvalue_err(e)),
330 }
331}
332
333#[pyfunction(name = "trades_to_arrow_record_batch_bytes")]
342#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.serialization")]
343#[expect(clippy::needless_pass_by_value)]
344pub fn py_trades_to_arrow_record_batch_bytes(
345 py: Python,
346 data: Vec<TradeTick>,
347) -> PyResult<Py<PyBytes>> {
348 match trades_to_arrow_record_batch_bytes(&data) {
349 Ok(batch) => arrow_record_batch_to_pybytes(py, &batch),
350 Err(e) => Err(to_pyvalue_err(e)),
351 }
352}
353
354#[pyfunction(name = "bars_to_arrow_record_batch_bytes")]
363#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.serialization")]
364#[expect(clippy::needless_pass_by_value)]
365pub fn py_bars_to_arrow_record_batch_bytes(py: Python, data: Vec<Bar>) -> PyResult<Py<PyBytes>> {
366 match bars_to_arrow_record_batch_bytes(&data) {
367 Ok(batch) => arrow_record_batch_to_pybytes(py, &batch),
368 Err(e) => Err(to_pyvalue_err(e)),
369 }
370}
371
372#[pyfunction(name = "mark_prices_to_arrow_record_batch_bytes")]
381#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.serialization")]
382#[expect(clippy::needless_pass_by_value)]
383pub fn py_mark_prices_to_arrow_record_batch_bytes(
384 py: Python,
385 data: Vec<MarkPriceUpdate>,
386) -> PyResult<Py<PyBytes>> {
387 match mark_prices_to_arrow_record_batch_bytes(&data) {
388 Ok(batch) => arrow_record_batch_to_pybytes(py, &batch),
389 Err(e) => Err(to_pyvalue_err(e)),
390 }
391}
392
393#[pyfunction(name = "index_prices_to_arrow_record_batch_bytes")]
402#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.serialization")]
403#[expect(clippy::needless_pass_by_value)]
404pub fn py_index_prices_to_arrow_record_batch_bytes(
405 py: Python,
406 data: Vec<IndexPriceUpdate>,
407) -> PyResult<Py<PyBytes>> {
408 match index_prices_to_arrow_record_batch_bytes(&data) {
409 Ok(batch) => arrow_record_batch_to_pybytes(py, &batch),
410 Err(e) => Err(to_pyvalue_err(e)),
411 }
412}
413
414#[pyfunction(name = "instrument_status_to_arrow_record_batch_bytes")]
423#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.serialization")]
424#[expect(clippy::needless_pass_by_value)]
425pub fn py_instrument_status_to_arrow_record_batch_bytes(
426 py: Python,
427 data: Vec<InstrumentStatus>,
428) -> PyResult<Py<PyBytes>> {
429 match instrument_status_to_arrow_record_batch_bytes(&data) {
430 Ok(batch) => arrow_record_batch_to_pybytes(py, &batch),
431 Err(e) => Err(to_pyvalue_err(e)),
432 }
433}
434
435#[pyfunction(name = "option_greeks_to_arrow_record_batch_bytes")]
444#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.serialization")]
445#[expect(clippy::needless_pass_by_value)]
446pub fn py_option_greeks_to_arrow_record_batch_bytes(
447 py: Python,
448 data: Vec<OptionGreeks>,
449) -> PyResult<Py<PyBytes>> {
450 match option_greeks_to_arrow_record_batch_bytes(&data) {
451 Ok(batch) => arrow_record_batch_to_pybytes(py, &batch),
452 Err(e) => Err(to_pyvalue_err(e)),
453 }
454}
455
456#[pyfunction(name = "option_greeks_from_arrow_record_batch_bytes")]
462#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.serialization")]
463pub fn py_option_greeks_from_arrow_record_batch_bytes(
464 _py: Python,
465 data: Vec<u8>,
466) -> PyResult<Vec<OptionGreeks>> {
467 let cursor = Cursor::new(data);
468 let reader = StreamReader::try_new(cursor, None).map_err(to_pyruntime_err)?;
469
470 let mut results = Vec::new();
471
472 for batch_result in reader {
473 let batch = batch_result.map_err(to_pyruntime_err)?;
474 let metadata = batch.schema().metadata().clone();
475 let decoded = OptionGreeks::decode_batch(&metadata, batch).map_err(to_pyvalue_err)?;
476 results.extend(decoded);
477 }
478
479 Ok(results)
480}
481
482#[pyfunction(name = "instrument_status_from_arrow_record_batch_bytes")]
488#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.serialization")]
489pub fn py_instrument_status_from_arrow_record_batch_bytes(
490 _py: Python,
491 data: Vec<u8>,
492) -> PyResult<Vec<InstrumentStatus>> {
493 let cursor = Cursor::new(data);
494 let reader = StreamReader::try_new(cursor, None).map_err(to_pyruntime_err)?;
495
496 let mut results = Vec::new();
497
498 for batch_result in reader {
499 let batch = batch_result.map_err(to_pyruntime_err)?;
500 let metadata = batch.schema().metadata().clone();
501 let decoded =
502 InstrumentStatus::decode_typed_batch(&metadata, batch).map_err(to_pyvalue_err)?;
503 results.extend(decoded);
504 }
505
506 Ok(results)
507}
508
509#[pyfunction(name = "instrument_closes_to_arrow_record_batch_bytes")]
518#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.serialization")]
519#[expect(clippy::needless_pass_by_value)]
520pub fn py_instrument_closes_to_arrow_record_batch_bytes(
521 py: Python,
522 data: Vec<InstrumentClose>,
523) -> PyResult<Py<PyBytes>> {
524 match instrument_closes_to_arrow_record_batch_bytes(&data) {
525 Ok(batch) => arrow_record_batch_to_pybytes(py, &batch),
526 Err(e) => Err(to_pyvalue_err(e)),
527 }
528}
529
530#[cfg(test)]
531mod tests {
532 use std::collections::HashMap;
533
534 use nautilus_model::data::stubs::stub_instrument_status;
535 use pyo3::{
536 exceptions::{PyRuntimeError, PyTypeError, PyValueError},
537 types::PyString,
538 };
539 use rstest::rstest;
540
541 use super::*;
542 use crate::arrow::EncodeToRecordBatch;
543
544 #[rstest]
545 fn test_schema_bytes_and_map_match_rust_schema() {
546 Python::initialize();
547 Python::attach(|py| {
548 let cls = py.get_type::<InstrumentStatus>();
549 let bytes = get_arrow_schema_bytes(py, &cls).unwrap();
550 let mut reader = StreamReader::try_new(Cursor::new(bytes.as_bytes(py)), None).unwrap();
551 let map: HashMap<String, String> =
552 get_arrow_schema_map(py, &cls).unwrap().extract(py).unwrap();
553
554 assert_eq!(
555 reader.schema().as_ref(),
556 &InstrumentStatus::get_schema(None)
557 );
558 assert!(reader.next().is_none());
559 assert_eq!(map, InstrumentStatus::get_schema_map());
560 });
561 }
562
563 #[rstest]
564 fn test_schema_rejects_unsupported_class() {
565 Python::initialize();
566 Python::attach(|py| {
567 let cls = py.get_type::<PyString>();
568 let errors = [
569 get_arrow_schema_bytes(py, &cls).unwrap_err(),
570 get_arrow_schema_map(py, &cls).unwrap_err(),
571 ];
572
573 for error in errors {
574 assert!(error.is_instance_of::<PyTypeError>(py));
575 assert_eq!(
576 error.value(py).to_string(),
577 "Arrow schema for `str` is not currently implemented in Rust."
578 );
579 }
580 });
581 }
582
583 #[rstest]
584 fn test_status_ipc_preserves_multiple_batches() {
585 let first = stub_instrument_status();
586 let mut second = first;
587 second.ts_event = 31.into();
588 second.ts_init = 47.into();
589 second.reason = Some("venue halt".into());
590 second.is_trading = Some(false);
591 second.is_quoting = Some(true);
592 let rows = [first, second];
593 let batches = rows
594 .iter()
595 .map(|row| InstrumentStatus::encode_batch(&row.metadata(), &[*row]).unwrap())
596 .collect::<Vec<_>>();
597 Python::initialize();
598 Python::attach(|py| {
599 let bytes =
600 arrow_record_batches_to_pybytes(py, &batches[0].schema(), &batches).unwrap();
601 let decoded =
602 py_instrument_status_from_arrow_record_batch_bytes(py, bytes.as_bytes(py).to_vec())
603 .unwrap();
604 let single =
605 py_instrument_status_to_arrow_record_batch_bytes(py, rows.to_vec()).unwrap();
606 let single_decoded = py_instrument_status_from_arrow_record_batch_bytes(
607 py,
608 single.as_bytes(py).to_vec(),
609 )
610 .unwrap();
611
612 assert_eq!(decoded, rows);
613 assert_eq!(single_decoded, rows);
614 });
615 }
616
617 #[rstest]
618 fn test_ipc_decode_rejects_invalid_stream() {
619 Python::initialize();
620 Python::attach(|py| {
621 let errors = [
622 py_instrument_status_from_arrow_record_batch_bytes(py, vec![1, 2, 3]).unwrap_err(),
623 py_option_greeks_from_arrow_record_batch_bytes(py, vec![1, 2, 3]).unwrap_err(),
624 ];
625
626 for error in errors {
627 assert!(error.is_instance_of::<PyRuntimeError>(py));
628 assert_eq!(
629 error.value(py).to_string(),
630 "Ipc error: Expected schema message, found empty stream."
631 );
632 }
633 });
634 }
635
636 #[rstest]
637 fn test_python_encoders_reject_empty_data() {
638 Python::initialize();
639 Python::attach(|py| {
640 let errors = [
641 py_book_deltas_to_arrow_record_batch_bytes(py, vec![]).unwrap_err(),
642 py_book_depths_to_arrow_record_batch_bytes(py, vec![]).unwrap_err(),
643 py_quotes_to_arrow_record_batch_bytes(py, vec![]).unwrap_err(),
644 py_trades_to_arrow_record_batch_bytes(py, vec![]).unwrap_err(),
645 py_bars_to_arrow_record_batch_bytes(py, vec![]).unwrap_err(),
646 py_mark_prices_to_arrow_record_batch_bytes(py, vec![]).unwrap_err(),
647 py_index_prices_to_arrow_record_batch_bytes(py, vec![]).unwrap_err(),
648 py_instrument_status_to_arrow_record_batch_bytes(py, vec![]).unwrap_err(),
649 py_option_greeks_to_arrow_record_batch_bytes(py, vec![]).unwrap_err(),
650 py_instrument_closes_to_arrow_record_batch_bytes(py, vec![]).unwrap_err(),
651 ];
652
653 for error in errors {
654 assert!(error.is_instance_of::<PyValueError>(py));
655 assert_eq!(
656 error.value(py).to_string(),
657 crate::arrow::EncodingError::EmptyData.to_string()
658 );
659 }
660 });
661 }
662}