1use std::sync::Arc;
22
23use nautilus_core::UnixNanos;
24use nautilus_model::{
25 custom_data,
26 data::{HasTsInit, custom::CustomDataTrait},
27 enums::OrderSide,
28 identifiers::InstrumentId,
29 types::{Price, Quantity},
30};
31use nautilus_serialization::arrow_custom_data;
32use rust_decimal::Decimal;
33use serde::{Deserialize, Serialize};
34
35#[arrow_custom_data(pyo3, stub_module = "nautilus_trader.adapters.binance")]
37#[custom_data(pyo3, stub_module = "nautilus_trader.adapters.binance")]
38pub struct BinanceFuturesOpenInterest {
39 pub instrument_id: InstrumentId,
41 pub open_interest: Decimal,
43 pub ts_event: UnixNanos,
45 pub ts_init: UnixNanos,
47}
48
49#[cfg_attr(
51 feature = "python",
52 pyo3::pyclass(module = "nautilus_trader.adapters.binance", from_py_object)
53)]
54#[cfg_attr(
55 feature = "python",
56 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.binance")
57)]
58#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
59pub struct BinanceFuturesOpenInterestHistPoint {
60 pub sum_open_interest: Decimal,
62 pub sum_open_interest_value: Decimal,
64 pub ts_event: UnixNanos,
66}
67
68impl BinanceFuturesOpenInterestHistPoint {
69 #[must_use]
71 pub fn new(
72 sum_open_interest: Decimal,
73 sum_open_interest_value: Decimal,
74 ts_event: UnixNanos,
75 ) -> Self {
76 Self {
77 sum_open_interest,
78 sum_open_interest_value,
79 ts_event,
80 }
81 }
82}
83
84#[cfg_attr(
90 feature = "python",
91 pyo3::pyclass(module = "nautilus_trader.adapters.binance", from_py_object)
92)]
93#[cfg_attr(
94 feature = "python",
95 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.binance")
96)]
97#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
98pub struct BinanceFuturesOpenInterestHist {
99 pub instrument_id: InstrumentId,
101 pub period: String,
103 pub points: Vec<BinanceFuturesOpenInterestHistPoint>,
105 pub ts_event: UnixNanos,
107 pub ts_init: UnixNanos,
109}
110
111impl BinanceFuturesOpenInterestHist {
112 #[must_use]
114 pub fn new(
115 instrument_id: InstrumentId,
116 period: String,
117 points: Vec<BinanceFuturesOpenInterestHistPoint>,
118 ts_event: UnixNanos,
119 ts_init: UnixNanos,
120 ) -> Self {
121 Self {
122 instrument_id,
123 period,
124 points,
125 ts_event,
126 ts_init,
127 }
128 }
129}
130
131impl HasTsInit for BinanceFuturesOpenInterestHist {
132 fn ts_init(&self) -> UnixNanos {
133 self.ts_init
134 }
135}
136
137impl CustomDataTrait for BinanceFuturesOpenInterestHist {
138 fn type_name(&self) -> &'static str {
139 "BinanceFuturesOpenInterestHist"
140 }
141
142 fn as_any(&self) -> &dyn std::any::Any {
143 self
144 }
145
146 fn ts_event(&self) -> UnixNanos {
147 self.ts_event
148 }
149
150 fn to_json(&self) -> anyhow::Result<String> {
151 Ok(serde_json::to_string(self)?)
152 }
153
154 fn clone_arc(&self) -> Arc<dyn CustomDataTrait> {
155 Arc::new(self.clone())
156 }
157
158 fn eq_arc(&self, other: &dyn CustomDataTrait) -> bool {
159 if let Some(o) = other.as_any().downcast_ref::<Self>() {
160 self == o
161 } else {
162 false
163 }
164 }
165
166 #[cfg(feature = "python")]
167 fn to_pyobject(&self, py: pyo3::Python<'_>) -> pyo3::PyResult<pyo3::Py<pyo3::PyAny>> {
168 nautilus_model::data::custom::clone_pyclass_to_pyobject(self, py)
169 }
170
171 fn type_name_static() -> &'static str {
172 "BinanceFuturesOpenInterestHist"
173 }
174
175 fn from_json(value: serde_json::Value) -> anyhow::Result<Arc<dyn CustomDataTrait>> {
176 let json_str = serde_json::to_string(&value)?;
177 let parsed: Self = serde_json::from_str(&json_str)?;
178 Ok(Arc::new(parsed))
179 }
180}
181
182#[arrow_custom_data(pyo3, stub_module = "nautilus_trader.adapters.binance")]
184#[custom_data(pyo3, stub_module = "nautilus_trader.adapters.binance")]
185pub struct BinanceFuturesLiquidation {
186 pub instrument_id: InstrumentId,
188 #[custom_data_field(native_enum)]
190 pub side: OrderSide,
191 pub price: Price,
193 pub average_price: Price,
195 pub last_filled_qty: Quantity,
197 pub accumulated_qty: Quantity,
199 pub ts_event: UnixNanos,
201 pub ts_init: UnixNanos,
203}
204
205#[cfg_attr(
207 feature = "python",
208 pyo3::pyclass(module = "nautilus_trader.adapters.binance", from_py_object)
209)]
210#[cfg_attr(
211 feature = "python",
212 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.binance")
213)]
214#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
215pub struct BinanceSpotTicker {
216 pub instrument_id: InstrumentId,
218 pub price_change: Decimal,
220 pub price_change_percent: Decimal,
222 pub weighted_avg_price: Decimal,
224 pub prev_close_price: Decimal,
226 pub last_price: Decimal,
228 pub last_qty: Decimal,
230 pub bid_price: Decimal,
232 pub bid_qty: Decimal,
234 pub ask_price: Decimal,
236 pub ask_qty: Decimal,
238 pub open_price: Decimal,
240 pub high_price: Decimal,
242 pub low_price: Decimal,
244 pub volume: Decimal,
246 pub quote_volume: Decimal,
248 pub open_time: UnixNanos,
250 pub close_time: UnixNanos,
252 pub first_trade_id: i64,
254 pub last_trade_id: i64,
256 pub num_trades: i64,
258 pub ts_event: UnixNanos,
260 pub ts_init: UnixNanos,
262}
263
264impl HasTsInit for BinanceSpotTicker {
265 fn ts_init(&self) -> UnixNanos {
266 self.ts_init
267 }
268}
269
270impl CustomDataTrait for BinanceSpotTicker {
271 fn type_name(&self) -> &'static str {
272 "BinanceSpotTicker"
273 }
274
275 fn as_any(&self) -> &dyn std::any::Any {
276 self
277 }
278
279 fn ts_event(&self) -> UnixNanos {
280 self.ts_event
281 }
282
283 fn to_json(&self) -> anyhow::Result<String> {
284 Ok(serde_json::to_string(self)?)
285 }
286
287 fn clone_arc(&self) -> Arc<dyn CustomDataTrait> {
288 Arc::new(self.clone())
289 }
290
291 fn eq_arc(&self, other: &dyn CustomDataTrait) -> bool {
292 other.as_any().downcast_ref::<Self>() == Some(self)
293 }
294
295 #[cfg(feature = "python")]
296 fn to_pyobject(&self, py: pyo3::Python<'_>) -> pyo3::PyResult<pyo3::Py<pyo3::PyAny>> {
297 nautilus_model::data::custom::clone_pyclass_to_pyobject(self, py)
298 }
299
300 fn type_name_static() -> &'static str {
301 "BinanceSpotTicker"
302 }
303
304 fn from_json(value: serde_json::Value) -> anyhow::Result<Arc<dyn CustomDataTrait>> {
305 let json_str = serde_json::to_string(&value)?;
306 Ok(Arc::new(serde_json::from_str::<Self>(&json_str)?))
307 }
308}
309
310#[cfg_attr(
312 feature = "python",
313 pyo3::pyclass(module = "nautilus_trader.adapters.binance", from_py_object)
314)]
315#[cfg_attr(
316 feature = "python",
317 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.binance")
318)]
319#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
320pub struct BinanceFuturesMarkPriceUpdate {
321 pub instrument_id: InstrumentId,
323 pub mark_price: Price,
325 pub index_price: Price,
327 pub estimated_settle_price: Price,
329 pub funding_rate: Decimal,
331 pub next_funding_time: Option<UnixNanos>,
333 pub ts_event: UnixNanos,
335 pub ts_init: UnixNanos,
337}
338
339impl HasTsInit for BinanceFuturesMarkPriceUpdate {
340 fn ts_init(&self) -> UnixNanos {
341 self.ts_init
342 }
343}
344
345impl CustomDataTrait for BinanceFuturesMarkPriceUpdate {
346 fn type_name(&self) -> &'static str {
347 "BinanceFuturesMarkPriceUpdate"
348 }
349
350 fn as_any(&self) -> &dyn std::any::Any {
351 self
352 }
353
354 fn ts_event(&self) -> UnixNanos {
355 self.ts_event
356 }
357
358 fn to_json(&self) -> anyhow::Result<String> {
359 Ok(serde_json::to_string(self)?)
360 }
361
362 fn clone_arc(&self) -> Arc<dyn CustomDataTrait> {
363 Arc::new(self.clone())
364 }
365
366 fn eq_arc(&self, other: &dyn CustomDataTrait) -> bool {
367 other.as_any().downcast_ref::<Self>() == Some(self)
368 }
369
370 #[cfg(feature = "python")]
371 fn to_pyobject(&self, py: pyo3::Python<'_>) -> pyo3::PyResult<pyo3::Py<pyo3::PyAny>> {
372 nautilus_model::data::custom::clone_pyclass_to_pyobject(self, py)
373 }
374
375 fn type_name_static() -> &'static str {
376 "BinanceFuturesMarkPriceUpdate"
377 }
378
379 fn from_json(value: serde_json::Value) -> anyhow::Result<Arc<dyn CustomDataTrait>> {
380 let json_str = serde_json::to_string(&value)?;
381 Ok(Arc::new(serde_json::from_str::<Self>(&json_str)?))
382 }
383}
384
385#[arrow_custom_data(pyo3, stub_module = "nautilus_trader.adapters.binance")]
387#[custom_data(pyo3, stub_module = "nautilus_trader.adapters.binance")]
388pub struct BinanceFuturesTicker {
389 pub instrument_id: InstrumentId,
391 pub price_change: Decimal,
393 pub price_change_percent: Decimal,
395 pub weighted_avg_price: Decimal,
397 pub last_price: Decimal,
399 pub last_qty: Decimal,
401 pub open_price: Decimal,
403 pub high_price: Decimal,
405 pub low_price: Decimal,
407 pub volume: Decimal,
409 pub quote_volume: Decimal,
411 pub open_time: UnixNanos,
413 pub close_time: UnixNanos,
415 pub first_trade_id: i64,
417 pub last_trade_id: i64,
419 pub num_trades: i64,
421 pub ts_event: UnixNanos,
423 pub ts_init: UnixNanos,
425}
426
427pub fn register_binance_custom_data() {
431 nautilus_serialization::ensure_custom_data_registered::<BinanceFuturesOpenInterest>();
432 nautilus_serialization::ensure_custom_data_registered::<BinanceFuturesLiquidation>();
433 nautilus_serialization::ensure_custom_data_registered::<BinanceFuturesTicker>();
434 let _ = nautilus_model::data::ensure_custom_data_json_registered::<
435 BinanceFuturesOpenInterestHist,
436 >();
437 let _ = nautilus_model::data::ensure_custom_data_json_registered::<BinanceSpotTicker>();
438 let _ =
439 nautilus_model::data::ensure_custom_data_json_registered::<BinanceFuturesMarkPriceUpdate>();
440}
441
442#[cfg(test)]
443mod tests {
444 use std::{str::FromStr, sync::Arc};
445
446 #[cfg(feature = "python")]
447 use nautilus_core::Params;
448 use nautilus_model::data::Data;
449 #[cfg(feature = "python")]
450 use nautilus_model::data::{CustomData, DataType};
451 use nautilus_serialization::arrow::{
452 ArrowSchemaProvider, DecodeDataFromRecordBatch, EncodeToRecordBatch,
453 };
454 #[cfg(feature = "python")]
455 use pyo3::{prelude::*, types::PyList};
456 use rstest::rstest;
457 use rust_decimal::Decimal;
458
459 use super::*;
460
461 #[rstest]
462 fn test_register_binance_custom_data_is_idempotent() {
463 register_binance_custom_data();
464 register_binance_custom_data();
465 }
466
467 #[rstest]
468 fn test_binance_futures_open_interest_arrow_round_trip() {
469 let original = BinanceFuturesOpenInterest::new(
470 InstrumentId::from("BTCUSDT-PERP.BINANCE"),
471 Decimal::from_str("123456.789012345678").unwrap(),
472 UnixNanos::from(1),
473 UnixNanos::from(2),
474 );
475 let metadata = EncodeToRecordBatch::metadata(&original);
476 let batch =
477 BinanceFuturesOpenInterest::encode_batch(&metadata, std::slice::from_ref(&original))
478 .unwrap();
479 let decoded = BinanceFuturesOpenInterest::decode_data_batch(&metadata, batch).unwrap();
480
481 assert_eq!(decoded.len(), 1);
482
483 match &decoded[0] {
484 Data::Custom(custom) => {
485 let round_trip = custom
486 .data
487 .as_any()
488 .downcast_ref::<BinanceFuturesOpenInterest>()
489 .expect("expected BinanceFuturesOpenInterest");
490 assert_eq!(round_trip, &original);
491 }
492 other => panic!("Expected Data::Custom, was {other:?}"),
493 }
494 }
495
496 #[rstest]
497 fn test_binance_futures_liquidation_arrow_schema_uses_native_types() {
498 use arrow::datatypes::DataType;
499
500 let schema = BinanceFuturesLiquidation::get_schema(None);
501
502 assert_eq!(schema.fields().len(), 8);
503 assert_eq!(schema.field(0).name(), "instrument_id");
504 assert!(matches!(
505 schema.field(0).data_type(),
506 DataType::Utf8 | DataType::Utf8View
507 ));
508 assert_eq!(
509 schema.field_with_name("side").unwrap().data_type(),
510 &nautilus_serialization::arrow::enum_dictionary_data_type(),
511 );
512
513 for field_name in [
514 "price",
515 "average_price",
516 "last_filled_qty",
517 "accumulated_qty",
518 ] {
519 assert_eq!(
520 schema.field_with_name(field_name).unwrap().data_type(),
521 &DataType::Decimal128(38, 16),
522 );
523 }
524 }
525
526 #[rstest]
527 fn test_binance_futures_liquidation_arrow_round_trip() {
528 let original = BinanceFuturesLiquidation::new(
529 InstrumentId::from("BTCUSDT-PERP.BINANCE"),
530 OrderSide::Sell,
531 Price::from("65432.10"),
532 Price::from("65431.50"),
533 Quantity::from("0.250"),
534 Quantity::from("1.500"),
535 UnixNanos::from(3),
536 UnixNanos::from(4),
537 );
538 let metadata = EncodeToRecordBatch::metadata(&original);
539 let batch =
540 BinanceFuturesLiquidation::encode_batch(&metadata, std::slice::from_ref(&original))
541 .unwrap();
542 let decoded = BinanceFuturesLiquidation::decode_data_batch(&metadata, batch).unwrap();
543
544 assert_eq!(decoded.len(), 1);
545
546 match &decoded[0] {
547 Data::Custom(custom) => {
548 let round_trip = custom
549 .data
550 .as_any()
551 .downcast_ref::<BinanceFuturesLiquidation>()
552 .expect("expected BinanceFuturesLiquidation");
553 assert_eq!(round_trip, &original);
554 }
555 other => panic!("Expected Data::Custom, was {other:?}"),
556 }
557 }
558
559 #[rstest]
560 fn test_binance_futures_ticker_arrow_round_trip() {
561 let original = BinanceFuturesTicker::new(
562 InstrumentId::from("BTCUSDT-PERP.BINANCE"),
563 Decimal::from_str("12.34").unwrap(),
564 Decimal::from_str("5.67").unwrap(),
565 Decimal::from_str("62345.123456").unwrap(),
566 Decimal::from_str("62350.000001").unwrap(),
567 Decimal::from_str("0.010000").unwrap(),
568 Decimal::from_str("62000.000000").unwrap(),
569 Decimal::from_str("63000.000000").unwrap(),
570 Decimal::from_str("61000.000000").unwrap(),
571 Decimal::from_str("1234.567890").unwrap(),
572 Decimal::from_str("76543210.123456").unwrap(),
573 UnixNanos::from(10),
574 UnixNanos::from(11),
575 100,
576 200,
577 300,
578 UnixNanos::from(12),
579 UnixNanos::from(13),
580 );
581 let metadata = EncodeToRecordBatch::metadata(&original);
582 let batch =
583 BinanceFuturesTicker::encode_batch(&metadata, std::slice::from_ref(&original)).unwrap();
584 let decoded = BinanceFuturesTicker::decode_data_batch(&metadata, batch).unwrap();
585
586 assert_eq!(decoded.len(), 1);
587
588 match &decoded[0] {
589 Data::Custom(custom) => {
590 let round_trip = custom
591 .data
592 .as_any()
593 .downcast_ref::<BinanceFuturesTicker>()
594 .expect("expected BinanceFuturesTicker");
595 assert_eq!(round_trip, &original);
596 }
597 other => panic!("Expected Data::Custom, was {other:?}"),
598 }
599 }
600
601 #[rstest]
602 fn test_binance_futures_custom_data_json_round_trip() {
603 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
604
605 let open_interest = BinanceFuturesOpenInterest::new(
606 instrument_id,
607 Decimal::from_str("123456.789012345678").unwrap(),
608 UnixNanos::from(1),
609 UnixNanos::from(2),
610 );
611 let value: serde_json::Value =
612 serde_json::from_str(&open_interest.to_json().unwrap()).unwrap();
613 let restored = <BinanceFuturesOpenInterest as CustomDataTrait>::from_json(value).unwrap();
614 assert_eq!(
615 restored
616 .as_any()
617 .downcast_ref::<BinanceFuturesOpenInterest>()
618 .unwrap(),
619 &open_interest,
620 );
621
622 let liquidation = BinanceFuturesLiquidation::new(
623 instrument_id,
624 OrderSide::Sell,
625 Price::from("65432.10"),
626 Price::from("65431.50"),
627 Quantity::from("0.250"),
628 Quantity::from("1.500"),
629 UnixNanos::from(3),
630 UnixNanos::from(4),
631 );
632 let value: serde_json::Value =
633 serde_json::from_str(&liquidation.to_json().unwrap()).unwrap();
634 let restored = <BinanceFuturesLiquidation as CustomDataTrait>::from_json(value).unwrap();
635 assert_eq!(
636 restored
637 .as_any()
638 .downcast_ref::<BinanceFuturesLiquidation>()
639 .unwrap(),
640 &liquidation,
641 );
642
643 let ticker = BinanceFuturesTicker::new(
644 instrument_id,
645 Decimal::from_str("12.34").unwrap(),
646 Decimal::from_str("5.67").unwrap(),
647 Decimal::from_str("62345.123456").unwrap(),
648 Decimal::from_str("62350.000001").unwrap(),
649 Decimal::from_str("0.010000").unwrap(),
650 Decimal::from_str("62000.000000").unwrap(),
651 Decimal::from_str("63000.000000").unwrap(),
652 Decimal::from_str("61000.000000").unwrap(),
653 Decimal::from_str("1234.567890").unwrap(),
654 Decimal::from_str("76543210.123456").unwrap(),
655 UnixNanos::from(10),
656 UnixNanos::from(11),
657 100,
658 200,
659 300,
660 UnixNanos::from(12),
661 UnixNanos::from(13),
662 );
663 let value: serde_json::Value = serde_json::from_str(&ticker.to_json().unwrap()).unwrap();
664 let restored = <BinanceFuturesTicker as CustomDataTrait>::from_json(value).unwrap();
665 assert_eq!(
666 restored
667 .as_any()
668 .downcast_ref::<BinanceFuturesTicker>()
669 .unwrap(),
670 &ticker,
671 );
672 }
673
674 #[rstest]
675 fn test_binance_futures_custom_data_catalog_round_trip() {
676 use nautilus_model::data::{CustomData as CatalogCustomData, DataType as CatalogDataType};
677 use nautilus_persistence::backend::parquet::catalog::ParquetDataCatalog;
678 use tempfile::TempDir;
679
680 register_binance_custom_data();
681 let temp_dir = TempDir::new().unwrap();
682 let catalog = ParquetDataCatalog::new(temp_dir.path(), None, None, None, None);
683 let mut catalog = catalog;
684 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
685 let ids = vec![instrument_id.to_string()];
686
687 let liquidation_type = CatalogDataType::new(
688 "BinanceFuturesLiquidation",
689 None,
690 Some(instrument_id.to_string()),
691 );
692
693 let liquidation = BinanceFuturesLiquidation::new(
694 instrument_id,
695 OrderSide::Sell,
696 Price::from("65432.10"),
697 Price::from("65431.50"),
698 Quantity::from("0.250"),
699 Quantity::from("1.500"),
700 UnixNanos::from(100),
701 UnixNanos::from(101),
702 );
703 let liquidation_path = catalog
704 .write_custom_data_batch(
705 vec![CatalogCustomData::new(
706 Arc::new(liquidation.clone()),
707 liquidation_type,
708 )],
709 None,
710 None,
711 Some(false),
712 )
713 .unwrap();
714 assert!(
715 liquidation_path
716 .to_string_lossy()
717 .contains("data/custom/BinanceFuturesLiquidation/BTCUSDT-PERP.BINANCE")
718 );
719
720 let liquidation_rows = catalog
721 .query_custom_data_dynamic(
722 "BinanceFuturesLiquidation",
723 Some(&ids),
724 None,
725 None,
726 None,
727 None,
728 true,
729 )
730 .unwrap();
731 assert_eq!(liquidation_rows.len(), 1);
732
733 match &liquidation_rows[0] {
734 Data::Custom(custom) => {
735 let row = custom
736 .data
737 .as_any()
738 .downcast_ref::<BinanceFuturesLiquidation>()
739 .expect("expected BinanceFuturesLiquidation");
740 assert_eq!(row, &liquidation);
741 }
742 other => panic!("Expected Data::Custom, was {other:?}"),
743 }
744
745 let ticker_type = CatalogDataType::new(
746 "BinanceFuturesTicker",
747 None,
748 Some(instrument_id.to_string()),
749 );
750
751 let ticker = BinanceFuturesTicker::new(
752 instrument_id,
753 Decimal::from_str("12.34").unwrap(),
754 Decimal::from_str("5.67").unwrap(),
755 Decimal::from_str("62345.123456").unwrap(),
756 Decimal::from_str("62350.000001").unwrap(),
757 Decimal::from_str("0.010000").unwrap(),
758 Decimal::from_str("62000.000000").unwrap(),
759 Decimal::from_str("63000.000000").unwrap(),
760 Decimal::from_str("61000.000000").unwrap(),
761 Decimal::from_str("1234.567890").unwrap(),
762 Decimal::from_str("76543210.123456").unwrap(),
763 UnixNanos::from(110),
764 UnixNanos::from(111),
765 100,
766 200,
767 300,
768 UnixNanos::from(112),
769 UnixNanos::from(113),
770 );
771 let ticker_path = catalog
772 .write_custom_data_batch(
773 vec![CatalogCustomData::new(
774 Arc::new(ticker.clone()),
775 ticker_type,
776 )],
777 None,
778 None,
779 Some(false),
780 )
781 .unwrap();
782 assert!(
783 ticker_path
784 .to_string_lossy()
785 .contains("data/custom/BinanceFuturesTicker/BTCUSDT-PERP.BINANCE")
786 );
787
788 let ticker_rows = catalog
789 .query_custom_data_dynamic(
790 "BinanceFuturesTicker",
791 Some(&ids),
792 None,
793 None,
794 None,
795 None,
796 true,
797 )
798 .unwrap();
799 assert_eq!(ticker_rows.len(), 1);
800
801 match &ticker_rows[0] {
802 Data::Custom(custom) => {
803 let row = custom
804 .data
805 .as_any()
806 .downcast_ref::<BinanceFuturesTicker>()
807 .expect("expected BinanceFuturesTicker");
808 assert_eq!(row, &ticker);
809 }
810 other => panic!("Expected Data::Custom, was {other:?}"),
811 }
812
813 let open_interest_type = CatalogDataType::new(
814 "BinanceFuturesOpenInterest",
815 None,
816 Some(instrument_id.to_string()),
817 );
818
819 let open_interest = BinanceFuturesOpenInterest::new(
820 instrument_id,
821 Decimal::from_str("123456.789012345678").unwrap(),
822 UnixNanos::from(120),
823 UnixNanos::from(121),
824 );
825 let open_interest_path = catalog
826 .write_custom_data_batch(
827 vec![CatalogCustomData::new(
828 Arc::new(open_interest.clone()),
829 open_interest_type,
830 )],
831 None,
832 None,
833 Some(false),
834 )
835 .unwrap();
836 assert!(
837 open_interest_path
838 .to_string_lossy()
839 .contains("data/custom/BinanceFuturesOpenInterest/BTCUSDT-PERP.BINANCE")
840 );
841
842 let open_interest_rows = catalog
843 .query_custom_data_dynamic(
844 "BinanceFuturesOpenInterest",
845 Some(&ids),
846 None,
847 None,
848 None,
849 None,
850 true,
851 )
852 .unwrap();
853 assert_eq!(open_interest_rows.len(), 1);
854
855 match &open_interest_rows[0] {
856 Data::Custom(custom) => {
857 let row = custom
858 .data
859 .as_any()
860 .downcast_ref::<BinanceFuturesOpenInterest>()
861 .expect("expected BinanceFuturesOpenInterest");
862 assert_eq!(row, &open_interest);
863 }
864 other => panic!("Expected Data::Custom, was {other:?}"),
865 }
866 }
867
868 #[cfg(feature = "python")]
869 #[rstest]
870 fn test_open_interest_hist_points_roundtrip_as_typed_python_list() {
871 pyo3::Python::initialize();
872 register_binance_custom_data();
873
874 Python::attach(|py| {
875 let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
876 let points = vec![
877 BinanceFuturesOpenInterestHistPoint::new(
878 Decimal::from_str_exact("100.0").unwrap(),
879 Decimal::from_str_exact("1000.0").unwrap(),
880 UnixNanos::from_millis(1_700_000_000_000),
881 ),
882 BinanceFuturesOpenInterestHistPoint::new(
883 Decimal::from_str_exact("101.0").unwrap(),
884 Decimal::from_str_exact("1005.0").unwrap(),
885 UnixNanos::from_millis(1_700_000_300_000),
886 ),
887 ];
888 let payload = BinanceFuturesOpenInterestHist::new(
889 instrument_id,
890 "5m".to_string(),
891 points,
892 UnixNanos::from_millis(1_700_000_300_000),
893 UnixNanos::from(42_u64),
894 );
895
896 let mut metadata = Params::new();
897 metadata.insert(
898 "instrument_id".to_string(),
899 serde_json::Value::String("BTCUSDT-PERP.BINANCE".to_string()),
900 );
901 metadata.insert(
902 "period".to_string(),
903 serde_json::Value::String("5m".to_string()),
904 );
905
906 let custom = CustomData::new(
907 Arc::new(payload),
908 DataType::new(
909 "BinanceFuturesOpenInterestHist",
910 Some(metadata),
911 Some("BTCUSDT-PERP.BINANCE".to_string()),
912 ),
913 );
914
915 let py_custom = Py::new(py, custom).unwrap();
916 let py_payload = py_custom.bind(py).getattr("data").unwrap();
917 let py_points = py_payload
918 .getattr("points")
919 .unwrap()
920 .cast_into::<PyList>()
921 .unwrap();
922
923 assert_eq!(py_points.len(), 2);
924 assert!(
925 py_points
926 .get_item(0)
927 .unwrap()
928 .is_instance_of::<BinanceFuturesOpenInterestHistPoint>()
929 );
930
931 let point0 = py_points
932 .get_item(0)
933 .unwrap()
934 .extract::<BinanceFuturesOpenInterestHistPoint>()
935 .unwrap();
936 let point1 = py_points
937 .get_item(1)
938 .unwrap()
939 .extract::<BinanceFuturesOpenInterestHistPoint>()
940 .unwrap();
941
942 assert_eq!(
943 point0.sum_open_interest,
944 Decimal::from_str_exact("100.0").unwrap()
945 );
946 assert_eq!(
947 point1.sum_open_interest_value,
948 Decimal::from_str_exact("1005.0").unwrap()
949 );
950 });
951 }
952}