use std::sync::Arc;
use nautilus_core::UnixNanos;
use nautilus_model::{
data::{HasTsInit, custom::CustomDataTrait},
enums::OrderSide,
identifiers::InstrumentId,
types::{Price, Quantity},
};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.binance", from_py_object)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.binance")
)]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BinanceFuturesOpenInterest {
pub instrument_id: InstrumentId,
pub open_interest: Decimal,
pub ts_event: UnixNanos,
pub ts_init: UnixNanos,
}
impl BinanceFuturesOpenInterest {
#[must_use]
pub fn new(
instrument_id: InstrumentId,
open_interest: Decimal,
ts_event: UnixNanos,
ts_init: UnixNanos,
) -> Self {
Self {
instrument_id,
open_interest,
ts_event,
ts_init,
}
}
}
impl HasTsInit for BinanceFuturesOpenInterest {
fn ts_init(&self) -> UnixNanos {
self.ts_init
}
}
impl CustomDataTrait for BinanceFuturesOpenInterest {
fn type_name(&self) -> &'static str {
"BinanceFuturesOpenInterest"
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn ts_event(&self) -> UnixNanos {
self.ts_event
}
fn to_json(&self) -> anyhow::Result<String> {
Ok(serde_json::to_string(self)?)
}
fn clone_arc(&self) -> Arc<dyn CustomDataTrait> {
Arc::new(self.clone())
}
fn eq_arc(&self, other: &dyn CustomDataTrait) -> bool {
if let Some(o) = other.as_any().downcast_ref::<Self>() {
self == o
} else {
false
}
}
#[cfg(feature = "python")]
fn to_pyobject(&self, py: pyo3::Python<'_>) -> pyo3::PyResult<pyo3::Py<pyo3::PyAny>> {
nautilus_model::data::custom::clone_pyclass_to_pyobject(self, py)
}
fn type_name_static() -> &'static str {
"BinanceFuturesOpenInterest"
}
fn from_json(value: serde_json::Value) -> anyhow::Result<Arc<dyn CustomDataTrait>> {
let json_str = serde_json::to_string(&value)?;
let parsed: Self = serde_json::from_str(&json_str)?;
Ok(Arc::new(parsed))
}
}
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.binance", from_py_object)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.binance")
)]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BinanceFuturesOpenInterestHistPoint {
pub sum_open_interest: Decimal,
pub sum_open_interest_value: Decimal,
pub ts_event: UnixNanos,
}
impl BinanceFuturesOpenInterestHistPoint {
#[must_use]
pub fn new(
sum_open_interest: Decimal,
sum_open_interest_value: Decimal,
ts_event: UnixNanos,
) -> Self {
Self {
sum_open_interest,
sum_open_interest_value,
ts_event,
}
}
}
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.binance", from_py_object)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.binance")
)]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BinanceFuturesOpenInterestHist {
pub instrument_id: InstrumentId,
pub period: String,
pub points: Vec<BinanceFuturesOpenInterestHistPoint>,
pub ts_event: UnixNanos,
pub ts_init: UnixNanos,
}
impl BinanceFuturesOpenInterestHist {
#[must_use]
pub fn new(
instrument_id: InstrumentId,
period: String,
points: Vec<BinanceFuturesOpenInterestHistPoint>,
ts_event: UnixNanos,
ts_init: UnixNanos,
) -> Self {
Self {
instrument_id,
period,
points,
ts_event,
ts_init,
}
}
}
impl HasTsInit for BinanceFuturesOpenInterestHist {
fn ts_init(&self) -> UnixNanos {
self.ts_init
}
}
impl CustomDataTrait for BinanceFuturesOpenInterestHist {
fn type_name(&self) -> &'static str {
"BinanceFuturesOpenInterestHist"
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn ts_event(&self) -> UnixNanos {
self.ts_event
}
fn to_json(&self) -> anyhow::Result<String> {
Ok(serde_json::to_string(self)?)
}
fn clone_arc(&self) -> Arc<dyn CustomDataTrait> {
Arc::new(self.clone())
}
fn eq_arc(&self, other: &dyn CustomDataTrait) -> bool {
if let Some(o) = other.as_any().downcast_ref::<Self>() {
self == o
} else {
false
}
}
#[cfg(feature = "python")]
fn to_pyobject(&self, py: pyo3::Python<'_>) -> pyo3::PyResult<pyo3::Py<pyo3::PyAny>> {
nautilus_model::data::custom::clone_pyclass_to_pyobject(self, py)
}
fn type_name_static() -> &'static str {
"BinanceFuturesOpenInterestHist"
}
fn from_json(value: serde_json::Value) -> anyhow::Result<Arc<dyn CustomDataTrait>> {
let json_str = serde_json::to_string(&value)?;
let parsed: Self = serde_json::from_str(&json_str)?;
Ok(Arc::new(parsed))
}
}
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.binance", from_py_object)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.binance")
)]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct BinanceFuturesLiquidation {
pub instrument_id: InstrumentId,
pub side: OrderSide,
pub price: Price,
pub average_price: Price,
pub last_filled_qty: Quantity,
pub accumulated_qty: Quantity,
pub ts_event: UnixNanos,
pub ts_init: UnixNanos,
}
impl BinanceFuturesLiquidation {
#[must_use]
#[expect(clippy::too_many_arguments)]
pub fn new(
instrument_id: InstrumentId,
side: OrderSide,
price: Price,
average_price: Price,
last_filled_qty: Quantity,
accumulated_qty: Quantity,
ts_event: UnixNanos,
ts_init: UnixNanos,
) -> Self {
Self {
instrument_id,
side,
price,
average_price,
last_filled_qty,
accumulated_qty,
ts_event,
ts_init,
}
}
}
impl HasTsInit for BinanceFuturesLiquidation {
fn ts_init(&self) -> UnixNanos {
self.ts_init
}
}
impl CustomDataTrait for BinanceFuturesLiquidation {
fn type_name(&self) -> &'static str {
"BinanceFuturesLiquidation"
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn ts_event(&self) -> UnixNanos {
self.ts_event
}
fn to_json(&self) -> anyhow::Result<String> {
Ok(serde_json::to_string(self)?)
}
fn clone_arc(&self) -> Arc<dyn CustomDataTrait> {
Arc::new(self.clone())
}
fn eq_arc(&self, other: &dyn CustomDataTrait) -> bool {
if let Some(o) = other.as_any().downcast_ref::<Self>() {
self == o
} else {
false
}
}
#[cfg(feature = "python")]
fn to_pyobject(&self, py: pyo3::Python<'_>) -> pyo3::PyResult<pyo3::Py<pyo3::PyAny>> {
nautilus_model::data::custom::clone_pyclass_to_pyobject(self, py)
}
fn type_name_static() -> &'static str {
"BinanceFuturesLiquidation"
}
fn from_json(value: serde_json::Value) -> anyhow::Result<Arc<dyn CustomDataTrait>> {
let json_str = serde_json::to_string(&value)?;
let parsed: Self = serde_json::from_str(&json_str)?;
Ok(Arc::new(parsed))
}
}
pub fn register_binance_custom_data() {
let _ =
nautilus_model::data::ensure_custom_data_json_registered::<BinanceFuturesOpenInterest>();
let _ = nautilus_model::data::ensure_custom_data_json_registered::<
BinanceFuturesOpenInterestHist,
>();
let _ = nautilus_model::data::ensure_custom_data_json_registered::<BinanceFuturesLiquidation>();
}
#[cfg(test)]
mod tests {
#[cfg(feature = "python")]
use std::sync::Arc;
#[cfg(feature = "python")]
use nautilus_core::Params;
#[cfg(feature = "python")]
use nautilus_model::data::{CustomData, DataType};
#[cfg(feature = "python")]
use pyo3::{prelude::*, types::PyList};
use rstest::rstest;
#[cfg(feature = "python")]
use rust_decimal::Decimal;
use super::*;
#[rstest]
fn test_register_binance_custom_data_is_idempotent() {
register_binance_custom_data();
register_binance_custom_data();
}
#[cfg(feature = "python")]
#[rstest]
fn test_open_interest_hist_points_roundtrip_as_typed_python_list() {
pyo3::Python::initialize();
register_binance_custom_data();
Python::attach(|py| {
let instrument_id = InstrumentId::from("BTCUSDT-PERP.BINANCE");
let points = vec![
BinanceFuturesOpenInterestHistPoint::new(
Decimal::from_str_exact("100.0").unwrap(),
Decimal::from_str_exact("1000.0").unwrap(),
UnixNanos::from_millis(1_700_000_000_000),
),
BinanceFuturesOpenInterestHistPoint::new(
Decimal::from_str_exact("101.0").unwrap(),
Decimal::from_str_exact("1005.0").unwrap(),
UnixNanos::from_millis(1_700_000_300_000),
),
];
let payload = BinanceFuturesOpenInterestHist::new(
instrument_id,
"5m".to_string(),
points,
UnixNanos::from_millis(1_700_000_300_000),
UnixNanos::from(42_u64),
);
let mut metadata = Params::new();
metadata.insert(
"instrument_id".to_string(),
serde_json::Value::String("BTCUSDT-PERP.BINANCE".to_string()),
);
metadata.insert(
"period".to_string(),
serde_json::Value::String("5m".to_string()),
);
let custom = CustomData::new(
Arc::new(payload),
DataType::new(
"BinanceFuturesOpenInterestHist",
Some(metadata),
Some("BTCUSDT-PERP.BINANCE".to_string()),
),
);
let py_custom = Py::new(py, custom).unwrap();
let py_payload = py_custom.bind(py).getattr("data").unwrap();
let py_points = py_payload
.getattr("points")
.unwrap()
.cast_into::<PyList>()
.unwrap();
assert_eq!(py_points.len(), 2);
assert!(
py_points
.get_item(0)
.unwrap()
.is_instance_of::<BinanceFuturesOpenInterestHistPoint>()
);
let point0 = py_points
.get_item(0)
.unwrap()
.extract::<BinanceFuturesOpenInterestHistPoint>()
.unwrap();
let point1 = py_points
.get_item(1)
.unwrap()
.extract::<BinanceFuturesOpenInterestHistPoint>()
.unwrap();
assert_eq!(
point0.sum_open_interest,
Decimal::from_str_exact("100.0").unwrap()
);
assert_eq!(
point1.sum_open_interest_value,
Decimal::from_str_exact("1005.0").unwrap()
);
});
}
}