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 nautilus_core::UnixNanos;
19use nautilus_model::{
20    data::{HasTsInit, bar::BarType, custom::CustomDataTrait},
21    types::{Price, Quantity},
22};
23use rust_decimal::Decimal;
24use serde::{Deserialize, Serialize};
25
26/// Represents a Binance bar (kline/candlestick) with additional Binance-specific fields.
27///
28/// Extends the core `Bar` fields with `quote_volume`, `count`,
29/// `taker_buy_base_volume`, and `taker_buy_quote_volume`.
30#[cfg_attr(
31    feature = "python",
32    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.binance", from_py_object)
33)]
34#[cfg_attr(
35    feature = "python",
36    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.binance")
37)]
38#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
39pub struct BinanceBar {
40    /// The bar type for this bar.
41    pub bar_type: BarType,
42    /// The bars open price.
43    pub open: Price,
44    /// The bars high price.
45    pub high: Price,
46    /// The bars low price.
47    pub low: Price,
48    /// The bars close price.
49    pub close: Price,
50    /// The bars volume.
51    pub volume: Quantity,
52    /// The quote asset volume.
53    pub quote_volume: Decimal,
54    /// The number of trades.
55    pub count: u64,
56    /// Taker buy base asset volume.
57    pub taker_buy_base_volume: Decimal,
58    /// Taker buy quote asset volume.
59    pub taker_buy_quote_volume: Decimal,
60    /// UNIX timestamp (nanoseconds) when the data event occurred.
61    pub ts_event: UnixNanos,
62    /// UNIX timestamp (nanoseconds) when the data object was initialized.
63    pub ts_init: UnixNanos,
64}
65
66impl BinanceBar {
67    /// Creates a new [`BinanceBar`] instance.
68    #[expect(clippy::too_many_arguments)]
69    #[must_use]
70    pub fn new(
71        bar_type: BarType,
72        open: Price,
73        high: Price,
74        low: Price,
75        close: Price,
76        volume: Quantity,
77        quote_volume: Decimal,
78        count: u64,
79        taker_buy_base_volume: Decimal,
80        taker_buy_quote_volume: Decimal,
81        ts_event: UnixNanos,
82        ts_init: UnixNanos,
83    ) -> Self {
84        Self {
85            bar_type,
86            open,
87            high,
88            low,
89            close,
90            volume,
91            quote_volume,
92            count,
93            taker_buy_base_volume,
94            taker_buy_quote_volume,
95            ts_event,
96            ts_init,
97        }
98    }
99
100    /// Returns the metadata for the type, for use with serialization formats.
101    #[must_use]
102    pub fn get_metadata(bar_type: &BarType) -> HashMap<String, String> {
103        let mut metadata = HashMap::new();
104        metadata.insert("bar_type".to_string(), bar_type.to_string());
105        metadata.insert(
106            "instrument_id".to_string(),
107            bar_type.instrument_id().to_string(),
108        );
109        metadata
110    }
111
112    /// Returns the taker sell base asset volume.
113    #[must_use]
114    pub fn taker_sell_base_volume(&self) -> Decimal {
115        Decimal::from(self.volume.raw) / Decimal::new(10i64.pow(self.volume.precision.into()), 0)
116            - self.taker_buy_base_volume
117    }
118
119    /// Returns the taker sell quote asset volume.
120    #[must_use]
121    pub fn taker_sell_quote_volume(&self) -> Decimal {
122        self.quote_volume - self.taker_buy_quote_volume
123    }
124}
125
126impl HasTsInit for BinanceBar {
127    fn ts_init(&self) -> UnixNanos {
128        self.ts_init
129    }
130}
131
132impl CustomDataTrait for BinanceBar {
133    fn type_name(&self) -> &'static str {
134        "BinanceBar"
135    }
136
137    fn as_any(&self) -> &dyn std::any::Any {
138        self
139    }
140
141    fn ts_event(&self) -> UnixNanos {
142        self.ts_event
143    }
144
145    fn to_json(&self) -> anyhow::Result<String> {
146        Ok(serde_json::to_string(self)?)
147    }
148
149    fn clone_arc(&self) -> Arc<dyn CustomDataTrait> {
150        Arc::new(self.clone())
151    }
152
153    fn eq_arc(&self, other: &dyn CustomDataTrait) -> bool {
154        if let Some(o) = other.as_any().downcast_ref::<Self>() {
155            self == o
156        } else {
157            false
158        }
159    }
160
161    #[cfg(feature = "python")]
162    fn to_pyobject(&self, py: pyo3::Python<'_>) -> pyo3::PyResult<pyo3::Py<pyo3::PyAny>> {
163        nautilus_model::data::custom::clone_pyclass_to_pyobject(self, py)
164    }
165
166    fn type_name_static() -> &'static str {
167        "BinanceBar"
168    }
169
170    fn from_json(value: serde_json::Value) -> anyhow::Result<Arc<dyn CustomDataTrait>> {
171        // Price/Quantity deserialize from borrowed &str, so we must go through
172        // a string representation rather than serde_json::from_value which
173        // produces owned strings.
174        let json_str = serde_json::to_string(&value)?;
175        let parsed: Self = serde_json::from_str(&json_str)?;
176        Ok(Arc::new(parsed))
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use rstest::rstest;
183    use rust_decimal_macros::dec;
184
185    use super::*;
186
187    fn stub_binance_bar() -> BinanceBar {
188        BinanceBar::new(
189            BarType::from("BTCUSDT.BINANCE-1-MINUTE-LAST-EXTERNAL"),
190            Price::from("0.01634790"),
191            Price::from("0.01640000"),
192            Price::from("0.01575800"),
193            Price::from("0.01577100"),
194            Quantity::from("148976.11427815"),
195            dec!(2434.19055334),
196            100,
197            dec!(1756.87402397),
198            dec!(28.46694368),
199            UnixNanos::from(1_650_000_000_000_000_000u64),
200            UnixNanos::from(1_650_000_000_000_000_000u64),
201        )
202    }
203
204    #[rstest]
205    fn test_type_name() {
206        let bar = stub_binance_bar();
207        assert_eq!(bar.type_name(), "BinanceBar");
208        assert_eq!(BinanceBar::type_name_static(), "BinanceBar");
209    }
210
211    #[rstest]
212    fn test_taker_sell_quote_volume() {
213        let bar = stub_binance_bar();
214        assert_eq!(bar.taker_sell_quote_volume(), dec!(2405.72360966));
215    }
216
217    #[rstest]
218    fn test_json_round_trip() {
219        let bar = stub_binance_bar();
220        let json = bar.to_json().unwrap();
221        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
222        let restored = BinanceBar::from_json(value).unwrap();
223        let restored_bar = restored.as_any().downcast_ref::<BinanceBar>().unwrap();
224        assert_eq!(restored_bar, &bar);
225    }
226}