1use std::{collections::HashMap, sync::Arc};
17
18use anyhow::Context;
19use nautilus_core::{Params, UnixNanos};
20use nautilus_model::{
21 data::{
22 DataType, HasTsInit,
23 bar::{Bar, BarType},
24 custom::CustomDataTrait,
25 },
26 types::{Price, Quantity},
27};
28use rust_decimal::Decimal;
29use serde::{Deserialize, Serialize};
30
31#[cfg_attr(
36 feature = "python",
37 pyo3::pyclass(module = "nautilus_trader.adapters.binance", from_py_object)
38)]
39#[cfg_attr(
40 feature = "python",
41 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.binance")
42)]
43#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
44pub struct BinanceBar {
45 pub bar_type: BarType,
47 pub open: Price,
49 pub high: Price,
51 pub low: Price,
53 pub close: Price,
55 pub volume: Quantity,
57 pub quote_volume: Decimal,
59 pub count: u64,
61 pub taker_buy_base_volume: Decimal,
63 pub taker_buy_quote_volume: Decimal,
65 pub ts_event: UnixNanos,
67 pub ts_init: UnixNanos,
69}
70
71impl BinanceBar {
72 #[expect(clippy::too_many_arguments)]
74 #[must_use]
75 pub fn new(
76 bar_type: BarType,
77 open: Price,
78 high: Price,
79 low: Price,
80 close: Price,
81 volume: Quantity,
82 quote_volume: Decimal,
83 count: u64,
84 taker_buy_base_volume: Decimal,
85 taker_buy_quote_volume: Decimal,
86 ts_event: UnixNanos,
87 ts_init: UnixNanos,
88 ) -> Self {
89 Self {
90 bar_type,
91 open,
92 high,
93 low,
94 close,
95 volume,
96 quote_volume,
97 count,
98 taker_buy_base_volume,
99 taker_buy_quote_volume,
100 ts_event,
101 ts_init,
102 }
103 }
104
105 #[must_use]
107 pub fn get_metadata(bar_type: &BarType) -> HashMap<String, String> {
108 let mut metadata = HashMap::new();
109 metadata.insert("bar_type".to_string(), bar_type.to_string());
110 metadata.insert(
111 "instrument_id".to_string(),
112 bar_type.instrument_id().to_string(),
113 );
114 metadata
115 }
116
117 #[must_use]
119 pub fn taker_sell_base_volume(&self) -> Decimal {
120 Decimal::from(self.volume.raw) / Decimal::new(10i64.pow(self.volume.precision.into()), 0)
121 - self.taker_buy_base_volume
122 }
123
124 #[must_use]
126 pub fn taker_sell_quote_volume(&self) -> Decimal {
127 self.quote_volume - self.taker_buy_quote_volume
128 }
129
130 #[must_use]
132 pub fn bar(&self) -> Bar {
133 Bar::new(
134 self.bar_type,
135 self.open,
136 self.high,
137 self.low,
138 self.close,
139 self.volume,
140 self.ts_event,
141 self.ts_init,
142 )
143 }
144}
145
146impl HasTsInit for BinanceBar {
147 fn ts_init(&self) -> UnixNanos {
148 self.ts_init
149 }
150}
151
152pub(crate) fn binance_bar_data_type(bar_type: BarType) -> DataType {
153 let mut metadata = Params::new();
154 metadata.insert(
155 "bar_type".to_string(),
156 serde_json::Value::String(bar_type.to_string()),
157 );
158 metadata.insert(
159 "instrument_id".to_string(),
160 serde_json::Value::String(bar_type.instrument_id().to_string()),
161 );
162 DataType::new("BinanceBar", Some(metadata), Some(bar_type.to_string()))
163}
164
165pub(crate) fn parse_binance_bar_type(data_type: &DataType) -> anyhow::Result<BarType> {
166 let raw = data_type
167 .metadata()
168 .as_ref()
169 .and_then(|metadata| metadata.get("bar_type"))
170 .and_then(|value| value.as_str())
171 .map(str::trim)
172 .filter(|value| !value.is_empty())
173 .context("BinanceBar custom data requires `bar_type` metadata")?;
174 raw.parse()
175 .with_context(|| format!("invalid bar_type metadata `{raw}`"))
176}
177
178impl CustomDataTrait for BinanceBar {
179 fn type_name(&self) -> &'static str {
180 "BinanceBar"
181 }
182
183 fn as_any(&self) -> &dyn std::any::Any {
184 self
185 }
186
187 fn ts_event(&self) -> UnixNanos {
188 self.ts_event
189 }
190
191 fn to_json(&self) -> anyhow::Result<String> {
192 Ok(serde_json::to_string(self)?)
193 }
194
195 fn clone_arc(&self) -> Arc<dyn CustomDataTrait> {
196 Arc::new(self.clone())
197 }
198
199 fn eq_arc(&self, other: &dyn CustomDataTrait) -> bool {
200 if let Some(o) = other.as_any().downcast_ref::<Self>() {
201 self == o
202 } else {
203 false
204 }
205 }
206
207 #[cfg(feature = "python")]
208 fn to_pyobject(&self, py: pyo3::Python<'_>) -> pyo3::PyResult<pyo3::Py<pyo3::PyAny>> {
209 nautilus_model::data::custom::clone_pyclass_to_pyobject(self, py)
210 }
211
212 fn type_name_static() -> &'static str {
213 "BinanceBar"
214 }
215
216 fn from_json(value: serde_json::Value) -> anyhow::Result<Arc<dyn CustomDataTrait>> {
217 let json_str = serde_json::to_string(&value)?;
221 let parsed: Self = serde_json::from_str(&json_str)?;
222 Ok(Arc::new(parsed))
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use rstest::rstest;
229 use rust_decimal_macros::dec;
230
231 use super::*;
232
233 fn stub_binance_bar() -> BinanceBar {
234 BinanceBar::new(
235 BarType::from("BTCUSDT.BINANCE-1-MINUTE-LAST-EXTERNAL"),
236 Price::from("0.01634790"),
237 Price::from("0.01640000"),
238 Price::from("0.01575800"),
239 Price::from("0.01577100"),
240 Quantity::from("148976.11427815"),
241 dec!(2434.19055334),
242 100,
243 dec!(1756.87402397),
244 dec!(28.46694368),
245 UnixNanos::from(1_650_000_000_000_000_000u64),
246 UnixNanos::from(1_650_000_000_000_000_000u64),
247 )
248 }
249
250 #[rstest]
251 fn test_type_name() {
252 let bar = stub_binance_bar();
253 assert_eq!(bar.type_name(), "BinanceBar");
254 assert_eq!(BinanceBar::type_name_static(), "BinanceBar");
255 }
256
257 #[rstest]
258 fn test_taker_sell_quote_volume() {
259 let bar = stub_binance_bar();
260 assert_eq!(bar.taker_sell_quote_volume(), dec!(2405.72360966));
261 }
262
263 #[rstest]
264 fn test_json_round_trip() {
265 let bar = stub_binance_bar();
266 let json = bar.to_json().unwrap();
267 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
268 let restored = BinanceBar::from_json(value).unwrap();
269 let restored_bar = restored.as_any().downcast_ref::<BinanceBar>().unwrap();
270 assert_eq!(restored_bar, &bar);
271 }
272}