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, close::InstrumentClose,
29 },
30 python::data::{
31 pyobjects_to_bars, pyobjects_to_book_deltas, pyobjects_to_index_prices,
32 pyobjects_to_instrument_closes, pyobjects_to_instrument_statuses, pyobjects_to_mark_prices,
33 pyobjects_to_option_greeks, pyobjects_to_quotes, pyobjects_to_trades,
34 },
35};
36use pyo3::{
37 conversion::IntoPyObjectExt,
38 prelude::*,
39 types::{PyBytes, PyCapsule, PyType},
40};
41
42use crate::arrow::{
43 ArrowSchemaProvider, DecodeFromRecordBatch, DecodeTypedFromRecordBatch,
44 bars_to_arrow_record_batch_bytes, book_deltas_to_arrow_record_batch_bytes,
45 book_depths_to_arrow_record_batch_bytes, index_prices_to_arrow_record_batch_bytes,
46 instrument_closes_to_arrow_record_batch_bytes, instrument_status_to_arrow_record_batch_bytes,
47 mark_prices_to_arrow_record_batch_bytes, option_greeks_to_arrow_record_batch_bytes,
48 quotes_to_arrow_record_batch_bytes, trades_to_arrow_record_batch_bytes,
49};
50
51pub fn arrow_record_batch_to_pybytes(py: Python, batch: &RecordBatch) -> PyResult<Py<PyBytes>> {
57 arrow_record_batches_to_pybytes(py, &batch.schema(), std::slice::from_ref(batch))
58}
59
60pub fn arrow_record_batches_to_pybytes(
66 py: Python,
67 schema: &Schema,
68 batches: &[RecordBatch],
69) -> PyResult<Py<PyBytes>> {
70 let mut cursor = Cursor::new(Vec::new());
71 {
72 let mut writer = StreamWriter::try_new(&mut cursor, schema).map_err(to_pyruntime_err)?;
73
74 for batch in batches {
75 writer.write(batch).map_err(to_pyruntime_err)?;
76 }
77
78 writer.finish().map_err(to_pyruntime_err)?;
79 }
80
81 let buffer = cursor.into_inner();
82 let pybytes = PyBytes::new(py, &buffer);
83
84 Ok(pybytes.into())
85}
86
87pub fn arrow_record_batches_to_pyarrow_stream(
93 py: Python<'_>,
94 schema: &Schema,
95 batches: Vec<RecordBatch>,
96) -> PyResult<Py<PyAny>> {
97 let reader = RecordBatchIterator::new(batches.into_iter().map(Ok), Arc::new(schema.clone()));
98 let stream = FFI_ArrowArrayStream::new(Box::new(reader));
99
100 let capsule = PyCapsule::new_with_value(py, stream, c"arrow_array_stream")?;
102 Ok(capsule.into_any().unbind())
103}
104
105#[pyfunction]
111#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.serialization")]
112pub fn get_arrow_schema_map(py: Python<'_>, cls: &Bound<'_, PyType>) -> PyResult<Py<PyAny>> {
113 let cls_str: String = cls.getattr("__name__")?.extract()?;
114 let result_map = match cls_str.as_str() {
115 stringify!(OrderBookDelta) => OrderBookDelta::get_schema_map(),
116 stringify!(OrderBookDepth) => OrderBookDepth::get_schema_map(),
117 stringify!(QuoteTick) => QuoteTick::get_schema_map(),
118 stringify!(TradeTick) => TradeTick::get_schema_map(),
119 stringify!(Bar) => Bar::get_schema_map(),
120 stringify!(MarkPriceUpdate) => MarkPriceUpdate::get_schema_map(),
121 stringify!(IndexPriceUpdate) => IndexPriceUpdate::get_schema_map(),
122 stringify!(FundingRateUpdate) => FundingRateUpdate::get_schema_map(),
123 stringify!(InstrumentStatus) => InstrumentStatus::get_schema_map(),
124 stringify!(OptionGreeks) => OptionGreeks::get_schema_map(),
125 stringify!(InstrumentClose) => InstrumentClose::get_schema_map(),
126 _ => {
127 return Err(to_pytype_err(format!(
128 "Arrow schema for `{cls_str}` is not currently implemented in Rust."
129 )));
130 }
131 };
132
133 result_map.into_py_any(py)
134}
135
136#[pyfunction]
142#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.serialization")]
143pub fn get_arrow_schema_bytes(py: Python<'_>, cls: &Bound<'_, PyType>) -> PyResult<Py<PyBytes>> {
144 let cls_str: String = cls.getattr("__name__")?.extract()?;
145 let schema = match cls_str.as_str() {
146 stringify!(OrderBookDelta) => OrderBookDelta::get_schema(None),
147 stringify!(OrderBookDepth) => OrderBookDepth::get_schema(None),
148 stringify!(QuoteTick) => QuoteTick::get_schema(None),
149 stringify!(TradeTick) => TradeTick::get_schema(None),
150 stringify!(Bar) => Bar::get_schema(None),
151 stringify!(MarkPriceUpdate) => MarkPriceUpdate::get_schema(None),
152 stringify!(IndexPriceUpdate) => IndexPriceUpdate::get_schema(None),
153 stringify!(FundingRateUpdate) => FundingRateUpdate::get_schema(None),
154 stringify!(InstrumentStatus) => InstrumentStatus::get_schema(None),
155 stringify!(OptionGreeks) => OptionGreeks::get_schema(None),
156 stringify!(InstrumentClose) => InstrumentClose::get_schema(None),
157 _ => {
158 return Err(to_pytype_err(format!(
159 "Arrow schema for `{cls_str}` is not currently implemented in Rust."
160 )));
161 }
162 };
163
164 arrow_record_batches_to_pybytes(py, &schema, &[])
165}
166
167#[pyfunction]
169#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.serialization")]
170#[expect(clippy::missing_panics_doc)] pub fn pyobjects_to_arrow_record_batch_bytes(
172 py: Python,
173 data: Vec<Bound<'_, PyAny>>,
174) -> PyResult<Py<PyBytes>> {
175 if data.is_empty() {
176 return Err(to_pyvalue_err("Empty data"));
177 }
178
179 let data_type: String = data
180 .first()
181 .unwrap() .getattr("__class__")?
183 .getattr("__name__")?
184 .extract()?;
185
186 match data_type.as_str() {
187 stringify!(OrderBookDelta) => {
188 let deltas = pyobjects_to_book_deltas(data)?;
189 py_book_deltas_to_arrow_record_batch_bytes(py, deltas)
190 }
191 stringify!(OrderBookDepth) => {
192 let depth_snapshots: Vec<OrderBookDepth> = data
193 .into_iter()
194 .map(|obj| obj.extract::<OrderBookDepth>().map_err(Into::into))
195 .collect::<PyResult<Vec<OrderBookDepth>>>()?;
196 py_book_depths_to_arrow_record_batch_bytes(py, depth_snapshots)
197 }
198 stringify!(QuoteTick) => {
199 let quotes = pyobjects_to_quotes(data)?;
200 py_quotes_to_arrow_record_batch_bytes(py, quotes)
201 }
202 stringify!(TradeTick) => {
203 let trades = pyobjects_to_trades(data)?;
204 py_trades_to_arrow_record_batch_bytes(py, trades)
205 }
206 stringify!(Bar) => {
207 let bars = pyobjects_to_bars(data)?;
208 py_bars_to_arrow_record_batch_bytes(py, bars)
209 }
210 stringify!(MarkPriceUpdate) => {
211 let updates = pyobjects_to_mark_prices(data)?;
212 py_mark_prices_to_arrow_record_batch_bytes(py, updates)
213 }
214 stringify!(IndexPriceUpdate) => {
215 let index_prices = pyobjects_to_index_prices(data)?;
216 py_index_prices_to_arrow_record_batch_bytes(py, index_prices)
217 }
218 stringify!(InstrumentStatus) => {
219 let statuses = pyobjects_to_instrument_statuses(data)?;
220 py_instrument_status_to_arrow_record_batch_bytes(py, statuses)
221 }
222 stringify!(OptionGreeks) => {
223 let greeks = pyobjects_to_option_greeks(data)?;
224 py_option_greeks_to_arrow_record_batch_bytes(py, greeks)
225 }
226 stringify!(InstrumentClose) => {
227 let closes = pyobjects_to_instrument_closes(data)?;
228 py_instrument_closes_to_arrow_record_batch_bytes(py, closes)
229 }
230 _ => Err(to_pyvalue_err(format!(
231 "unsupported data type: {data_type}"
232 ))),
233 }
234}
235
236#[pyfunction(name = "book_deltas_to_arrow_record_batch_bytes")]
246#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.serialization")]
247#[expect(clippy::needless_pass_by_value)]
248pub fn py_book_deltas_to_arrow_record_batch_bytes(
249 py: Python,
250 data: Vec<OrderBookDelta>,
251) -> PyResult<Py<PyBytes>> {
252 match book_deltas_to_arrow_record_batch_bytes(&data) {
253 Ok(batch) => arrow_record_batch_to_pybytes(py, &batch),
254 Err(e) => Err(to_pyvalue_err(e)),
255 }
256}
257
258#[pyfunction(name = "book_depths_to_arrow_record_batch_bytes")]
267#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.serialization")]
268#[expect(clippy::needless_pass_by_value)]
269pub fn py_book_depths_to_arrow_record_batch_bytes(
270 py: Python,
271 data: Vec<OrderBookDepth>,
272) -> PyResult<Py<PyBytes>> {
273 match book_depths_to_arrow_record_batch_bytes(&data) {
274 Ok(batch) => arrow_record_batch_to_pybytes(py, &batch),
275 Err(e) => Err(to_pyvalue_err(e)),
276 }
277}
278
279#[pyfunction(name = "quotes_to_arrow_record_batch_bytes")]
288#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.serialization")]
289#[expect(clippy::needless_pass_by_value)]
290pub fn py_quotes_to_arrow_record_batch_bytes(
291 py: Python,
292 data: Vec<QuoteTick>,
293) -> PyResult<Py<PyBytes>> {
294 match quotes_to_arrow_record_batch_bytes(&data) {
295 Ok(batch) => arrow_record_batch_to_pybytes(py, &batch),
296 Err(e) => Err(to_pyvalue_err(e)),
297 }
298}
299
300#[pyfunction(name = "trades_to_arrow_record_batch_bytes")]
309#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.serialization")]
310#[expect(clippy::needless_pass_by_value)]
311pub fn py_trades_to_arrow_record_batch_bytes(
312 py: Python,
313 data: Vec<TradeTick>,
314) -> PyResult<Py<PyBytes>> {
315 match trades_to_arrow_record_batch_bytes(&data) {
316 Ok(batch) => arrow_record_batch_to_pybytes(py, &batch),
317 Err(e) => Err(to_pyvalue_err(e)),
318 }
319}
320
321#[pyfunction(name = "bars_to_arrow_record_batch_bytes")]
330#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.serialization")]
331#[expect(clippy::needless_pass_by_value)]
332pub fn py_bars_to_arrow_record_batch_bytes(py: Python, data: Vec<Bar>) -> PyResult<Py<PyBytes>> {
333 match bars_to_arrow_record_batch_bytes(&data) {
334 Ok(batch) => arrow_record_batch_to_pybytes(py, &batch),
335 Err(e) => Err(to_pyvalue_err(e)),
336 }
337}
338
339#[pyfunction(name = "mark_prices_to_arrow_record_batch_bytes")]
348#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.serialization")]
349#[expect(clippy::needless_pass_by_value)]
350pub fn py_mark_prices_to_arrow_record_batch_bytes(
351 py: Python,
352 data: Vec<MarkPriceUpdate>,
353) -> PyResult<Py<PyBytes>> {
354 match mark_prices_to_arrow_record_batch_bytes(&data) {
355 Ok(batch) => arrow_record_batch_to_pybytes(py, &batch),
356 Err(e) => Err(to_pyvalue_err(e)),
357 }
358}
359
360#[pyfunction(name = "index_prices_to_arrow_record_batch_bytes")]
369#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.serialization")]
370#[expect(clippy::needless_pass_by_value)]
371pub fn py_index_prices_to_arrow_record_batch_bytes(
372 py: Python,
373 data: Vec<IndexPriceUpdate>,
374) -> PyResult<Py<PyBytes>> {
375 match index_prices_to_arrow_record_batch_bytes(&data) {
376 Ok(batch) => arrow_record_batch_to_pybytes(py, &batch),
377 Err(e) => Err(to_pyvalue_err(e)),
378 }
379}
380
381#[pyfunction(name = "instrument_status_to_arrow_record_batch_bytes")]
389#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.serialization")]
390#[expect(clippy::needless_pass_by_value)]
391pub fn py_instrument_status_to_arrow_record_batch_bytes(
392 py: Python,
393 data: Vec<InstrumentStatus>,
394) -> PyResult<Py<PyBytes>> {
395 match instrument_status_to_arrow_record_batch_bytes(&data) {
396 Ok(batch) => arrow_record_batch_to_pybytes(py, &batch),
397 Err(e) => Err(to_pyvalue_err(e)),
398 }
399}
400
401#[pyfunction(name = "option_greeks_to_arrow_record_batch_bytes")]
409#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.serialization")]
410#[expect(clippy::needless_pass_by_value)]
411pub fn py_option_greeks_to_arrow_record_batch_bytes(
412 py: Python,
413 data: Vec<OptionGreeks>,
414) -> PyResult<Py<PyBytes>> {
415 match option_greeks_to_arrow_record_batch_bytes(&data) {
416 Ok(batch) => arrow_record_batch_to_pybytes(py, &batch),
417 Err(e) => Err(to_pyvalue_err(e)),
418 }
419}
420
421#[pyfunction(name = "option_greeks_from_arrow_record_batch_bytes")]
427#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.serialization")]
428pub fn py_option_greeks_from_arrow_record_batch_bytes(
429 _py: Python,
430 data: Vec<u8>,
431) -> PyResult<Vec<OptionGreeks>> {
432 let cursor = Cursor::new(data);
433 let reader = StreamReader::try_new(cursor, None).map_err(to_pyruntime_err)?;
434
435 let mut results = Vec::new();
436
437 for batch_result in reader {
438 let batch = batch_result.map_err(to_pyruntime_err)?;
439 let metadata = batch.schema().metadata().clone();
440 let decoded = OptionGreeks::decode_batch(&metadata, batch).map_err(to_pyvalue_err)?;
441 results.extend(decoded);
442 }
443
444 Ok(results)
445}
446
447#[pyfunction(name = "instrument_status_from_arrow_record_batch_bytes")]
453#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.serialization")]
454pub fn py_instrument_status_from_arrow_record_batch_bytes(
455 _py: Python,
456 data: Vec<u8>,
457) -> PyResult<Vec<InstrumentStatus>> {
458 let cursor = Cursor::new(data);
459 let reader = StreamReader::try_new(cursor, None).map_err(to_pyruntime_err)?;
460
461 let mut results = Vec::new();
462
463 for batch_result in reader {
464 let batch = batch_result.map_err(to_pyruntime_err)?;
465 let metadata = batch.schema().metadata().clone();
466 let decoded =
467 InstrumentStatus::decode_typed_batch(&metadata, batch).map_err(to_pyvalue_err)?;
468 results.extend(decoded);
469 }
470
471 Ok(results)
472}
473
474#[pyfunction(name = "instrument_closes_to_arrow_record_batch_bytes")]
483#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.serialization")]
484#[expect(clippy::needless_pass_by_value)]
485pub fn py_instrument_closes_to_arrow_record_batch_bytes(
486 py: Python,
487 data: Vec<InstrumentClose>,
488) -> PyResult<Py<PyBytes>> {
489 match instrument_closes_to_arrow_record_batch_bytes(&data) {
490 Ok(batch) => arrow_record_batch_to_pybytes(py, &batch),
491 Err(e) => Err(to_pyvalue_err(e)),
492 }
493}
494
495#[cfg(test)]
496mod tests {
497 use std::collections::HashMap;
498
499 use nautilus_model::data::stubs::stub_instrument_status;
500 use pyo3::{
501 exceptions::{PyRuntimeError, PyTypeError, PyValueError},
502 types::PyString,
503 };
504 use rstest::rstest;
505
506 use super::*;
507 use crate::arrow::EncodeToRecordBatch;
508
509 #[rstest]
510 fn test_schema_bytes_and_map_match_rust_schema() {
511 Python::initialize();
512 Python::attach(|py| {
513 let cls = py.get_type::<InstrumentStatus>();
514 let bytes = get_arrow_schema_bytes(py, &cls).unwrap();
515 let mut reader = StreamReader::try_new(Cursor::new(bytes.as_bytes(py)), None).unwrap();
516 let map: HashMap<String, String> =
517 get_arrow_schema_map(py, &cls).unwrap().extract(py).unwrap();
518
519 assert_eq!(
520 reader.schema().as_ref(),
521 &InstrumentStatus::get_schema(None)
522 );
523 assert!(reader.next().is_none());
524 assert_eq!(map, InstrumentStatus::get_schema_map());
525 });
526 }
527
528 #[rstest]
529 fn test_schema_rejects_unsupported_class() {
530 Python::initialize();
531 Python::attach(|py| {
532 let cls = py.get_type::<PyString>();
533 let errors = [
534 get_arrow_schema_bytes(py, &cls).unwrap_err(),
535 get_arrow_schema_map(py, &cls).unwrap_err(),
536 ];
537
538 for error in errors {
539 assert!(error.is_instance_of::<PyTypeError>(py));
540 assert_eq!(
541 error.value(py).to_string(),
542 "Arrow schema for `str` is not currently implemented in Rust."
543 );
544 }
545 });
546 }
547
548 #[rstest]
549 fn test_status_ipc_preserves_multiple_batches() {
550 let first = stub_instrument_status();
551 let mut second = first;
552 second.ts_event = 31.into();
553 second.ts_init = 47.into();
554 second.reason = Some("venue halt".into());
555 second.is_trading = Some(false);
556 second.is_quoting = Some(true);
557 let rows = [first, second];
558 let batches = rows
559 .iter()
560 .map(|row| InstrumentStatus::encode_batch(&row.metadata(), &[*row]).unwrap())
561 .collect::<Vec<_>>();
562 Python::initialize();
563 Python::attach(|py| {
564 let bytes =
565 arrow_record_batches_to_pybytes(py, &batches[0].schema(), &batches).unwrap();
566 let decoded =
567 py_instrument_status_from_arrow_record_batch_bytes(py, bytes.as_bytes(py).to_vec())
568 .unwrap();
569 let single =
570 py_instrument_status_to_arrow_record_batch_bytes(py, rows.to_vec()).unwrap();
571 let single_decoded = py_instrument_status_from_arrow_record_batch_bytes(
572 py,
573 single.as_bytes(py).to_vec(),
574 )
575 .unwrap();
576
577 assert_eq!(decoded, rows);
578 assert_eq!(single_decoded, rows);
579 });
580 }
581
582 #[rstest]
583 fn test_ipc_decode_rejects_invalid_stream() {
584 Python::initialize();
585 Python::attach(|py| {
586 let errors = [
587 py_instrument_status_from_arrow_record_batch_bytes(py, vec![1, 2, 3]).unwrap_err(),
588 py_option_greeks_from_arrow_record_batch_bytes(py, vec![1, 2, 3]).unwrap_err(),
589 ];
590
591 for error in errors {
592 assert!(error.is_instance_of::<PyRuntimeError>(py));
593 assert_eq!(
594 error.value(py).to_string(),
595 "Ipc error: Expected schema message, found empty stream."
596 );
597 }
598 });
599 }
600
601 #[rstest]
602 fn test_python_encoders_reject_empty_data() {
603 Python::initialize();
604 Python::attach(|py| {
605 let errors = [
606 py_book_deltas_to_arrow_record_batch_bytes(py, vec![]).unwrap_err(),
607 py_book_depths_to_arrow_record_batch_bytes(py, vec![]).unwrap_err(),
608 py_quotes_to_arrow_record_batch_bytes(py, vec![]).unwrap_err(),
609 py_trades_to_arrow_record_batch_bytes(py, vec![]).unwrap_err(),
610 py_bars_to_arrow_record_batch_bytes(py, vec![]).unwrap_err(),
611 py_mark_prices_to_arrow_record_batch_bytes(py, vec![]).unwrap_err(),
612 py_index_prices_to_arrow_record_batch_bytes(py, vec![]).unwrap_err(),
613 py_instrument_status_to_arrow_record_batch_bytes(py, vec![]).unwrap_err(),
614 py_option_greeks_to_arrow_record_batch_bytes(py, vec![]).unwrap_err(),
615 py_instrument_closes_to_arrow_record_batch_bytes(py, vec![]).unwrap_err(),
616 ];
617
618 for error in errors {
619 assert!(error.is_instance_of::<PyValueError>(py));
620 assert_eq!(
621 error.value(py).to_string(),
622 crate::arrow::EncodingError::EmptyData.to_string()
623 );
624 }
625 });
626 }
627}