Skip to main content

nautilus_binance/
data_types.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Binance-specific custom data types.
17//!
18//! These types carry Binance domain data through the Nautilus data engine as
19//! [`CustomData`](nautilus_model::data::CustomData).
20
21use std::sync::Arc;
22
23use nautilus_core::UnixNanos;
24use nautilus_model::{
25    data::{HasTsInit, custom::CustomDataTrait},
26    enums::OrderSide,
27    identifiers::InstrumentId,
28    types::{Price, Quantity},
29};
30use rust_decimal::Decimal;
31use serde::{Deserialize, Serialize};
32
33/// Binance Futures current open interest snapshot.
34#[cfg_attr(
35    feature = "python",
36    pyo3::pyclass(module = "nautilus_trader.adapters.binance", from_py_object)
37)]
38#[cfg_attr(
39    feature = "python",
40    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.binance")
41)]
42#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
43pub struct BinanceFuturesOpenInterest {
44    /// The instrument for this snapshot.
45    pub instrument_id: InstrumentId,
46    /// The total open interest value.
47    pub open_interest: Decimal,
48    /// UNIX timestamp (nanoseconds) when the snapshot event occurred.
49    pub ts_event: UnixNanos,
50    /// UNIX timestamp (nanoseconds) when the instance was initialized.
51    pub ts_init: UnixNanos,
52}
53
54impl BinanceFuturesOpenInterest {
55    /// Creates a new [`BinanceFuturesOpenInterest`] instance.
56    #[must_use]
57    pub fn new(
58        instrument_id: InstrumentId,
59        open_interest: Decimal,
60        ts_event: UnixNanos,
61        ts_init: UnixNanos,
62    ) -> Self {
63        Self {
64            instrument_id,
65            open_interest,
66            ts_event,
67            ts_init,
68        }
69    }
70}
71
72impl HasTsInit for BinanceFuturesOpenInterest {
73    fn ts_init(&self) -> UnixNanos {
74        self.ts_init
75    }
76}
77
78impl CustomDataTrait for BinanceFuturesOpenInterest {
79    fn type_name(&self) -> &'static str {
80        "BinanceFuturesOpenInterest"
81    }
82
83    fn as_any(&self) -> &dyn std::any::Any {
84        self
85    }
86
87    fn ts_event(&self) -> UnixNanos {
88        self.ts_event
89    }
90
91    fn to_json(&self) -> anyhow::Result<String> {
92        Ok(serde_json::to_string(self)?)
93    }
94
95    fn clone_arc(&self) -> Arc<dyn CustomDataTrait> {
96        Arc::new(self.clone())
97    }
98
99    fn eq_arc(&self, other: &dyn CustomDataTrait) -> bool {
100        if let Some(o) = other.as_any().downcast_ref::<Self>() {
101            self == o
102        } else {
103            false
104        }
105    }
106
107    #[cfg(feature = "python")]
108    fn to_pyobject(&self, py: pyo3::Python<'_>) -> pyo3::PyResult<pyo3::Py<pyo3::PyAny>> {
109        nautilus_model::data::custom::clone_pyclass_to_pyobject(self, py)
110    }
111
112    fn type_name_static() -> &'static str {
113        "BinanceFuturesOpenInterest"
114    }
115
116    fn from_json(value: serde_json::Value) -> anyhow::Result<Arc<dyn CustomDataTrait>> {
117        let json_str = serde_json::to_string(&value)?;
118        let parsed: Self = serde_json::from_str(&json_str)?;
119        Ok(Arc::new(parsed))
120    }
121}
122
123/// Binance Futures historical open interest point.
124#[cfg_attr(
125    feature = "python",
126    pyo3::pyclass(module = "nautilus_trader.adapters.binance", from_py_object)
127)]
128#[cfg_attr(
129    feature = "python",
130    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.binance")
131)]
132#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
133pub struct BinanceFuturesOpenInterestHistPoint {
134    /// The total open interest value.
135    pub sum_open_interest: Decimal,
136    /// The total open interest notional value.
137    pub sum_open_interest_value: Decimal,
138    /// UNIX timestamp (nanoseconds) for the bucket represented by this point.
139    pub ts_event: UnixNanos,
140}
141
142impl BinanceFuturesOpenInterestHistPoint {
143    /// Creates a new [`BinanceFuturesOpenInterestHistPoint`] instance.
144    #[must_use]
145    pub fn new(
146        sum_open_interest: Decimal,
147        sum_open_interest_value: Decimal,
148        ts_event: UnixNanos,
149    ) -> Self {
150        Self {
151            sum_open_interest,
152            sum_open_interest_value,
153            ts_event,
154        }
155    }
156}
157
158/// Binance Futures historical open interest batch.
159///
160/// COIN-M requests are keyed by pair and contract type rather than by symbol.
161/// Perpetuals derive both from the `_PERP` symbol suffix, while delivery
162/// contracts resolve them from the cached instrument definition.
163#[cfg_attr(
164    feature = "python",
165    pyo3::pyclass(module = "nautilus_trader.adapters.binance", from_py_object)
166)]
167#[cfg_attr(
168    feature = "python",
169    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.binance")
170)]
171#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
172pub struct BinanceFuturesOpenInterestHist {
173    /// The instrument for this batch.
174    pub instrument_id: InstrumentId,
175    /// The Binance period string used for the request (e.g. "5m").
176    pub period: String,
177    /// Ordered open interest history points returned by Binance.
178    pub points: Vec<BinanceFuturesOpenInterestHistPoint>,
179    /// UNIX timestamp (nanoseconds) for the batch, represented by the final point.
180    pub ts_event: UnixNanos,
181    /// UNIX timestamp (nanoseconds) when the instance was initialized.
182    pub ts_init: UnixNanos,
183}
184
185impl BinanceFuturesOpenInterestHist {
186    /// Creates a new [`BinanceFuturesOpenInterestHist`] instance.
187    #[must_use]
188    pub fn new(
189        instrument_id: InstrumentId,
190        period: String,
191        points: Vec<BinanceFuturesOpenInterestHistPoint>,
192        ts_event: UnixNanos,
193        ts_init: UnixNanos,
194    ) -> Self {
195        Self {
196            instrument_id,
197            period,
198            points,
199            ts_event,
200            ts_init,
201        }
202    }
203}
204
205impl HasTsInit for BinanceFuturesOpenInterestHist {
206    fn ts_init(&self) -> UnixNanos {
207        self.ts_init
208    }
209}
210
211impl CustomDataTrait for BinanceFuturesOpenInterestHist {
212    fn type_name(&self) -> &'static str {
213        "BinanceFuturesOpenInterestHist"
214    }
215
216    fn as_any(&self) -> &dyn std::any::Any {
217        self
218    }
219
220    fn ts_event(&self) -> UnixNanos {
221        self.ts_event
222    }
223
224    fn to_json(&self) -> anyhow::Result<String> {
225        Ok(serde_json::to_string(self)?)
226    }
227
228    fn clone_arc(&self) -> Arc<dyn CustomDataTrait> {
229        Arc::new(self.clone())
230    }
231
232    fn eq_arc(&self, other: &dyn CustomDataTrait) -> bool {
233        if let Some(o) = other.as_any().downcast_ref::<Self>() {
234            self == o
235        } else {
236            false
237        }
238    }
239
240    #[cfg(feature = "python")]
241    fn to_pyobject(&self, py: pyo3::Python<'_>) -> pyo3::PyResult<pyo3::Py<pyo3::PyAny>> {
242        nautilus_model::data::custom::clone_pyclass_to_pyobject(self, py)
243    }
244
245    fn type_name_static() -> &'static str {
246        "BinanceFuturesOpenInterestHist"
247    }
248
249    fn from_json(value: serde_json::Value) -> anyhow::Result<Arc<dyn CustomDataTrait>> {
250        let json_str = serde_json::to_string(&value)?;
251        let parsed: Self = serde_json::from_str(&json_str)?;
252        Ok(Arc::new(parsed))
253    }
254}
255
256/// Binance Futures liquidation update from the `forceOrder` stream.
257#[cfg_attr(
258    feature = "python",
259    pyo3::pyclass(module = "nautilus_trader.adapters.binance", from_py_object)
260)]
261#[cfg_attr(
262    feature = "python",
263    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.binance")
264)]
265#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
266pub struct BinanceFuturesLiquidation {
267    /// The instrument for this liquidation event.
268    pub instrument_id: InstrumentId,
269    /// The liquidation order side.
270    pub side: OrderSide,
271    /// The order price.
272    pub price: Price,
273    /// The average fill price.
274    pub average_price: Price,
275    /// The last filled quantity.
276    pub last_filled_qty: Quantity,
277    /// The cumulative filled quantity.
278    pub accumulated_qty: Quantity,
279    /// UNIX timestamp (nanoseconds) when the data event occurred.
280    pub ts_event: UnixNanos,
281    /// UNIX timestamp (nanoseconds) when the instance was initialized.
282    pub ts_init: UnixNanos,
283}
284
285impl BinanceFuturesLiquidation {
286    /// Creates a new [`BinanceFuturesLiquidation`] instance.
287    #[must_use]
288    #[expect(clippy::too_many_arguments)]
289    pub fn new(
290        instrument_id: InstrumentId,
291        side: OrderSide,
292        price: Price,
293        average_price: Price,
294        last_filled_qty: Quantity,
295        accumulated_qty: Quantity,
296        ts_event: UnixNanos,
297        ts_init: UnixNanos,
298    ) -> Self {
299        Self {
300            instrument_id,
301            side,
302            price,
303            average_price,
304            last_filled_qty,
305            accumulated_qty,
306            ts_event,
307            ts_init,
308        }
309    }
310}
311
312impl HasTsInit for BinanceFuturesLiquidation {
313    fn ts_init(&self) -> UnixNanos {
314        self.ts_init
315    }
316}
317
318impl CustomDataTrait for BinanceFuturesLiquidation {
319    fn type_name(&self) -> &'static str {
320        "BinanceFuturesLiquidation"
321    }
322
323    fn as_any(&self) -> &dyn std::any::Any {
324        self
325    }
326
327    fn ts_event(&self) -> UnixNanos {
328        self.ts_event
329    }
330
331    fn to_json(&self) -> anyhow::Result<String> {
332        Ok(serde_json::to_string(self)?)
333    }
334
335    fn clone_arc(&self) -> Arc<dyn CustomDataTrait> {
336        Arc::new(self.clone())
337    }
338
339    fn eq_arc(&self, other: &dyn CustomDataTrait) -> bool {
340        if let Some(o) = other.as_any().downcast_ref::<Self>() {
341            self == o
342        } else {
343            false
344        }
345    }
346
347    #[cfg(feature = "python")]
348    fn to_pyobject(&self, py: pyo3::Python<'_>) -> pyo3::PyResult<pyo3::Py<pyo3::PyAny>> {
349        nautilus_model::data::custom::clone_pyclass_to_pyobject(self, py)
350    }
351
352    fn type_name_static() -> &'static str {
353        "BinanceFuturesLiquidation"
354    }
355
356    fn from_json(value: serde_json::Value) -> anyhow::Result<Arc<dyn CustomDataTrait>> {
357        let json_str = serde_json::to_string(&value)?;
358        let parsed: Self = serde_json::from_str(&json_str)?;
359        Ok(Arc::new(parsed))
360    }
361}
362
363/// Binance Spot 24-hour ticker statistics from the `ticker` stream.
364#[cfg_attr(
365    feature = "python",
366    pyo3::pyclass(module = "nautilus_trader.adapters.binance", from_py_object)
367)]
368#[cfg_attr(
369    feature = "python",
370    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.binance")
371)]
372#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
373pub struct BinanceSpotTicker {
374    /// The instrument for these 24-hour statistics.
375    pub instrument_id: InstrumentId,
376    /// Price change over the rolling 24-hour window.
377    pub price_change: Decimal,
378    /// Price change percentage over the rolling 24-hour window.
379    pub price_change_percent: Decimal,
380    /// Weighted average price over the rolling 24-hour window.
381    pub weighted_avg_price: Decimal,
382    /// Previous close price before the rolling window.
383    pub prev_close_price: Decimal,
384    /// Last traded price.
385    pub last_price: Decimal,
386    /// Last traded quantity.
387    pub last_qty: Decimal,
388    /// Best bid price.
389    pub bid_price: Decimal,
390    /// Best bid quantity.
391    pub bid_qty: Decimal,
392    /// Best ask price.
393    pub ask_price: Decimal,
394    /// Best ask quantity.
395    pub ask_qty: Decimal,
396    /// Open price for the rolling 24-hour window.
397    pub open_price: Decimal,
398    /// High price for the rolling 24-hour window.
399    pub high_price: Decimal,
400    /// Low price for the rolling 24-hour window.
401    pub low_price: Decimal,
402    /// Total traded base asset volume.
403    pub volume: Decimal,
404    /// Total traded quote asset volume.
405    pub quote_volume: Decimal,
406    /// Statistics open time.
407    pub open_time: UnixNanos,
408    /// Statistics close time.
409    pub close_time: UnixNanos,
410    /// First trade ID included in the statistics window.
411    pub first_trade_id: i64,
412    /// Last trade ID included in the statistics window.
413    pub last_trade_id: i64,
414    /// Total number of trades in the statistics window.
415    pub num_trades: i64,
416    /// UNIX timestamp (nanoseconds) when the ticker event occurred.
417    pub ts_event: UnixNanos,
418    /// UNIX timestamp (nanoseconds) when the instance was initialized.
419    pub ts_init: UnixNanos,
420}
421
422impl HasTsInit for BinanceSpotTicker {
423    fn ts_init(&self) -> UnixNanos {
424        self.ts_init
425    }
426}
427
428impl CustomDataTrait for BinanceSpotTicker {
429    fn type_name(&self) -> &'static str {
430        "BinanceSpotTicker"
431    }
432
433    fn as_any(&self) -> &dyn std::any::Any {
434        self
435    }
436
437    fn ts_event(&self) -> UnixNanos {
438        self.ts_event
439    }
440
441    fn to_json(&self) -> anyhow::Result<String> {
442        Ok(serde_json::to_string(self)?)
443    }
444
445    fn clone_arc(&self) -> Arc<dyn CustomDataTrait> {
446        Arc::new(self.clone())
447    }
448
449    fn eq_arc(&self, other: &dyn CustomDataTrait) -> bool {
450        other.as_any().downcast_ref::<Self>() == Some(self)
451    }
452
453    #[cfg(feature = "python")]
454    fn to_pyobject(&self, py: pyo3::Python<'_>) -> pyo3::PyResult<pyo3::Py<pyo3::PyAny>> {
455        nautilus_model::data::custom::clone_pyclass_to_pyobject(self, py)
456    }
457
458    fn type_name_static() -> &'static str {
459        "BinanceSpotTicker"
460    }
461
462    fn from_json(value: serde_json::Value) -> anyhow::Result<Arc<dyn CustomDataTrait>> {
463        let json_str = serde_json::to_string(&value)?;
464        Ok(Arc::new(serde_json::from_str::<Self>(&json_str)?))
465    }
466}
467
468/// Binance Futures mark-price stream update with venue-specific fields.
469#[cfg_attr(
470    feature = "python",
471    pyo3::pyclass(module = "nautilus_trader.adapters.binance", from_py_object)
472)]
473#[cfg_attr(
474    feature = "python",
475    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.binance")
476)]
477#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
478pub struct BinanceFuturesMarkPriceUpdate {
479    /// The instrument for this update.
480    pub instrument_id: InstrumentId,
481    /// Mark price.
482    pub mark_price: Price,
483    /// Index price.
484    pub index_price: Price,
485    /// Estimated settlement price.
486    pub estimated_settle_price: Price,
487    /// Funding rate.
488    pub funding_rate: Decimal,
489    /// Next funding time.
490    pub next_funding_time: Option<UnixNanos>,
491    /// UNIX timestamp (nanoseconds) when the update occurred.
492    pub ts_event: UnixNanos,
493    /// UNIX timestamp (nanoseconds) when the instance was initialized.
494    pub ts_init: UnixNanos,
495}
496
497impl HasTsInit for BinanceFuturesMarkPriceUpdate {
498    fn ts_init(&self) -> UnixNanos {
499        self.ts_init
500    }
501}
502
503impl CustomDataTrait for BinanceFuturesMarkPriceUpdate {
504    fn type_name(&self) -> &'static str {
505        "BinanceFuturesMarkPriceUpdate"
506    }
507
508    fn as_any(&self) -> &dyn std::any::Any {
509        self
510    }
511
512    fn ts_event(&self) -> UnixNanos {
513        self.ts_event
514    }
515
516    fn to_json(&self) -> anyhow::Result<String> {
517        Ok(serde_json::to_string(self)?)
518    }
519
520    fn clone_arc(&self) -> Arc<dyn CustomDataTrait> {
521        Arc::new(self.clone())
522    }
523
524    fn eq_arc(&self, other: &dyn CustomDataTrait) -> bool {
525        other.as_any().downcast_ref::<Self>() == Some(self)
526    }
527
528    #[cfg(feature = "python")]
529    fn to_pyobject(&self, py: pyo3::Python<'_>) -> pyo3::PyResult<pyo3::Py<pyo3::PyAny>> {
530        nautilus_model::data::custom::clone_pyclass_to_pyobject(self, py)
531    }
532
533    fn type_name_static() -> &'static str {
534        "BinanceFuturesMarkPriceUpdate"
535    }
536
537    fn from_json(value: serde_json::Value) -> anyhow::Result<Arc<dyn CustomDataTrait>> {
538        let json_str = serde_json::to_string(&value)?;
539        Ok(Arc::new(serde_json::from_str::<Self>(&json_str)?))
540    }
541}
542
543/// Binance Futures 24-hour ticker statistics from the `ticker` stream.
544#[cfg_attr(
545    feature = "python",
546    pyo3::pyclass(module = "nautilus_trader.adapters.binance", from_py_object)
547)]
548#[cfg_attr(
549    feature = "python",
550    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.binance")
551)]
552#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
553pub struct BinanceFuturesTicker {
554    /// The instrument for these 24-hour statistics.
555    pub instrument_id: InstrumentId,
556    /// Price change over the rolling 24-hour window.
557    pub price_change: Decimal,
558    /// Price change percentage over the rolling 24-hour window.
559    pub price_change_percent: Decimal,
560    /// Weighted average price over the rolling 24-hour window.
561    pub weighted_avg_price: Decimal,
562    /// Last traded price.
563    pub last_price: Decimal,
564    /// Last traded quantity.
565    pub last_qty: Decimal,
566    /// Open price for the rolling 24-hour window.
567    pub open_price: Decimal,
568    /// High price for the rolling 24-hour window.
569    pub high_price: Decimal,
570    /// Low price for the rolling 24-hour window.
571    pub low_price: Decimal,
572    /// Total traded base asset volume.
573    pub volume: Decimal,
574    /// Total traded quote asset volume.
575    pub quote_volume: Decimal,
576    /// Statistics open time.
577    pub open_time: UnixNanos,
578    /// Statistics close time.
579    pub close_time: UnixNanos,
580    /// First trade ID included in the statistics window.
581    pub first_trade_id: i64,
582    /// Last trade ID included in the statistics window.
583    pub last_trade_id: i64,
584    /// Total number of trades in the statistics window.
585    pub num_trades: i64,
586    /// UNIX timestamp (nanoseconds) when the ticker event occurred.
587    pub ts_event: UnixNanos,
588    /// UNIX timestamp (nanoseconds) when the instance was initialized.
589    pub ts_init: UnixNanos,
590}
591
592impl BinanceFuturesTicker {
593    /// Creates a new [`BinanceFuturesTicker`] instance.
594    #[must_use]
595    #[expect(clippy::too_many_arguments)]
596    pub fn new(
597        instrument_id: InstrumentId,
598        price_change: Decimal,
599        price_change_percent: Decimal,
600        weighted_avg_price: Decimal,
601        last_price: Decimal,
602        last_qty: Decimal,
603        open_price: Decimal,
604        high_price: Decimal,
605        low_price: Decimal,
606        volume: Decimal,
607        quote_volume: Decimal,
608        open_time: UnixNanos,
609        close_time: UnixNanos,
610        first_trade_id: i64,
611        last_trade_id: i64,
612        num_trades: i64,
613        ts_event: UnixNanos,
614        ts_init: UnixNanos,
615    ) -> Self {
616        Self {
617            instrument_id,
618            price_change,
619            price_change_percent,
620            weighted_avg_price,
621            last_price,
622            last_qty,
623            open_price,
624            high_price,
625            low_price,
626            volume,
627            quote_volume,
628            open_time,
629            close_time,
630            first_trade_id,
631            last_trade_id,
632            num_trades,
633            ts_event,
634            ts_init,
635        }
636    }
637}
638
639impl HasTsInit for BinanceFuturesTicker {
640    fn ts_init(&self) -> UnixNanos {
641        self.ts_init
642    }
643}
644
645impl CustomDataTrait for BinanceFuturesTicker {
646    fn type_name(&self) -> &'static str {
647        "BinanceFuturesTicker"
648    }
649
650    fn as_any(&self) -> &dyn std::any::Any {
651        self
652    }
653
654    fn ts_event(&self) -> UnixNanos {
655        self.ts_event
656    }
657
658    fn to_json(&self) -> anyhow::Result<String> {
659        Ok(serde_json::to_string(self)?)
660    }
661
662    fn clone_arc(&self) -> Arc<dyn CustomDataTrait> {
663        Arc::new(self.clone())
664    }
665
666    fn eq_arc(&self, other: &dyn CustomDataTrait) -> bool {
667        if let Some(o) = other.as_any().downcast_ref::<Self>() {
668            self == o
669        } else {
670            false
671        }
672    }
673
674    #[cfg(feature = "python")]
675    fn to_pyobject(&self, py: pyo3::Python<'_>) -> pyo3::PyResult<pyo3::Py<pyo3::PyAny>> {
676        nautilus_model::data::custom::clone_pyclass_to_pyobject(self, py)
677    }
678
679    fn type_name_static() -> &'static str {
680        "BinanceFuturesTicker"
681    }
682
683    fn from_json(value: serde_json::Value) -> anyhow::Result<Arc<dyn CustomDataTrait>> {
684        let json_str = serde_json::to_string(&value)?;
685        let parsed: Self = serde_json::from_str(&json_str)?;
686        Ok(Arc::new(parsed))
687    }
688}
689
690/// Registers Binance custom data types.
691///
692/// Safe to call multiple times (idempotent via internal `Once` guards).
693pub fn register_binance_custom_data() {
694    let _ =
695        nautilus_model::data::ensure_custom_data_json_registered::<BinanceFuturesOpenInterest>();
696    let _ = nautilus_model::data::ensure_custom_data_json_registered::<
697        BinanceFuturesOpenInterestHist,
698    >();
699    let _ = nautilus_model::data::ensure_custom_data_json_registered::<BinanceFuturesLiquidation>();
700    let _ = nautilus_model::data::ensure_custom_data_json_registered::<BinanceFuturesTicker>();
701    let _ = nautilus_model::data::ensure_custom_data_json_registered::<BinanceSpotTicker>();
702    let _ =
703        nautilus_model::data::ensure_custom_data_json_registered::<BinanceFuturesMarkPriceUpdate>();
704}
705
706#[cfg(test)]
707mod tests {
708    #[cfg(feature = "python")]
709    use std::sync::Arc;
710
711    #[cfg(feature = "python")]
712    use nautilus_core::Params;
713    #[cfg(feature = "python")]
714    use nautilus_model::data::{CustomData, DataType};
715    #[cfg(feature = "python")]
716    use pyo3::{prelude::*, types::PyList};
717    use rstest::rstest;
718    #[cfg(feature = "python")]
719    use rust_decimal::Decimal;
720
721    use super::*;
722
723    #[rstest]
724    fn test_register_binance_custom_data_is_idempotent() {
725        register_binance_custom_data();
726        register_binance_custom_data();
727    }
728
729    #[cfg(feature = "python")]
730    #[rstest]
731    fn test_open_interest_hist_points_roundtrip_as_typed_python_list() {
732        pyo3::Python::initialize();
733        register_binance_custom_data();
734
735        Python::attach(|py| {
736            let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
737            let points = vec![
738                BinanceFuturesOpenInterestHistPoint::new(
739                    Decimal::from_str_exact("100.0").unwrap(),
740                    Decimal::from_str_exact("1000.0").unwrap(),
741                    UnixNanos::from_millis(1_700_000_000_000),
742                ),
743                BinanceFuturesOpenInterestHistPoint::new(
744                    Decimal::from_str_exact("101.0").unwrap(),
745                    Decimal::from_str_exact("1005.0").unwrap(),
746                    UnixNanos::from_millis(1_700_000_300_000),
747                ),
748            ];
749            let payload = BinanceFuturesOpenInterestHist::new(
750                instrument_id,
751                "5m".to_string(),
752                points,
753                UnixNanos::from_millis(1_700_000_300_000),
754                UnixNanos::from(42_u64),
755            );
756
757            let mut metadata = Params::new();
758            metadata.insert(
759                "instrument_id".to_string(),
760                serde_json::Value::String("BTCUSDT-PERP.BINANCE".to_string()),
761            );
762            metadata.insert(
763                "period".to_string(),
764                serde_json::Value::String("5m".to_string()),
765            );
766
767            let custom = CustomData::new(
768                Arc::new(payload),
769                DataType::new(
770                    "BinanceFuturesOpenInterestHist",
771                    Some(metadata),
772                    Some("BTCUSDT-PERP.BINANCE".to_string()),
773                ),
774            );
775
776            let py_custom = Py::new(py, custom).unwrap();
777            let py_payload = py_custom.bind(py).getattr("data").unwrap();
778            let py_points = py_payload
779                .getattr("points")
780                .unwrap()
781                .cast_into::<PyList>()
782                .unwrap();
783
784            assert_eq!(py_points.len(), 2);
785            assert!(
786                py_points
787                    .get_item(0)
788                    .unwrap()
789                    .is_instance_of::<BinanceFuturesOpenInterestHistPoint>()
790            );
791
792            let point0 = py_points
793                .get_item(0)
794                .unwrap()
795                .extract::<BinanceFuturesOpenInterestHistPoint>()
796                .unwrap();
797            let point1 = py_points
798                .get_item(1)
799                .unwrap()
800                .extract::<BinanceFuturesOpenInterestHistPoint>()
801                .unwrap();
802
803            assert_eq!(
804                point0.sum_open_interest,
805                Decimal::from_str_exact("100.0").unwrap()
806            );
807            assert_eq!(
808                point1.sum_open_interest_value,
809                Decimal::from_str_exact("1005.0").unwrap()
810            );
811        });
812    }
813}