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