1use 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#[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 pub instrument_id: InstrumentId,
46 pub open_interest: Decimal,
48 pub ts_event: UnixNanos,
50 pub ts_init: UnixNanos,
52}
53
54impl BinanceFuturesOpenInterest {
55 #[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#[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 pub sum_open_interest: Decimal,
136 pub sum_open_interest_value: Decimal,
138 pub ts_event: UnixNanos,
140}
141
142impl BinanceFuturesOpenInterestHistPoint {
143 #[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#[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 pub instrument_id: InstrumentId,
176 pub period: String,
178 pub points: Vec<BinanceFuturesOpenInterestHistPoint>,
180 pub ts_event: UnixNanos,
182 pub ts_init: UnixNanos,
184}
185
186impl BinanceFuturesOpenInterestHist {
187 #[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#[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 pub instrument_id: InstrumentId,
270 pub side: OrderSide,
272 pub price: Price,
274 pub average_price: Price,
276 pub last_filled_qty: Quantity,
278 pub accumulated_qty: Quantity,
280 pub ts_event: UnixNanos,
282 pub ts_init: UnixNanos,
284}
285
286impl BinanceFuturesLiquidation {
287 #[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#[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 pub instrument_id: InstrumentId,
377 pub price_change: Decimal,
379 pub price_change_percent: Decimal,
381 pub weighted_avg_price: Decimal,
383 pub last_price: Decimal,
385 pub last_qty: Decimal,
387 pub open_price: Decimal,
389 pub high_price: Decimal,
391 pub low_price: Decimal,
393 pub volume: Decimal,
395 pub quote_volume: Decimal,
397 pub open_time: UnixNanos,
399 pub close_time: UnixNanos,
401 pub first_trade_id: i64,
403 pub last_trade_id: i64,
405 pub num_trades: i64,
407 pub ts_event: UnixNanos,
409 pub ts_init: UnixNanos,
411}
412
413impl BinanceFuturesTicker {
414 #[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
511pub 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}