Skip to content

Commit d69d091

Browse files
committed
Continue Binance adapter in Rust
1 parent 79e7094 commit d69d091

File tree

10 files changed

+1071
-8
lines changed

10 files changed

+1071
-8
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/adapters/binance/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ pyo3-async-runtimes = { workspace = true, optional = true }
9191
nautilus-testkit = { workspace = true }
9292

9393
criterion = { workspace = true }
94+
dotenvy = { workspace = true }
9495
rstest = { workspace = true }
9596
rust_decimal_macros = { workspace = true }
9697
url = { workspace = true }
@@ -105,3 +106,8 @@ doc = false
105106
name = "binance-spot-ws-data"
106107
path = "bin/ws_spot_data.rs"
107108
doc = false
109+
110+
[[example]]
111+
name = "binance-data-tester"
112+
path = "examples/node_data_tester.rs"
113+
doc = false
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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+
16+
//! Example demonstrating live data testing with the Binance Spot SBE adapter.
17+
//!
18+
//! Run with: `cargo run --example binance-data-tester --package nautilus-binance`
19+
20+
use std::num::NonZeroUsize;
21+
22+
use nautilus_binance::{
23+
common::enums::{BinanceEnvironment, BinanceProductType},
24+
config::BinanceDataClientConfig,
25+
factories::BinanceDataClientFactory,
26+
};
27+
use nautilus_common::enums::Environment;
28+
use nautilus_live::node::LiveNode;
29+
use nautilus_model::{
30+
identifiers::{ClientId, InstrumentId, TraderId},
31+
stubs::TestDefault,
32+
};
33+
use nautilus_testkit::testers::{DataTester, DataTesterConfig};
34+
35+
#[tokio::main]
36+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
37+
dotenvy::dotenv().ok();
38+
39+
let environment = Environment::Live;
40+
let trader_id = TraderId::test_default();
41+
let node_name = "BINANCE-TESTER-001".to_string();
42+
let instrument_ids = vec![
43+
InstrumentId::from("BTCUSDT.BINANCE"),
44+
// InstrumentId::from("ETHUSDT.BINANCE"),
45+
];
46+
47+
// SBE streams require Ed25519 authentication (not HMAC)
48+
// Generate Ed25519 keys in your Binance account API settings
49+
let binance_config = BinanceDataClientConfig {
50+
product_types: vec![BinanceProductType::Spot],
51+
environment: BinanceEnvironment::Mainnet,
52+
api_key: None, // HMAC key for HTTP API (optional)
53+
api_secret: None, // HMAC secret for HTTP API (optional)
54+
ed25519_api_key: std::env::var("BINANCE_ED25519_API_KEY").ok(),
55+
ed25519_api_secret: std::env::var("BINANCE_ED25519_API_SECRET").ok(),
56+
..Default::default()
57+
};
58+
59+
let client_factory = BinanceDataClientFactory::new();
60+
let client_id = ClientId::new("BINANCE");
61+
62+
let mut node = LiveNode::builder(trader_id, environment)?
63+
.with_name(node_name)
64+
.with_delay_post_stop_secs(2)
65+
.add_data_client(None, Box::new(client_factory), Box::new(binance_config))?
66+
.build()?;
67+
68+
let tester_config = DataTesterConfig::new(client_id, instrument_ids)
69+
.with_subscribe_book_at_interval(true)
70+
.with_book_interval_ms(NonZeroUsize::new(10).unwrap());
71+
let tester = DataTester::new(tester_config);
72+
73+
node.add_actor(tester)?;
74+
node.run().await?;
75+
76+
Ok(())
77+
}

crates/adapters/binance/src/common/enums.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,31 @@ pub enum BinanceKlineInterval {
563563
Month1,
564564
}
565565

566+
impl BinanceKlineInterval {
567+
/// Returns the string representation used by Binance API.
568+
#[must_use]
569+
pub const fn as_str(&self) -> &'static str {
570+
match self {
571+
Self::Second1 => "1s",
572+
Self::Minute1 => "1m",
573+
Self::Minute3 => "3m",
574+
Self::Minute5 => "5m",
575+
Self::Minute15 => "15m",
576+
Self::Minute30 => "30m",
577+
Self::Hour1 => "1h",
578+
Self::Hour2 => "2h",
579+
Self::Hour4 => "4h",
580+
Self::Hour6 => "6h",
581+
Self::Hour8 => "8h",
582+
Self::Hour12 => "12h",
583+
Self::Day1 => "1d",
584+
Self::Day3 => "3d",
585+
Self::Week1 => "1w",
586+
Self::Month1 => "1M",
587+
}
588+
}
589+
}
590+
566591
#[cfg(test)]
567592
mod tests {
568593
use rstest::rstest;

crates/adapters/binance/src/common/parse.rs

Lines changed: 139 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ use std::str::FromStr;
2323
use anyhow::Context;
2424
use nautilus_core::nanos::UnixNanos;
2525
use nautilus_model::{
26-
data::{Bar, BarType, TradeTick},
26+
data::{Bar, BarSpecification, BarType, TradeTick},
2727
enums::{
28-
AggressorSide, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, TriggerType,
28+
AggressorSide, BarAggregation, LiquiditySide, OrderSide, OrderStatus, OrderType,
29+
TimeInForce, TriggerType,
2930
},
3031
identifiers::{
3132
AccountId, ClientOrderId, InstrumentId, OrderListId, Symbol, TradeId, Venue, VenueOrderId,
@@ -42,7 +43,7 @@ use serde_json::Value;
4243

4344
use crate::{
4445
common::{
45-
enums::BinanceContractStatus,
46+
enums::{BinanceContractStatus, BinanceKlineInterval},
4647
fixed::{mantissa_to_price, mantissa_to_quantity},
4748
sbe::spot::{
4849
order_side::OrderSide as SbeOrderSide, order_status::OrderStatus as SbeOrderStatus,
@@ -875,6 +876,56 @@ pub fn parse_klines_to_bars(
875876
Ok(bars)
876877
}
877878

879+
/// Converts a Nautilus bar specification to a Binance kline interval.
880+
///
881+
/// # Errors
882+
///
883+
/// Returns an error if the bar specification does not map to a supported
884+
/// Binance kline interval.
885+
pub fn bar_spec_to_binance_interval(
886+
bar_spec: BarSpecification,
887+
) -> anyhow::Result<BinanceKlineInterval> {
888+
let step = bar_spec.step.get();
889+
let interval = match bar_spec.aggregation {
890+
BarAggregation::Second => {
891+
anyhow::bail!("Binance Spot does not support second-level kline intervals")
892+
}
893+
BarAggregation::Minute => match step {
894+
1 => BinanceKlineInterval::Minute1,
895+
3 => BinanceKlineInterval::Minute3,
896+
5 => BinanceKlineInterval::Minute5,
897+
15 => BinanceKlineInterval::Minute15,
898+
30 => BinanceKlineInterval::Minute30,
899+
_ => anyhow::bail!("Unsupported minute interval: {step}m"),
900+
},
901+
BarAggregation::Hour => match step {
902+
1 => BinanceKlineInterval::Hour1,
903+
2 => BinanceKlineInterval::Hour2,
904+
4 => BinanceKlineInterval::Hour4,
905+
6 => BinanceKlineInterval::Hour6,
906+
8 => BinanceKlineInterval::Hour8,
907+
12 => BinanceKlineInterval::Hour12,
908+
_ => anyhow::bail!("Unsupported hour interval: {step}h"),
909+
},
910+
BarAggregation::Day => match step {
911+
1 => BinanceKlineInterval::Day1,
912+
3 => BinanceKlineInterval::Day3,
913+
_ => anyhow::bail!("Unsupported day interval: {step}d"),
914+
},
915+
BarAggregation::Week => match step {
916+
1 => BinanceKlineInterval::Week1,
917+
_ => anyhow::bail!("Unsupported week interval: {step}w"),
918+
},
919+
BarAggregation::Month => match step {
920+
1 => BinanceKlineInterval::Month1,
921+
_ => anyhow::bail!("Unsupported month interval: {step}M"),
922+
},
923+
agg => anyhow::bail!("Unsupported bar aggregation for Binance: {agg:?}"),
924+
};
925+
926+
Ok(interval)
927+
}
928+
878929
#[cfg(test)]
879930
mod tests {
880931
use rstest::rstest;
@@ -986,4 +1037,89 @@ mod tests {
9861037
.contains("Missing PRICE_FILTER")
9871038
);
9881039
}
1040+
1041+
mod bar_spec_tests {
1042+
use std::num::NonZeroUsize;
1043+
1044+
use nautilus_model::{
1045+
data::BarSpecification,
1046+
enums::{BarAggregation, PriceType},
1047+
};
1048+
1049+
use super::*;
1050+
use crate::common::enums::BinanceKlineInterval;
1051+
1052+
fn make_bar_spec(step: usize, aggregation: BarAggregation) -> BarSpecification {
1053+
BarSpecification {
1054+
step: NonZeroUsize::new(step).unwrap(),
1055+
aggregation,
1056+
price_type: PriceType::Last,
1057+
}
1058+
}
1059+
1060+
#[rstest]
1061+
#[case(1, BarAggregation::Minute, BinanceKlineInterval::Minute1)]
1062+
#[case(3, BarAggregation::Minute, BinanceKlineInterval::Minute3)]
1063+
#[case(5, BarAggregation::Minute, BinanceKlineInterval::Minute5)]
1064+
#[case(15, BarAggregation::Minute, BinanceKlineInterval::Minute15)]
1065+
#[case(30, BarAggregation::Minute, BinanceKlineInterval::Minute30)]
1066+
#[case(1, BarAggregation::Hour, BinanceKlineInterval::Hour1)]
1067+
#[case(2, BarAggregation::Hour, BinanceKlineInterval::Hour2)]
1068+
#[case(4, BarAggregation::Hour, BinanceKlineInterval::Hour4)]
1069+
#[case(6, BarAggregation::Hour, BinanceKlineInterval::Hour6)]
1070+
#[case(8, BarAggregation::Hour, BinanceKlineInterval::Hour8)]
1071+
#[case(12, BarAggregation::Hour, BinanceKlineInterval::Hour12)]
1072+
#[case(1, BarAggregation::Day, BinanceKlineInterval::Day1)]
1073+
#[case(3, BarAggregation::Day, BinanceKlineInterval::Day3)]
1074+
#[case(1, BarAggregation::Week, BinanceKlineInterval::Week1)]
1075+
#[case(1, BarAggregation::Month, BinanceKlineInterval::Month1)]
1076+
fn test_bar_spec_to_binance_interval(
1077+
#[case] step: usize,
1078+
#[case] aggregation: BarAggregation,
1079+
#[case] expected: BinanceKlineInterval,
1080+
) {
1081+
let bar_spec = make_bar_spec(step, aggregation);
1082+
let result = bar_spec_to_binance_interval(bar_spec).unwrap();
1083+
assert_eq!(result, expected);
1084+
}
1085+
1086+
#[rstest]
1087+
fn test_unsupported_second_interval() {
1088+
let bar_spec = make_bar_spec(1, BarAggregation::Second);
1089+
let result = bar_spec_to_binance_interval(bar_spec);
1090+
assert!(result.is_err());
1091+
assert!(
1092+
result
1093+
.unwrap_err()
1094+
.to_string()
1095+
.contains("does not support second-level")
1096+
);
1097+
}
1098+
1099+
#[rstest]
1100+
fn test_unsupported_minute_interval() {
1101+
let bar_spec = make_bar_spec(7, BarAggregation::Minute);
1102+
let result = bar_spec_to_binance_interval(bar_spec);
1103+
assert!(result.is_err());
1104+
assert!(
1105+
result
1106+
.unwrap_err()
1107+
.to_string()
1108+
.contains("Unsupported minute interval")
1109+
);
1110+
}
1111+
1112+
#[rstest]
1113+
fn test_unsupported_aggregation() {
1114+
let bar_spec = make_bar_spec(100, BarAggregation::Tick);
1115+
let result = bar_spec_to_binance_interval(bar_spec);
1116+
assert!(result.is_err());
1117+
assert!(
1118+
result
1119+
.unwrap_err()
1120+
.to_string()
1121+
.contains("Unsupported bar aggregation")
1122+
);
1123+
}
1124+
}
9891125
}

crates/adapters/binance/src/config.rs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515

1616
//! Binance adapter configuration structures.
1717
18+
use std::any::Any;
19+
20+
use nautilus_system::factories::ClientConfig;
21+
1822
use crate::common::enums::{BinanceEnvironment, BinanceProductType};
1923

2024
/// Configuration for Binance data client.
@@ -28,10 +32,14 @@ pub struct BinanceDataClientConfig {
2832
pub base_url_http: Option<String>,
2933
/// Optional base URL override for WebSocket.
3034
pub base_url_ws: Option<String>,
31-
/// API key for authenticated endpoints.
35+
/// API key for HTTP authenticated endpoints (HMAC).
3236
pub api_key: Option<String>,
33-
/// API secret for request signing.
37+
/// API secret for HTTP request signing (HMAC).
3438
pub api_secret: Option<String>,
39+
/// Ed25519 API key for SBE WebSocket streams (required for SBE).
40+
pub ed25519_api_key: Option<String>,
41+
/// Ed25519 private key (base64) for SBE WebSocket streams (required for SBE).
42+
pub ed25519_api_secret: Option<String>,
3543
}
3644

3745
impl Default for BinanceDataClientConfig {
@@ -43,10 +51,18 @@ impl Default for BinanceDataClientConfig {
4351
base_url_ws: None,
4452
api_key: None,
4553
api_secret: None,
54+
ed25519_api_key: None,
55+
ed25519_api_secret: None,
4656
}
4757
}
4858
}
4959

60+
impl ClientConfig for BinanceDataClientConfig {
61+
fn as_any(&self) -> &dyn Any {
62+
self
63+
}
64+
}
65+
5066
/// Configuration for Binance execution client.
5167
#[derive(Clone, Debug)]
5268
pub struct BinanceExecClientConfig {
@@ -63,3 +79,9 @@ pub struct BinanceExecClientConfig {
6379
/// API secret for request signing (required).
6480
pub api_secret: String,
6581
}
82+
83+
impl ClientConfig for BinanceExecClientConfig {
84+
fn as_any(&self) -> &dyn Any {
85+
self
86+
}
87+
}

0 commit comments

Comments
 (0)