Skip to main content

nautilus_binance/common/
bar.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
16use 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/// Represents a Binance bar (kline/candlestick) with additional Binance-specific fields.
32///
33/// Extends the core `Bar` fields with `quote_volume`, `count`,
34/// `taker_buy_base_volume`, and `taker_buy_quote_volume`.
35#[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    /// The bar type for this bar.
46    pub bar_type: BarType,
47    /// The bars open price.
48    pub open: Price,
49    /// The bars high price.
50    pub high: Price,
51    /// The bars low price.
52    pub low: Price,
53    /// The bars close price.
54    pub close: Price,
55    /// The bars volume.
56    pub volume: Quantity,
57    /// The quote asset volume.
58    pub quote_volume: Decimal,
59    /// The number of trades.
60    pub count: u64,
61    /// Taker buy base asset volume.
62    pub taker_buy_base_volume: Decimal,
63    /// Taker buy quote asset volume.
64    pub taker_buy_quote_volume: Decimal,
65    /// UNIX timestamp (nanoseconds) when the data event occurred.
66    pub ts_event: UnixNanos,
67    /// UNIX timestamp (nanoseconds) when the data object was initialized.
68    pub ts_init: UnixNanos,
69}
70
71impl BinanceBar {
72    /// Creates a new [`BinanceBar`] instance.
73    #[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    /// Returns the metadata for the type, for use with serialization formats.
106    #[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    /// Returns the taker sell base asset volume.
118    #[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    /// Returns the taker sell quote asset volume.
125    #[must_use]
126    pub fn taker_sell_quote_volume(&self) -> Decimal {
127        self.quote_volume - self.taker_buy_quote_volume
128    }
129
130    /// Returns the core bar representation.
131    #[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        // Price/Quantity deserialize from borrowed &str, so we must go through
218        // a string representation rather than serde_json::from_value which
219        // produces owned strings.
220        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}