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