use serde::Deserialize;
use crate::actors::{CandleData, FundingData};
use crate::error::Result;
use crate::http::PublicRestClient;
const SPOT_BASE_URL: &str = "https://api.binance.com";
const FUTURES_BASE_URL: &str = "https://fapi.binance.com";
const EXCHANGE_NAME: &str = "binance";
pub(super) mod str_f64 {
use serde::{Deserialize, Deserializer, Serializer};
pub(super) fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<f64, D::Error> {
#[derive(Deserialize)]
#[serde(untagged)]
enum StringOrFloat {
S(String),
F(f64),
}
match StringOrFloat::deserialize(d)? {
StringOrFloat::S(s) => s.parse().map_err(serde::de::Error::custom),
StringOrFloat::F(f) => Ok(f),
}
}
#[allow(dead_code, clippy::trivially_copy_pass_by_ref)]
pub(super) fn serialize<S: Serializer>(v: &f64, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&v.to_string())
}
}
fn limit_str(n: u32) -> String {
n.to_string()
}
fn now_ms() -> i64 {
chrono::Utc::now().timestamp_millis()
}
#[derive(Debug, Clone)]
pub struct BinanceKline {
pub open_time: i64,
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
pub volume: f64,
pub close_time: i64,
pub quote_volume: f64,
pub trades: u64,
pub taker_buy_base_volume: f64,
pub taker_buy_quote_volume: f64,
}
impl<'de> Deserialize<'de> for BinanceKline {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
type Raw = (
i64,
String,
String,
String,
String,
String,
i64,
String,
u64,
String,
String,
String,
);
let (
open_time,
open_s,
high_s,
low_s,
close_s,
vol_s,
close_time,
quote_vol_s,
trades,
taker_buy_base_s,
taker_buy_quote_s,
_ignore,
) = Raw::deserialize(d)?;
let parse = |s: String| s.parse::<f64>().map_err(serde::de::Error::custom);
Ok(Self {
open_time,
open: parse(open_s)?,
high: parse(high_s)?,
low: parse(low_s)?,
close: parse(close_s)?,
volume: parse(vol_s)?,
close_time,
quote_volume: parse(quote_vol_s)?,
trades,
taker_buy_base_volume: parse(taker_buy_base_s)?,
taker_buy_quote_volume: parse(taker_buy_quote_s)?,
})
}
}
impl BinanceKline {
#[must_use]
pub fn into_candle_data(
self,
symbol: impl Into<String>,
interval: impl Into<String>,
) -> CandleData {
CandleData {
symbol: symbol.into(),
exchange: EXCHANGE_NAME.into(),
interval: interval.into(),
open_ts: self.open_time,
open: self.open,
high: self.high,
low: self.low,
close: self.close,
volume: self.volume,
is_closed: true,
receipt_ts: now_ms(),
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct BinanceOrderBook {
#[serde(rename = "lastUpdateId")]
pub last_update_id: i64,
pub bids: Vec<[String; 2]>,
pub asks: Vec<[String; 2]>,
}
impl BinanceOrderBook {
#[must_use]
pub fn bids_f64(&self) -> Vec<[f64; 2]> {
Self::parse_levels(&self.bids)
}
#[must_use]
pub fn asks_f64(&self) -> Vec<[f64; 2]> {
Self::parse_levels(&self.asks)
}
fn parse_levels(rows: &[[String; 2]]) -> Vec<[f64; 2]> {
rows.iter()
.filter_map(|[p, q]| Some([p.parse().ok()?, q.parse().ok()?]))
.collect()
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BinanceTrade {
pub id: u64,
#[serde(with = "str_f64")]
pub price: f64,
#[serde(with = "str_f64")]
pub qty: f64,
#[serde(with = "str_f64")]
pub quote_qty: f64,
pub time: i64,
pub is_buyer_maker: bool,
pub is_best_match: bool,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BinanceBookTicker {
pub symbol: String,
#[serde(with = "str_f64")]
pub bid_price: f64,
#[serde(with = "str_f64")]
pub bid_qty: f64,
#[serde(with = "str_f64")]
pub ask_price: f64,
#[serde(with = "str_f64")]
pub ask_qty: f64,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BinanceTicker24h {
pub symbol: String,
#[serde(with = "str_f64")]
pub price_change: f64,
#[serde(with = "str_f64")]
pub price_change_percent: f64,
#[serde(with = "str_f64")]
pub weighted_avg_price: f64,
#[serde(with = "str_f64")]
pub last_price: f64,
#[serde(with = "str_f64")]
pub last_qty: f64,
#[serde(with = "str_f64")]
pub bid_price: f64,
#[serde(with = "str_f64")]
pub ask_price: f64,
#[serde(with = "str_f64")]
pub open_price: f64,
#[serde(with = "str_f64")]
pub high_price: f64,
#[serde(with = "str_f64")]
pub low_price: f64,
#[serde(with = "str_f64")]
pub volume: f64,
#[serde(with = "str_f64")]
pub quote_volume: f64,
pub open_time: i64,
pub close_time: i64,
pub count: u64,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BinanceFundingRate {
pub symbol: String,
#[serde(with = "str_f64")]
pub funding_rate: f64,
pub funding_time: i64,
#[serde(default, with = "opt_str_f64")]
pub mark_price: Option<f64>,
}
pub(super) mod opt_str_f64 {
use serde::{Deserialize, Deserializer, Serializer};
pub(super) fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Option<f64>, D::Error> {
#[derive(Deserialize)]
#[serde(untagged)]
enum Wrapper {
None,
S(String),
F(f64),
}
match Option::<Wrapper>::deserialize(d)? {
None | Some(Wrapper::None) => Ok(None),
Some(Wrapper::S(s)) if s.is_empty() => Ok(None),
Some(Wrapper::S(s)) => s.parse().map(Some).map_err(serde::de::Error::custom),
Some(Wrapper::F(f)) => Ok(Some(f)),
}
}
#[allow(dead_code, clippy::ref_option)]
pub(super) fn serialize<S: Serializer>(v: &Option<f64>, s: S) -> Result<S::Ok, S::Error> {
match v {
Some(f) => s.serialize_str(&f.to_string()),
None => s.serialize_none(),
}
}
}
impl BinanceFundingRate {
#[must_use]
pub fn into_funding_data(self) -> FundingData {
FundingData {
symbol: self.symbol,
exchange: EXCHANGE_NAME.into(),
funding_rate: self.funding_rate,
next_funding_time: self.funding_time,
mark_price: self.mark_price,
index_price: None,
exchange_ts: self.funding_time,
receipt_ts: now_ms(),
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BinanceMarkPrice {
pub symbol: String,
#[serde(with = "str_f64")]
pub mark_price: f64,
#[serde(with = "str_f64")]
pub index_price: f64,
#[serde(default, with = "opt_str_f64")]
pub estimated_settle_price: Option<f64>,
#[serde(with = "str_f64")]
pub last_funding_rate: f64,
#[serde(default, with = "opt_str_f64")]
pub interest_rate: Option<f64>,
pub next_funding_time: i64,
pub time: i64,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BinanceOpenInterest {
pub symbol: String,
#[serde(with = "str_f64")]
pub open_interest: f64,
pub time: i64,
}
#[derive(Clone)]
pub struct BinanceRestClient {
spot: PublicRestClient,
futures: PublicRestClient,
}
impl BinanceRestClient {
pub fn new() -> Result<Self> {
Self::with_base_urls(SPOT_BASE_URL, FUTURES_BASE_URL)
}
pub fn with_base_urls(
spot_url: impl Into<String>,
futures_url: impl Into<String>,
) -> Result<Self> {
Ok(Self {
spot: PublicRestClient::new(spot_url)?,
futures: PublicRestClient::new(futures_url)?,
})
}
pub async fn get_klines(
&self,
symbol: &str,
interval: &str,
limit: u32,
) -> Result<Vec<BinanceKline>> {
let limit = limit_str(limit);
self.spot
.get(
"/api/v3/klines",
&[
("symbol", symbol),
("interval", interval),
("limit", &limit),
],
)
.await
}
pub async fn get_orderbook(&self, symbol: &str, limit: u32) -> Result<BinanceOrderBook> {
let limit = limit_str(limit);
self.spot
.get("/api/v3/depth", &[("symbol", symbol), ("limit", &limit)])
.await
}
pub async fn get_recent_trades(&self, symbol: &str, limit: u32) -> Result<Vec<BinanceTrade>> {
let limit = limit_str(limit);
self.spot
.get("/api/v3/trades", &[("symbol", symbol), ("limit", &limit)])
.await
}
pub async fn get_ticker(&self, symbol: &str) -> Result<BinanceBookTicker> {
self.spot
.get("/api/v3/ticker/bookTicker", &[("symbol", symbol)])
.await
}
pub async fn get_ticker_24h(&self, symbol: &str) -> Result<BinanceTicker24h> {
self.spot
.get("/api/v3/ticker/24hr", &[("symbol", symbol)])
.await
}
pub async fn get_exchange_info(&self) -> Result<serde_json::Value> {
self.spot.get("/api/v3/exchangeInfo", &[]).await
}
pub async fn get_futures_klines(
&self,
symbol: &str,
interval: &str,
limit: u32,
) -> Result<Vec<BinanceKline>> {
let limit = limit_str(limit);
self.futures
.get(
"/fapi/v1/klines",
&[
("symbol", symbol),
("interval", interval),
("limit", &limit),
],
)
.await
}
pub async fn get_futures_funding_rate(
&self,
symbol: &str,
limit: u32,
) -> Result<Vec<BinanceFundingRate>> {
let limit = limit_str(limit);
self.futures
.get(
"/fapi/v1/fundingRate",
&[("symbol", symbol), ("limit", &limit)],
)
.await
}
pub async fn get_futures_mark_price(&self, symbol: &str) -> Result<BinanceMarkPrice> {
self.futures
.get("/fapi/v1/premiumIndex", &[("symbol", symbol)])
.await
}
pub async fn get_futures_open_interest(&self, symbol: &str) -> Result<BinanceOpenInterest> {
self.futures
.get("/fapi/v1/openInterest", &[("symbol", symbol)])
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kline_deserializes_from_binance_array_shape() {
let raw = r#"[
1499040000000,
"0.01634790",
"0.80000000",
"0.01575800",
"0.01577100",
"148976.11427815",
1499644799999,
"2434.19055334",
308,
"1756.87402397",
"28.46694368",
"0"
]"#;
let k: BinanceKline = serde_json::from_str(raw).expect("kline deserialize");
assert_eq!(k.open_time, 1_499_040_000_000);
assert!((k.open - 0.016_347_9).abs() < 1e-9);
assert!((k.high - 0.8).abs() < 1e-9);
assert!((k.low - 0.015_758).abs() < 1e-9);
assert!((k.close - 0.015_771).abs() < 1e-9);
assert_eq!(k.trades, 308);
}
#[test]
fn kline_into_candle_data_marks_rest_bars_closed() {
let k = BinanceKline {
open_time: 100,
open: 1.0,
high: 2.0,
low: 0.5,
close: 1.5,
volume: 10.0,
close_time: 200,
quote_volume: 15.0,
trades: 5,
taker_buy_base_volume: 6.0,
taker_buy_quote_volume: 9.0,
};
let c = k.into_candle_data("BTCUSDT", "1m");
assert_eq!(c.symbol, "BTCUSDT");
assert_eq!(c.exchange, "binance");
assert_eq!(c.interval, "1m");
assert!(c.is_closed, "REST klines are always finalised");
assert!((c.close - 1.5).abs() < 1e-9);
}
#[test]
fn orderbook_parses_string_levels_to_f64() {
let raw = r#"{
"lastUpdateId": 42,
"bids": [["100.5", "1.0"], ["100.4", "2.5"]],
"asks": [["100.6", "0.5"], ["100.7", "3.0"]]
}"#;
let book: BinanceOrderBook = serde_json::from_str(raw).expect("orderbook deserialize");
assert_eq!(book.last_update_id, 42);
let bids = book.bids_f64();
assert_eq!(bids.len(), 2);
assert!((bids[0][0] - 100.5).abs() < 1e-9);
let asks = book.asks_f64();
assert!((asks[1][1] - 3.0).abs() < 1e-9);
}
#[test]
fn book_ticker_deserialises_string_fields() {
let raw = r#"{
"symbol": "BTCUSDT",
"bidPrice": "96000.50",
"bidQty": "0.5",
"askPrice": "96001.00",
"askQty": "1.0"
}"#;
let t: BinanceBookTicker = serde_json::from_str(raw).expect("ticker deserialize");
assert_eq!(t.symbol, "BTCUSDT");
assert!((t.bid_price - 96_000.5).abs() < 1e-9);
assert!((t.ask_qty - 1.0).abs() < 1e-9);
}
#[test]
fn funding_rate_into_funding_data() {
let r = BinanceFundingRate {
symbol: "BTCUSDT".into(),
funding_rate: 0.000_1,
funding_time: 1_700_028_800_000,
mark_price: Some(96_010.0),
};
let f = r.into_funding_data();
assert_eq!(f.symbol, "BTCUSDT");
assert_eq!(f.exchange, "binance");
assert_eq!(f.next_funding_time, 1_700_028_800_000);
assert_eq!(f.mark_price, Some(96_010.0));
}
}