1use std::{
19 collections::hash_map::DefaultHasher,
20 hash::{Hash, Hasher},
21};
22
23pub mod bar;
24pub mod bet;
25pub mod close;
26pub mod data_type;
27pub mod delta;
28pub mod deltas;
29pub mod depth;
30pub mod funding;
31pub mod greeks;
32pub mod option_chain;
33pub mod order;
34pub mod prices;
35pub mod quote;
36pub mod status;
37pub mod trade;
38
39#[cfg(feature = "python")]
40pub mod custom;
41
42#[cfg(feature = "python")]
43use nautilus_core::python::{to_pyruntime_err, to_pytype_err, to_pyvalue_err};
44use pyo3::prelude::*;
45
46use crate::{
47 data::{
48 Bar, CustomData, Data, FundingRateUpdate, IndexPriceUpdate, InstrumentStatus,
49 MarkPriceUpdate, NautilusDataType, NautilusRecordType, OptionGreeks, OrderBookDelta,
50 QuoteTick, TradeTick, close::InstrumentClose, is_monotonically_increasing_by_init,
51 register_python_data_class,
52 },
53 python::instruments::instrument_any_to_pyobject,
54};
55
56const ERROR_MONOTONICITY: &str = "`data` was not monotonically increasing by the `ts_init` field";
57
58#[derive(Clone, Debug, Eq, Hash, PartialEq)]
59#[pyclass(
60 frozen,
61 name = "NautilusDataType",
62 module = "nautilus_trader.model",
63 skip_from_py_object
64)]
65#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")]
66pub struct PyNautilusDataType {
67 inner: NautilusDataType,
68}
69
70impl PyNautilusDataType {
71 #[must_use]
72 pub const fn new(inner: NautilusDataType) -> Self {
73 Self { inner }
74 }
75
76 #[must_use]
77 pub fn into_inner(self) -> NautilusDataType {
78 self.inner
79 }
80
81 #[must_use]
82 pub fn inner(&self) -> NautilusDataType {
83 self.inner.clone()
84 }
85}
86
87macro_rules! define_nautilus_data_type_class_attrs {
88 (
89 $(($variant:ident, $type:ident, $data:ident, $batch:ident, $prefix:literal)),+ $(,)?
90 ) => {
91 #[pyo3_stub_gen::derive::gen_stub_pymethods]
92 #[pymethods]
93 impl PyNautilusDataType {
94 $(
95 #[classattr]
96 #[expect(
97 non_snake_case,
98 reason = "Python selector attributes match Rust data type variant names"
99 )]
100 fn $variant() -> PyNautilusDataType {
101 Self::new(NautilusDataType::$variant)
102 }
103 )+
104 }
105 };
106}
107
108crate::for_each_data_type!(define_nautilus_data_type_class_attrs);
109
110#[pyo3_stub_gen::derive::gen_stub_pymethods]
111#[pymethods]
112impl PyNautilusDataType {
113 #[cfg(feature = "defi")]
114 #[classattr]
115 #[expect(
116 clippy::use_self,
117 reason = "PyO3 stub generation needs the concrete return type"
118 )]
119 #[expect(
120 non_snake_case,
121 reason = "Python selector attributes match Rust data type variant names"
122 )]
123 fn Defi() -> PyNautilusDataType {
124 Self::new(NautilusDataType::Defi)
125 }
126
127 #[new]
128 fn py_new(value: &str) -> PyResult<Self> {
129 value
130 .parse::<NautilusDataType>()
131 .map(Self::new)
132 .map_err(to_pyruntime_err)
133 }
134
135 #[staticmethod]
136 #[pyo3(name = "Custom")]
137 fn py_custom(type_name: String) -> Self {
138 Self::new(NautilusDataType::Custom { type_name })
139 }
140
141 #[getter]
142 fn type_name(&self) -> Option<&str> {
143 match &self.inner {
144 NautilusDataType::Custom { type_name } => Some(type_name),
145 _ => None,
146 }
147 }
148
149 fn __str__(&self) -> String {
150 self.inner.to_string()
151 }
152
153 fn __repr__(&self) -> String {
154 match &self.inner {
155 NautilusDataType::Custom { type_name } => {
156 format!("NautilusDataType.Custom({type_name:?})")
157 }
158 _ => format!("NautilusDataType.{}", self.inner),
159 }
160 }
161
162 fn __richcmp__(&self, other: &Self, op: pyo3::pyclass::CompareOp, py: Python<'_>) -> Py<PyAny> {
163 use nautilus_core::python::IntoPyObjectNautilusExt;
164
165 match op {
166 pyo3::pyclass::CompareOp::Eq => (self.inner == other.inner).into_py_any_unwrap(py),
167 pyo3::pyclass::CompareOp::Ne => (self.inner != other.inner).into_py_any_unwrap(py),
168 _ => py.NotImplemented(),
169 }
170 }
171
172 fn __hash__(&self) -> isize {
173 let mut hasher = DefaultHasher::new();
174 self.inner.hash(&mut hasher);
175 hasher.finish() as isize
176 }
177}
178
179#[derive(Clone, Debug, Eq, Hash, PartialEq)]
180#[pyclass(
181 frozen,
182 name = "NautilusRecordType",
183 module = "nautilus_trader.model",
184 skip_from_py_object
185)]
186#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")]
187pub struct PyNautilusRecordType {
188 inner: NautilusRecordType,
189}
190
191impl PyNautilusRecordType {
192 #[must_use]
193 pub const fn new(inner: NautilusRecordType) -> Self {
194 Self { inner }
195 }
196
197 #[must_use]
198 pub fn into_inner(self) -> NautilusRecordType {
199 self.inner
200 }
201
202 #[must_use]
203 pub const fn inner(&self) -> NautilusRecordType {
204 self.inner
205 }
206}
207
208#[pymethods]
209#[pyo3_stub_gen::derive::gen_stub_pymethods]
210impl PyNautilusRecordType {
211 #[new]
212 fn py_new(value: &str) -> PyResult<Self> {
213 value
214 .parse::<NautilusRecordType>()
215 .map(Self::new)
216 .map_err(to_pyruntime_err)
217 }
218
219 fn __str__(&self) -> String {
220 self.inner.to_string()
221 }
222
223 fn __repr__(&self) -> String {
224 format!("NautilusRecordType.{}", self.inner)
225 }
226
227 fn __richcmp__(&self, other: &Self, op: pyo3::pyclass::CompareOp, py: Python<'_>) -> Py<PyAny> {
228 use nautilus_core::python::IntoPyObjectNautilusExt;
229
230 match op {
231 pyo3::pyclass::CompareOp::Eq => (self.inner == other.inner).into_py_any_unwrap(py),
232 pyo3::pyclass::CompareOp::Ne => (self.inner != other.inner).into_py_any_unwrap(py),
233 _ => py.NotImplemented(),
234 }
235 }
236
237 fn __hash__(&self) -> isize {
238 let mut hasher = DefaultHasher::new();
239 self.inner.hash(&mut hasher);
240 hasher.finish() as isize
241 }
242}
243
244pub fn data_to_pyobject(py: Python<'_>, data: Data) -> PyResult<Py<PyAny>> {
250 match data {
251 Data::Custom(custom) => Py::new(py, custom).map(Py::into_any),
252 Data::Instrument(instrument) => instrument_any_to_pyobject(py, *instrument),
253 Data::Quote(quote) => Py::new(py, quote).map(Py::into_any),
254 Data::Trade(trade) => Py::new(py, trade).map(Py::into_any),
255 Data::Bar(bar) => Py::new(py, bar).map(Py::into_any),
256 Data::BookDelta(delta) => Py::new(py, delta).map(Py::into_any),
257 Data::BookDeltas(deltas) => Py::new(py, (*deltas).clone()).map(Py::into_any),
258 Data::BookDepth(depth) => Py::new(py, *depth).map(Py::into_any),
259 Data::MarkPrice(price) => Py::new(py, price).map(Py::into_any),
260 Data::IndexPrice(price) => Py::new(py, price).map(Py::into_any),
261 Data::FundingRate(funding) => Py::new(py, funding).map(Py::into_any),
262 Data::OptionGreeks(greeks) => Py::new(py, greeks).map(Py::into_any),
263 Data::InstrumentStatus(status) => Py::new(py, status).map(Py::into_any),
264 Data::InstrumentClose(close) => Py::new(py, close).map(Py::into_any),
265 #[cfg(feature = "defi")]
266 Data::Defi(_) => Err(to_pytype_err("Unsupported DeFi data variant")),
267 }
268}
269
270pub fn pyobjects_to_book_deltas(data: Vec<Bound<'_, PyAny>>) -> PyResult<Vec<OrderBookDelta>> {
276 let deltas: Vec<OrderBookDelta> = data
277 .into_iter()
278 .map(|obj| obj.extract::<OrderBookDelta>().map_err(PyErr::from))
279 .collect::<PyResult<Vec<OrderBookDelta>>>()?;
280
281 if !is_monotonically_increasing_by_init(&deltas) {
283 return Err(to_pyvalue_err(ERROR_MONOTONICITY));
284 }
285
286 Ok(deltas)
287}
288
289pub fn pyobjects_to_quotes(data: Vec<Bound<'_, PyAny>>) -> PyResult<Vec<QuoteTick>> {
295 let quotes: Vec<QuoteTick> = data
296 .into_iter()
297 .map(|obj| obj.extract::<QuoteTick>().map_err(PyErr::from))
298 .collect::<PyResult<Vec<QuoteTick>>>()?;
299
300 if !is_monotonically_increasing_by_init("es) {
302 return Err(to_pyvalue_err(ERROR_MONOTONICITY));
303 }
304
305 Ok(quotes)
306}
307
308pub fn pyobjects_to_trades(data: Vec<Bound<'_, PyAny>>) -> PyResult<Vec<TradeTick>> {
314 let trades: Vec<TradeTick> = data
315 .into_iter()
316 .map(|obj| obj.extract::<TradeTick>().map_err(PyErr::from))
317 .collect::<PyResult<Vec<TradeTick>>>()?;
318
319 if !is_monotonically_increasing_by_init(&trades) {
321 return Err(to_pyvalue_err(ERROR_MONOTONICITY));
322 }
323
324 Ok(trades)
325}
326
327pub fn pyobjects_to_bars(data: Vec<Bound<'_, PyAny>>) -> PyResult<Vec<Bar>> {
333 let bars: Vec<Bar> = data
334 .into_iter()
335 .map(|obj| obj.extract::<Bar>().map_err(PyErr::from))
336 .collect::<PyResult<Vec<Bar>>>()?;
337
338 if !is_monotonically_increasing_by_init(&bars) {
340 return Err(to_pyvalue_err(ERROR_MONOTONICITY));
341 }
342
343 Ok(bars)
344}
345
346pub fn pyobjects_to_mark_prices(data: Vec<Bound<'_, PyAny>>) -> PyResult<Vec<MarkPriceUpdate>> {
352 let mark_prices: Vec<MarkPriceUpdate> = data
353 .into_iter()
354 .map(|obj| obj.extract::<MarkPriceUpdate>().map_err(PyErr::from))
355 .collect::<PyResult<Vec<MarkPriceUpdate>>>()?;
356
357 if !is_monotonically_increasing_by_init(&mark_prices) {
359 return Err(to_pyvalue_err(ERROR_MONOTONICITY));
360 }
361
362 Ok(mark_prices)
363}
364
365pub fn pyobjects_to_index_prices(data: Vec<Bound<'_, PyAny>>) -> PyResult<Vec<IndexPriceUpdate>> {
371 let index_prices: Vec<IndexPriceUpdate> = data
372 .into_iter()
373 .map(|obj| obj.extract::<IndexPriceUpdate>().map_err(PyErr::from))
374 .collect::<PyResult<Vec<IndexPriceUpdate>>>()?;
375
376 if !is_monotonically_increasing_by_init(&index_prices) {
378 return Err(to_pyvalue_err(ERROR_MONOTONICITY));
379 }
380
381 Ok(index_prices)
382}
383
384pub fn pyobjects_to_instrument_statuses(
390 data: Vec<Bound<'_, PyAny>>,
391) -> PyResult<Vec<InstrumentStatus>> {
392 let statuses: Vec<InstrumentStatus> = data
393 .into_iter()
394 .map(|obj| obj.extract::<InstrumentStatus>().map_err(PyErr::from))
395 .collect::<PyResult<Vec<InstrumentStatus>>>()?;
396
397 if !is_monotonically_increasing_by_init(&statuses) {
398 return Err(to_pyvalue_err(ERROR_MONOTONICITY));
399 }
400
401 Ok(statuses)
402}
403
404pub fn pyobjects_to_option_greeks(data: Vec<Bound<'_, PyAny>>) -> PyResult<Vec<OptionGreeks>> {
410 let greeks: Vec<OptionGreeks> = data
411 .into_iter()
412 .map(|obj| obj.extract::<OptionGreeks>().map_err(PyErr::from))
413 .collect::<PyResult<Vec<OptionGreeks>>>()?;
414
415 if !is_monotonically_increasing_by_init(&greeks) {
416 return Err(to_pyvalue_err(ERROR_MONOTONICITY));
417 }
418
419 Ok(greeks)
420}
421
422pub fn pyobjects_to_instrument_closes(
428 data: Vec<Bound<'_, PyAny>>,
429) -> PyResult<Vec<InstrumentClose>> {
430 let closes: Vec<InstrumentClose> = data
431 .into_iter()
432 .map(|obj| obj.extract::<InstrumentClose>().map_err(PyErr::from))
433 .collect::<PyResult<Vec<InstrumentClose>>>()?;
434
435 if !is_monotonically_increasing_by_init(&closes) {
437 return Err(to_pyvalue_err(ERROR_MONOTONICITY));
438 }
439
440 Ok(closes)
441}
442
443#[cfg(feature = "python")]
449#[pyfunction]
450pub fn deserialize_custom_from_json(type_name: &str, payload: &[u8]) -> PyResult<CustomData> {
451 use crate::data::registry;
452 let value: serde_json::Value = serde_json::from_slice(payload)
453 .map_err(|e| to_pyvalue_err(format!("Invalid JSON: {e}")))?;
454 let Some(Data::Custom(custom)) = registry::deserialize_custom_from_json(type_name, &value)
455 .map_err(|e| to_pyvalue_err(format!("Deserialization failed: {e}")))?
456 else {
457 return Err(to_pyvalue_err(format!(
458 "Custom data type \"{type_name}\" is not registered"
459 )));
460 };
461 Ok(custom)
462}
463
464#[cfg(feature = "python")]
466fn py_json_deserialize_custom_data(
467 data_class: &pyo3::Py<pyo3::PyAny>,
468 value: &serde_json::Value,
469) -> Result<std::sync::Arc<dyn crate::data::CustomDataTrait>, anyhow::Error> {
470 use std::sync::Arc;
471
472 use crate::data::PythonCustomDataWrapper;
473
474 pyo3::Python::attach(|py| {
475 let json_str = serde_json::to_string(&value)?;
476 let json_module = py
477 .import("json")
478 .map_err(|e| anyhow::anyhow!("Failed to import json: {e}"))?;
479 let py_dict = json_module
480 .call_method1("loads", (json_str,))
481 .map_err(|e| anyhow::anyhow!("Failed to parse JSON: {e}"))?;
482
483 let instance = data_class
484 .bind(py)
485 .call_method1("from_json", (py_dict,))
486 .map_err(|e| anyhow::anyhow!("Failed to call from_json: {e}"))?;
487
488 let wrapper = PythonCustomDataWrapper::new(py, &instance)
489 .map_err(|e| anyhow::anyhow!("Failed to create wrapper: {e}"))?;
490
491 Ok(Arc::new(wrapper) as Arc<dyn crate::data::CustomDataTrait>)
492 })
493}
494
495#[allow(unsafe_code)]
497#[cfg(all(feature = "python", feature = "arrow"))]
498fn py_encode_custom_data_to_record_batch(
499 items: &[std::sync::Arc<dyn crate::data::CustomDataTrait>],
500) -> Result<arrow::record_batch::RecordBatch, anyhow::Error> {
501 pyo3::Python::attach(|py| {
502 let py_items: Result<Vec<_>, _> = items.iter().map(|item| item.to_pyobject(py)).collect();
503 let py_items = py_items.map_err(|e| anyhow::anyhow!("Failed to convert to Python: {e}"))?;
504 let py_list = pyo3::types::PyList::new(py, &py_items)
505 .map_err(|e| anyhow::anyhow!("Failed to create list: {e}"))?;
506
507 let first = items
508 .first()
509 .ok_or_else(|| anyhow::anyhow!("No items to encode"))?;
510 let first_py = first.to_pyobject(py)?;
511
512 if first_py
513 .bind(py)
514 .hasattr("encode_record_batch_py")
515 .unwrap_or(false)
516 {
517 let py_batch = first_py
518 .bind(py)
519 .call_method1("encode_record_batch_py", (py_list,))
520 .map_err(|e| anyhow::anyhow!("Failed to call encode_record_batch_py: {e}"))?;
521
522 let mut ffi_array = arrow::ffi::FFI_ArrowArray::empty();
523 let mut ffi_schema = arrow::ffi::FFI_ArrowSchema::empty();
524
525 py_batch.call_method1(
526 "_export_to_c",
527 (
528 (&raw mut ffi_array as usize),
529 (&raw mut ffi_schema as usize),
530 ),
531 )?;
532
533 let schema = std::sync::Arc::new(arrow::datatypes::Schema::try_from(&ffi_schema)?);
534 let struct_array_data = unsafe {
535 arrow::ffi::from_ffi_and_data_type(
536 ffi_array,
537 arrow::datatypes::DataType::Struct(schema.fields().clone()),
538 )?
539 };
540 let struct_array = arrow::array::StructArray::from(struct_array_data);
541 Ok(arrow::record_batch::RecordBatch::from(&struct_array))
542 } else {
543 anyhow::bail!("Instances must have encode_record_batch_py method")
544 }
545 })
546}
547
548#[cfg(all(feature = "python", feature = "arrow"))]
549fn pyarrow_schema_to_arrow_schema(
550 py_schema: &pyo3::Bound<'_, pyo3::PyAny>,
551) -> PyResult<arrow::datatypes::Schema> {
552 let mut ffi_schema = arrow::ffi::FFI_ArrowSchema::empty();
553 py_schema.call_method1("_export_to_c", ((&raw mut ffi_schema as usize),))?;
554 arrow::datatypes::Schema::try_from(&ffi_schema)
555 .map_err(|e| to_pyvalue_err(format!("Failed to import PyArrow schema: {e}")))
556}
557
558#[allow(unsafe_code)]
560#[cfg(all(feature = "python", feature = "arrow"))]
561fn py_decode_record_batch_to_custom_data(
562 data_class: &pyo3::Py<pyo3::PyAny>,
563 metadata: &std::collections::HashMap<String, String>,
564 batch: arrow::record_batch::RecordBatch,
565) -> Result<Vec<crate::data::Data>, anyhow::Error> {
566 use std::sync::Arc;
567
568 use crate::data::PythonCustomDataWrapper;
569
570 pyo3::Python::attach(|py| {
571 let struct_array: arrow::array::StructArray = batch.into();
572 let array_data = arrow::array::Array::to_data(&struct_array);
573 let mut ffi_array = arrow::ffi::FFI_ArrowArray::new(&array_data);
574 let fields = match arrow::array::Array::data_type(&struct_array) {
575 arrow::datatypes::DataType::Struct(f) => f.clone(),
576 _ => unreachable!(),
577 };
578 let mut ffi_schema =
579 arrow::ffi::FFI_ArrowSchema::try_from(arrow::datatypes::DataType::Struct(fields))?;
580
581 let pyarrow = py.import("pyarrow")?;
582 let cls = pyarrow.getattr("RecordBatch")?;
583 let py_batch = cls.call_method1(
584 "_import_from_c",
585 (
586 (&raw mut ffi_array as usize),
587 (&raw mut ffi_schema as usize),
588 ),
589 )?;
590
591 let metadata_py = pyo3::types::PyDict::new(py);
592 for (k, v) in metadata {
593 metadata_py.set_item(k, v)?;
594 }
595
596 let py_list = data_class
597 .bind(py)
598 .call_method1("decode_record_batch_py", (metadata_py, py_batch))
599 .map_err(|e| anyhow::anyhow!("Failed to call decode_record_batch_py: {e}"))?;
600
601 let list = py_list
602 .cast::<pyo3::types::PyList>()
603 .map_err(|_| anyhow::anyhow!("Expected list from decode_record_batch_py"))?;
604
605 let mut result = Vec::new();
606 for item in list.iter() {
607 let wrapper = PythonCustomDataWrapper::new(py, &item)
608 .map_err(|e| anyhow::anyhow!("Failed to create wrapper: {e}"))?;
609 result.push(crate::data::Data::Custom(
610 crate::data::CustomData::from_arc(Arc::new(wrapper)),
611 ));
612 }
613 Ok(result)
614 })
615}
616
617#[cfg(feature = "python")]
656#[pyfunction]
657#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.model")]
658pub fn register_custom_data_class(data_class: &Bound<'_, PyAny>) -> PyResult<()> {
659 use std::sync::Arc;
660
661 use crate::data::registry;
662
663 let _py = data_class.py();
664
665 let type_name: String = if data_class.hasattr("type_name_static")? {
666 data_class.call_method0("type_name_static")?.extract()?
667 } else {
668 data_class.getattr("__name__")?.extract()?
669 };
670
671 if registry::conflicts_with_rust_class(&type_name, data_class) {
672 return Err(to_pyvalue_err(format!(
673 "Custom data type \"{type_name}\" is already registered for a different Rust class"
674 )));
675 }
676
677 #[cfg(feature = "arrow")]
678 if !data_class.hasattr("decode_record_batch_py")? {
679 return Err(to_pytype_err(
680 "Custom data class must have decode_record_batch_py(metadata, batch) class method",
681 ));
682 }
683
684 if !data_class.hasattr("from_json")? {
685 return Err(to_pytype_err(
686 "Custom data class must have from_json(data) class method (Rust macro provides it)",
687 ));
688 }
689
690 register_python_data_class(&type_name, data_class);
691
692 if let Some(extractor) = registry::get_rust_extractor(&type_name) {
693 let _ = registry::ensure_py_extractor_registered(&type_name, extractor);
694 }
695
696 let data_class_for_json = data_class.clone().unbind();
697
698 let json_deserializer = Box::new(
699 move |value: serde_json::Value| -> Result<Arc<dyn crate::data::CustomDataTrait>, anyhow::Error> {
700 pyo3::Python::attach(|py| {
701 py_json_deserialize_custom_data(&data_class_for_json.clone_ref(py), &value)
702 })
703 },
704 );
705
706 registry::ensure_json_deserializer_registered(&type_name, json_deserializer).map_err(|e| {
707 to_pyruntime_err(format!(
708 "Failed to register JSON deserializer for {type_name}: {e}"
709 ))
710 })?;
711
712 #[cfg(feature = "arrow")]
713 {
714 let data_class_for_decode = data_class.clone().unbind();
715 let pyarrow_schema = if let Ok(schema) = data_class.getattr("_schema") {
716 Some(schema)
717 } else if data_class.hasattr("arrow_schema_py")? {
718 Some(data_class.call_method0("arrow_schema_py")?)
719 } else {
720 None
721 }
722 .filter(|schema| schema.hasattr("_export_to_c").unwrap_or(false));
723 let schema = if let Some(py_schema) = pyarrow_schema {
724 Arc::new(pyarrow_schema_to_arrow_schema(&py_schema)?)
725 } else if let Some(schema) = registry::get_arrow_schema(&type_name) {
726 schema
727 } else {
728 Arc::new(arrow::datatypes::Schema::empty())
729 };
730 registry::validate_custom_arrow_schema(&type_name, &schema, false)
731 .map_err(|e| to_pyvalue_err(e.to_string()))?;
732
733 let encoder = Box::new(
734 move |items: &[Arc<dyn crate::data::CustomDataTrait>]| -> Result<
735 arrow::record_batch::RecordBatch,
736 anyhow::Error,
737 > { py_encode_custom_data_to_record_batch(items) },
738 );
739
740 let decoder = Box::new(
741 move |metadata: &std::collections::HashMap<String, String>,
742 batch: arrow::record_batch::RecordBatch|
743 -> Result<Vec<crate::data::Data>, anyhow::Error> {
744 pyo3::Python::attach(|py| {
745 py_decode_record_batch_to_custom_data(
746 &data_class_for_decode.clone_ref(py),
747 metadata,
748 batch,
749 )
750 })
751 },
752 );
753
754 registry::ensure_arrow_registered(&type_name, schema, encoder, decoder).map_err(|e| {
755 to_pyruntime_err(format!(
756 "Failed to register Arrow encoder/decoder for {type_name}: {e}"
757 ))
758 })?;
759 }
760
761 Ok(())
762}
763
764pub fn pyobjects_to_funding_rates(data: Vec<Bound<'_, PyAny>>) -> PyResult<Vec<FundingRateUpdate>> {
770 let funding_rates: Vec<FundingRateUpdate> = data
771 .into_iter()
772 .map(|obj| obj.extract::<FundingRateUpdate>().map_err(PyErr::from))
773 .collect::<PyResult<Vec<FundingRateUpdate>>>()?;
774
775 if !is_monotonically_increasing_by_init(&funding_rates) {
777 return Err(to_pyvalue_err(ERROR_MONOTONICITY));
778 }
779
780 Ok(funding_rates)
781}
782
783#[cfg(test)]
784mod tests {
785 use std::sync::Once;
786
787 use rstest::rstest;
788
789 use super::*;
790 use crate::data::{
791 OrderBookDeltas, OrderBookDepth,
792 stubs::{
793 quote_audusd, stub_bar, stub_delta, stub_deltas, stub_depth10, stub_trade_ethusdt_buy,
794 },
795 };
796
797 fn ensure_python_initialized() {
798 static INIT: Once = Once::new();
799 INIT.call_once(Python::initialize);
800 }
801
802 #[rstest]
803 fn data_to_pyobject_preserves_built_in_data() {
804 ensure_python_initialized();
805
806 let expected_delta = stub_delta();
807 let expected_deltas = stub_deltas();
808 let expected_depth = stub_depth10();
809 let expected_quote = quote_audusd();
810 let expected_trade = stub_trade_ethusdt_buy();
811 let expected_bar = stub_bar();
812
813 Python::attach(|py| {
814 let py_delta = data_to_pyobject(py, Data::BookDelta(expected_delta)).unwrap();
815 let py_deltas =
816 data_to_pyobject(py, Data::BookDeltas(Box::new(expected_deltas.clone()))).unwrap();
817 let py_depth =
818 data_to_pyobject(py, Data::BookDepth(Box::new(expected_depth.clone()))).unwrap();
819 let py_quote = data_to_pyobject(py, Data::Quote(expected_quote)).unwrap();
820 let py_trade = data_to_pyobject(py, Data::Trade(expected_trade)).unwrap();
821 let py_bar = data_to_pyobject(py, Data::Bar(expected_bar)).unwrap();
822
823 let actual_delta = *py_delta.bind(py).cast::<OrderBookDelta>().unwrap().borrow();
824 let actual_deltas = py_deltas
825 .bind(py)
826 .cast::<OrderBookDeltas>()
827 .unwrap()
828 .borrow()
829 .clone();
830 let actual_depth = py_depth
831 .bind(py)
832 .cast::<OrderBookDepth>()
833 .unwrap()
834 .borrow()
835 .clone();
836 let actual_quote = *py_quote.bind(py).cast::<QuoteTick>().unwrap().borrow();
837 let actual_trade = *py_trade.bind(py).cast::<TradeTick>().unwrap().borrow();
838 let actual_bar = *py_bar.bind(py).cast::<Bar>().unwrap().borrow();
839
840 assert_eq!(actual_delta, expected_delta);
841 assert_eq!(actual_deltas.instrument_id, expected_deltas.instrument_id);
842 assert_eq!(actual_deltas.deltas, expected_deltas.deltas);
843 assert_eq!(actual_deltas.flags, expected_deltas.flags);
844 assert_eq!(actual_deltas.sequence, expected_deltas.sequence);
845 assert_eq!(actual_deltas.ts_event, expected_deltas.ts_event);
846 assert_eq!(actual_deltas.ts_init, expected_deltas.ts_init);
847 assert_eq!(actual_depth, expected_depth);
848 assert_eq!(actual_quote, expected_quote);
849 assert_eq!(actual_trade, expected_trade);
850 assert_eq!(actual_bar, expected_bar);
851 });
852 }
853
854 #[cfg(feature = "defi")]
855 #[rstest]
856 fn data_to_pyobject_rejects_defi_variant() {
857 use nautilus_core::UnixNanos;
858 use ustr::Ustr;
859
860 use crate::defi::{Blockchain, data::Block};
861
862 ensure_python_initialized();
863
864 let block = Block::new(
865 "0x1234".to_string(),
866 "0xabcd".to_string(),
867 42,
868 Ustr::from("0x0000000000000000000000000000000000000000"),
869 100_000,
870 50_000,
871 UnixNanos::from(1_700_000_000u64),
872 Some(Blockchain::Ethereum),
873 );
874
875 Python::attach(|py| {
876 let e = data_to_pyobject(
877 py,
878 Data::Defi(Box::new(crate::defi::data::DefiData::Block(block))),
879 )
880 .unwrap_err();
881
882 assert_eq!(e.to_string(), "TypeError: Unsupported DeFi data variant");
883 });
884 }
885}