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.core.nautilus_pyo3.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.core.nautilus_pyo3.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/// For COIN-M requests, the current Binance adapter support is limited to
161/// perpetual instruments. Although Binance also exposes quarter-delivery
162/// contract types on the historical OI endpoint, the futures instrument
163/// parsing/symbology path in this adapter is still perpetual-only.
164#[cfg_attr(
165    feature = "python",
166    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.binance", from_py_object)
167)]
168#[cfg_attr(
169    feature = "python",
170    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.binance")
171)]
172#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
173pub struct BinanceFuturesOpenInterestHist {
174    /// The instrument for this batch.
175    pub instrument_id: InstrumentId,
176    /// The Binance period string used for the request (e.g. "5m").
177    pub period: String,
178    /// Ordered open interest history points returned by Binance.
179    pub points: Vec<BinanceFuturesOpenInterestHistPoint>,
180    /// UNIX timestamp (nanoseconds) for the batch, represented by the final point.
181    pub ts_event: UnixNanos,
182    /// UNIX timestamp (nanoseconds) when the instance was initialized.
183    pub ts_init: UnixNanos,
184}
185
186impl BinanceFuturesOpenInterestHist {
187    /// Creates a new [`BinanceFuturesOpenInterestHist`] instance.
188    #[must_use]
189    pub fn new(
190        instrument_id: InstrumentId,
191        period: String,
192        points: Vec<BinanceFuturesOpenInterestHistPoint>,
193        ts_event: UnixNanos,
194        ts_init: UnixNanos,
195    ) -> Self {
196        Self {
197            instrument_id,
198            period,
199            points,
200            ts_event,
201            ts_init,
202        }
203    }
204}
205
206impl HasTsInit for BinanceFuturesOpenInterestHist {
207    fn ts_init(&self) -> UnixNanos {
208        self.ts_init
209    }
210}
211
212impl CustomDataTrait for BinanceFuturesOpenInterestHist {
213    fn type_name(&self) -> &'static str {
214        "BinanceFuturesOpenInterestHist"
215    }
216
217    fn as_any(&self) -> &dyn std::any::Any {
218        self
219    }
220
221    fn ts_event(&self) -> UnixNanos {
222        self.ts_event
223    }
224
225    fn to_json(&self) -> anyhow::Result<String> {
226        Ok(serde_json::to_string(self)?)
227    }
228
229    fn clone_arc(&self) -> Arc<dyn CustomDataTrait> {
230        Arc::new(self.clone())
231    }
232
233    fn eq_arc(&self, other: &dyn CustomDataTrait) -> bool {
234        if let Some(o) = other.as_any().downcast_ref::<Self>() {
235            self == o
236        } else {
237            false
238        }
239    }
240
241    #[cfg(feature = "python")]
242    fn to_pyobject(&self, py: pyo3::Python<'_>) -> pyo3::PyResult<pyo3::Py<pyo3::PyAny>> {
243        nautilus_model::data::custom::clone_pyclass_to_pyobject(self, py)
244    }
245
246    fn type_name_static() -> &'static str {
247        "BinanceFuturesOpenInterestHist"
248    }
249
250    fn from_json(value: serde_json::Value) -> anyhow::Result<Arc<dyn CustomDataTrait>> {
251        let json_str = serde_json::to_string(&value)?;
252        let parsed: Self = serde_json::from_str(&json_str)?;
253        Ok(Arc::new(parsed))
254    }
255}
256
257/// Binance Futures liquidation update from the `forceOrder` stream.
258#[cfg_attr(
259    feature = "python",
260    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.binance", from_py_object)
261)]
262#[cfg_attr(
263    feature = "python",
264    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.binance")
265)]
266#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
267pub struct BinanceFuturesLiquidation {
268    /// The instrument for this liquidation event.
269    pub instrument_id: InstrumentId,
270    /// The liquidation order side.
271    pub side: OrderSide,
272    /// The order price.
273    pub price: Price,
274    /// The average fill price.
275    pub average_price: Price,
276    /// The last filled quantity.
277    pub last_filled_qty: Quantity,
278    /// The cumulative filled quantity.
279    pub accumulated_qty: Quantity,
280    /// UNIX timestamp (nanoseconds) when the data event occurred.
281    pub ts_event: UnixNanos,
282    /// UNIX timestamp (nanoseconds) when the instance was initialized.
283    pub ts_init: UnixNanos,
284}
285
286impl BinanceFuturesLiquidation {
287    /// Creates a new [`BinanceFuturesLiquidation`] instance.
288    #[must_use]
289    #[expect(clippy::too_many_arguments)]
290    pub fn new(
291        instrument_id: InstrumentId,
292        side: OrderSide,
293        price: Price,
294        average_price: Price,
295        last_filled_qty: Quantity,
296        accumulated_qty: Quantity,
297        ts_event: UnixNanos,
298        ts_init: UnixNanos,
299    ) -> Self {
300        Self {
301            instrument_id,
302            side,
303            price,
304            average_price,
305            last_filled_qty,
306            accumulated_qty,
307            ts_event,
308            ts_init,
309        }
310    }
311}
312
313impl HasTsInit for BinanceFuturesLiquidation {
314    fn ts_init(&self) -> UnixNanos {
315        self.ts_init
316    }
317}
318
319impl CustomDataTrait for BinanceFuturesLiquidation {
320    fn type_name(&self) -> &'static str {
321        "BinanceFuturesLiquidation"
322    }
323
324    fn as_any(&self) -> &dyn std::any::Any {
325        self
326    }
327
328    fn ts_event(&self) -> UnixNanos {
329        self.ts_event
330    }
331
332    fn to_json(&self) -> anyhow::Result<String> {
333        Ok(serde_json::to_string(self)?)
334    }
335
336    fn clone_arc(&self) -> Arc<dyn CustomDataTrait> {
337        Arc::new(self.clone())
338    }
339
340    fn eq_arc(&self, other: &dyn CustomDataTrait) -> bool {
341        if let Some(o) = other.as_any().downcast_ref::<Self>() {
342            self == o
343        } else {
344            false
345        }
346    }
347
348    #[cfg(feature = "python")]
349    fn to_pyobject(&self, py: pyo3::Python<'_>) -> pyo3::PyResult<pyo3::Py<pyo3::PyAny>> {
350        nautilus_model::data::custom::clone_pyclass_to_pyobject(self, py)
351    }
352
353    fn type_name_static() -> &'static str {
354        "BinanceFuturesLiquidation"
355    }
356
357    fn from_json(value: serde_json::Value) -> anyhow::Result<Arc<dyn CustomDataTrait>> {
358        let json_str = serde_json::to_string(&value)?;
359        let parsed: Self = serde_json::from_str(&json_str)?;
360        Ok(Arc::new(parsed))
361    }
362}
363
364/// Binance Futures 24-hour ticker statistics from the `ticker` stream.
365#[cfg_attr(
366    feature = "python",
367    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.binance", from_py_object)
368)]
369#[cfg_attr(
370    feature = "python",
371    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.binance")
372)]
373#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
374pub struct BinanceFuturesTicker {
375    /// The instrument for these 24-hour statistics.
376    pub instrument_id: InstrumentId,
377    /// Price change over the rolling 24-hour window.
378    pub price_change: Decimal,
379    /// Price change percentage over the rolling 24-hour window.
380    pub price_change_percent: Decimal,
381    /// Weighted average price over the rolling 24-hour window.
382    pub weighted_avg_price: Decimal,
383    /// Last traded price.
384    pub last_price: Decimal,
385    /// Last traded quantity.
386    pub last_qty: Decimal,
387    /// Open price for the rolling 24-hour window.
388    pub open_price: Decimal,
389    /// High price for the rolling 24-hour window.
390    pub high_price: Decimal,
391    /// Low price for the rolling 24-hour window.
392    pub low_price: Decimal,
393    /// Total traded base asset volume.
394    pub volume: Decimal,
395    /// Total traded quote asset volume.
396    pub quote_volume: Decimal,
397    /// Statistics open time.
398    pub open_time: UnixNanos,
399    /// Statistics close time.
400    pub close_time: UnixNanos,
401    /// First trade ID included in the statistics window.
402    pub first_trade_id: i64,
403    /// Last trade ID included in the statistics window.
404    pub last_trade_id: i64,
405    /// Total number of trades in the statistics window.
406    pub num_trades: i64,
407    /// UNIX timestamp (nanoseconds) when the ticker event occurred.
408    pub ts_event: UnixNanos,
409    /// UNIX timestamp (nanoseconds) when the instance was initialized.
410    pub ts_init: UnixNanos,
411}
412
413impl BinanceFuturesTicker {
414    /// Creates a new [`BinanceFuturesTicker`] instance.
415    #[must_use]
416    #[expect(clippy::too_many_arguments)]
417    pub fn new(
418        instrument_id: InstrumentId,
419        price_change: Decimal,
420        price_change_percent: Decimal,
421        weighted_avg_price: Decimal,
422        last_price: Decimal,
423        last_qty: Decimal,
424        open_price: Decimal,
425        high_price: Decimal,
426        low_price: Decimal,
427        volume: Decimal,
428        quote_volume: Decimal,
429        open_time: UnixNanos,
430        close_time: UnixNanos,
431        first_trade_id: i64,
432        last_trade_id: i64,
433        num_trades: i64,
434        ts_event: UnixNanos,
435        ts_init: UnixNanos,
436    ) -> Self {
437        Self {
438            instrument_id,
439            price_change,
440            price_change_percent,
441            weighted_avg_price,
442            last_price,
443            last_qty,
444            open_price,
445            high_price,
446            low_price,
447            volume,
448            quote_volume,
449            open_time,
450            close_time,
451            first_trade_id,
452            last_trade_id,
453            num_trades,
454            ts_event,
455            ts_init,
456        }
457    }
458}
459
460impl HasTsInit for BinanceFuturesTicker {
461    fn ts_init(&self) -> UnixNanos {
462        self.ts_init
463    }
464}
465
466impl CustomDataTrait for BinanceFuturesTicker {
467    fn type_name(&self) -> &'static str {
468        "BinanceFuturesTicker"
469    }
470
471    fn as_any(&self) -> &dyn std::any::Any {
472        self
473    }
474
475    fn ts_event(&self) -> UnixNanos {
476        self.ts_event
477    }
478
479    fn to_json(&self) -> anyhow::Result<String> {
480        Ok(serde_json::to_string(self)?)
481    }
482
483    fn clone_arc(&self) -> Arc<dyn CustomDataTrait> {
484        Arc::new(self.clone())
485    }
486
487    fn eq_arc(&self, other: &dyn CustomDataTrait) -> bool {
488        if let Some(o) = other.as_any().downcast_ref::<Self>() {
489            self == o
490        } else {
491            false
492        }
493    }
494
495    #[cfg(feature = "python")]
496    fn to_pyobject(&self, py: pyo3::Python<'_>) -> pyo3::PyResult<pyo3::Py<pyo3::PyAny>> {
497        nautilus_model::data::custom::clone_pyclass_to_pyobject(self, py)
498    }
499
500    fn type_name_static() -> &'static str {
501        "BinanceFuturesTicker"
502    }
503
504    fn from_json(value: serde_json::Value) -> anyhow::Result<Arc<dyn CustomDataTrait>> {
505        let json_str = serde_json::to_string(&value)?;
506        let parsed: Self = serde_json::from_str(&json_str)?;
507        Ok(Arc::new(parsed))
508    }
509}
510
511/// Registers Binance custom data types.
512///
513/// Safe to call multiple times (idempotent via internal `Once` guards).
514pub fn register_binance_custom_data() {
515    let _ =
516        nautilus_model::data::ensure_custom_data_json_registered::<BinanceFuturesOpenInterest>();
517    let _ = nautilus_model::data::ensure_custom_data_json_registered::<
518        BinanceFuturesOpenInterestHist,
519    >();
520    let _ = nautilus_model::data::ensure_custom_data_json_registered::<BinanceFuturesLiquidation>();
521    let _ = nautilus_model::data::ensure_custom_data_json_registered::<BinanceFuturesTicker>();
522}
523
524#[cfg(test)]
525mod tests {
526    #[cfg(feature = "python")]
527    use std::sync::Arc;
528
529    #[cfg(feature = "python")]
530    use nautilus_core::Params;
531    #[cfg(feature = "python")]
532    use nautilus_model::data::{CustomData, DataType};
533    #[cfg(feature = "python")]
534    use pyo3::{prelude::*, types::PyList};
535    use rstest::rstest;
536    #[cfg(feature = "python")]
537    use rust_decimal::Decimal;
538
539    use super::*;
540
541    #[rstest]
542    fn test_register_binance_custom_data_is_idempotent() {
543        register_binance_custom_data();
544        register_binance_custom_data();
545    }
546
547    #[cfg(feature = "python")]
548    #[rstest]
549    fn test_open_interest_hist_points_roundtrip_as_typed_python_list() {
550        pyo3::Python::initialize();
551        register_binance_custom_data();
552
553        Python::attach(|py| {
554            let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
555            let points = vec![
556                BinanceFuturesOpenInterestHistPoint::new(
557                    Decimal::from_str_exact("100.0").unwrap(),
558                    Decimal::from_str_exact("1000.0").unwrap(),
559                    UnixNanos::from_millis(1_700_000_000_000),
560                ),
561                BinanceFuturesOpenInterestHistPoint::new(
562                    Decimal::from_str_exact("101.0").unwrap(),
563                    Decimal::from_str_exact("1005.0").unwrap(),
564                    UnixNanos::from_millis(1_700_000_300_000),
565                ),
566            ];
567            let payload = BinanceFuturesOpenInterestHist::new(
568                instrument_id,
569                "5m".to_string(),
570                points,
571                UnixNanos::from_millis(1_700_000_300_000),
572                UnixNanos::from(42_u64),
573            );
574
575            let mut metadata = Params::new();
576            metadata.insert(
577                "instrument_id".to_string(),
578                serde_json::Value::String("BTCUSDT-PERP.BINANCE".to_string()),
579            );
580            metadata.insert(
581                "period".to_string(),
582                serde_json::Value::String("5m".to_string()),
583            );
584
585            let custom = CustomData::new(
586                Arc::new(payload),
587                DataType::new(
588                    "BinanceFuturesOpenInterestHist",
589                    Some(metadata),
590                    Some("BTCUSDT-PERP.BINANCE".to_string()),
591                ),
592            );
593
594            let py_custom = Py::new(py, custom).unwrap();
595            let py_payload = py_custom.bind(py).getattr("data").unwrap();
596            let py_points = py_payload
597                .getattr("points")
598                .unwrap()
599                .cast_into::<PyList>()
600                .unwrap();
601
602            assert_eq!(py_points.len(), 2);
603            assert!(
604                py_points
605                    .get_item(0)
606                    .unwrap()
607                    .is_instance_of::<BinanceFuturesOpenInterestHistPoint>()
608            );
609
610            let point0 = py_points
611                .get_item(0)
612                .unwrap()
613                .extract::<BinanceFuturesOpenInterestHistPoint>()
614                .unwrap();
615            let point1 = py_points
616                .get_item(1)
617                .unwrap()
618                .extract::<BinanceFuturesOpenInterestHistPoint>()
619                .unwrap();
620
621            assert_eq!(
622                point0.sum_open_interest,
623                Decimal::from_str_exact("100.0").unwrap()
624            );
625            assert_eq!(
626                point1.sum_open_interest_value,
627                Decimal::from_str_exact("1005.0").unwrap()
628            );
629        });
630    }
631}