binance/spot/
api.rs

1use rust_decimal::Decimal;
2use serde::{Deserialize, Serialize};
3
4use crate::spot::{
5    ExchangeFilter, KlineInterval, OrderType, RateLimitInterval, RateLimiter, STPMode, SymbolStatus,
6};
7
8pub type Timestamp = u64;
9
10#[derive(Debug, PartialEq)]
11pub struct Response<T> {
12    pub result: T,
13    pub headers: Headers,
14}
15
16#[derive(Debug, PartialEq)]
17pub struct Headers {
18    pub retry_after: Option<Timestamp>,
19}
20
21#[derive(Debug, Deserialize, PartialEq)]
22pub struct TestConnectivity {}
23
24#[derive(Debug, Deserialize, PartialEq)]
25#[serde(rename_all = "camelCase")]
26pub struct ServerTime {
27    server_time: Timestamp,
28}
29
30#[derive(Debug, Serialize, PartialEq)]
31#[serde(rename_all = "camelCase")]
32pub struct GetExchangeInfoParams {
33    /// Example: curl -X GET "https://api.binance.com/api/v3/exchangeInfo?symbol=BNBBTC"
34    pub symbol: Option<String>,
35    /// Examples: curl -X GET "https://api.binance.com/api/v3/exchangeInfo?symbols=%5B%22BNBBTC%22,%22BTCUSDT%22%5D"
36    /// or
37    /// curl -g -X GET 'https://api.binance.com/api/v3/exchangeInfo?symbols=["BTCUSDT","BNBBTC"]'
38    /// TODO: Check serialization.
39    pub symbols: Option<Vec<String>>,
40    /// Examples: curl -X GET "https://api.binance.com/api/v3/exchangeInfo?permissions=SPOT"
41    /// or
42    /// curl -X GET "https://api.binance.com/api/v3/exchangeInfo?permissions=%5B%22MARGIN%22%2C%22LEVERAGED%22%5D"
43    /// or
44    /// curl -g -X GET 'https://api.binance.com/api/v3/exchangeInfo?permissions=["MARGIN","LEVERAGED"]'
45    /// TODO: Check serialization.
46    pub permissions: Option<Vec<String>>,
47    /// Controls whether the content of the permissionSets field is populated or not. Defaults to true
48    pub show_permission_sets: Option<bool>,
49    /// Filters symbols that have this tradingStatus. Valid values: TRADING, HALT, BREAK
50    /// Cannot be used in combination with symbols or symbol.
51    pub symbol_status: Option<SymbolStatus>,
52}
53
54#[derive(Debug, Deserialize, PartialEq)]
55#[serde(rename_all = "camelCase")]
56pub struct ExchangeInfo {
57    timezone: String,
58    server_time: Timestamp,
59    rate_limits: Vec<RateLimit>,
60    exchange_filters: Vec<ExchangeFilter>,
61    symbols: Vec<SymbolInfo>,
62    /// Optional field. Present only when SOR is available.
63    /// LINK: https://github.com/binance/binance-spot-api-docs/blob/master/faqs/sor_faq.md
64    sors: Option<Vec<SOR>>,
65}
66
67#[derive(Debug, Deserialize, PartialEq)]
68#[serde(rename_all = "camelCase")]
69pub struct RateLimit {
70    rate_limit_type: RateLimiter,
71    interval: RateLimitInterval,
72    interval_num: u64,
73    limit: u64,
74}
75
76#[derive(Debug, Deserialize, PartialEq)]
77#[serde(rename_all = "camelCase")]
78pub struct SymbolInfo {
79    symbol: String,
80    status: SymbolStatus,
81    base_asset: String,
82    base_asset_precision: u8, // value range: [0:8]
83    quote_asset: String,
84    // INFO: 'quote_precision' will be removed in future api versions (v4+)
85    quote_asset_precision: u8,      // value range: [0:8]
86    base_commission_precision: u8,  // value range: [0:8]
87    quote_commission_precision: u8, // value range: [0:8]
88    order_types: Vec<OrderType>,
89    iceberg_allowed: bool,
90    oco_allowed: bool,
91    oto_allowed: bool,
92    quote_order_qty_market_allowed: bool,
93    allow_trailing_stop: bool,
94    cancel_replace_allowed: bool,
95    amend_allowed: bool,
96    is_spot_trading_allowed: bool,
97    is_margin_trading_allowed: bool,
98    filters: Vec<Filter>,
99    permissions: Vec<String>,
100    permission_sets: Vec<Vec<String>>,
101    default_self_trade_prevention_mode: STPMode,
102    allowed_self_trade_prevention_modes: Vec<STPMode>,
103}
104
105#[derive(Debug, Deserialize, PartialEq)]
106#[serde(rename_all = "camelCase")]
107pub struct Filter {
108    // TODO:
109}
110
111/// Smart Order Routing (SOR).
112#[derive(Debug, Deserialize, PartialEq)]
113#[serde(rename_all = "camelCase")]
114pub struct SOR {
115    base_asset: String,
116    symbols: Vec<String>,
117}
118
119#[derive(Debug, Serialize, PartialEq)]
120#[serde(rename_all = "camelCase")]
121pub struct GetOrderBookParams {
122    pub symbol: String,
123    /// Default: 100; Maximum: 5000.
124    /// If limit > 5000, only 5000 entries will be returned.
125    pub limit: Option<u64>,
126}
127
128#[derive(Debug, Deserialize, PartialEq)]
129#[serde(rename_all = "camelCase")]
130pub struct OrderBook {
131    last_update_id: u64,
132    bids: Vec<OrderLevel>,
133    asks: Vec<OrderLevel>,
134}
135
136#[derive(Debug, Deserialize, PartialEq)]
137pub struct OrderLevel(Decimal, Decimal);
138
139impl OrderLevel {
140    pub fn price(&self) -> Decimal {
141        self.0
142    }
143    pub fn qty(&self) -> Decimal {
144        self.0
145    }
146}
147
148#[derive(Debug, Serialize, PartialEq)]
149#[serde(rename_all = "camelCase")]
150pub struct GetRecentTradesParams {
151    pub symbol: String,
152    /// Default: 500; Maximum: 1000.
153    pub limit: Option<u64>,
154}
155
156#[derive(Debug, Deserialize, PartialEq)]
157#[serde(rename_all = "camelCase")]
158pub struct RecentTrade {
159    id: u64,
160    price: Decimal,
161    qty: Decimal,
162    quote_qty: Decimal,
163    time: Timestamp,
164    is_buyer_maker: bool,
165    is_best_match: bool,
166}
167
168#[derive(Debug, Serialize, PartialEq)]
169#[serde(rename_all = "camelCase")]
170pub struct GetOlderTradesParams {
171    pub symbol: String,
172    /// Default: 500; Maximum: 1000.
173    pub limit: Option<u64>,
174    /// TradeId to fetch from. Default gets most recent trades.
175    pub from_id: Option<u64>,
176}
177
178#[derive(Debug, Serialize, PartialEq)]
179#[serde(rename_all = "camelCase")]
180pub struct GetAggregateTradesParams {
181    pub symbol: String,
182    /// ID to get aggregate trades from INCLUSIVE.
183    pub from_id: Option<u64>,
184    /// Timestamp in ms to get aggregate trades from INCLUSIVE.
185    pub start_time: Option<Timestamp>,
186    /// Timestamp in ms to get aggregate trades until INCLUSIVE.
187    pub end_time: Option<Timestamp>,
188    /// Default: 500; Maximum: 1000.
189    pub limit: Option<u64>,
190}
191
192#[derive(Debug, Deserialize, PartialEq)]
193pub struct AggregateTrade {
194    /// Aggregate tradeId
195    #[serde(rename = "a")]
196    id: u64,
197    /// Price
198    #[serde(rename = "p")]
199    price: Decimal,
200    /// Quantity
201    #[serde(rename = "q")]
202    qty: Decimal,
203    /// First tradeId
204    #[serde(rename = "f")]
205    first_trade_id: u64,
206    /// Last tradeId
207    #[serde(rename = "l")]
208    last_trade_id: u64,
209    /// Timestamp
210    #[serde(rename = "T")]
211    time: Timestamp,
212    /// Was the buyer the maker?
213    #[serde(rename = "m")]
214    is_buyer_maker: bool,
215    /// Was the trade the best price match?
216    #[serde(rename = "M")]
217    is_best_match: bool,
218}
219
220#[derive(Debug, Serialize, PartialEq)]
221#[serde(rename_all = "camelCase")]
222pub struct GetKlineListParams {
223    pub symbol: String,
224    pub interval: KlineInterval,
225    pub start_time: Option<Timestamp>,
226    pub end_time: Option<Timestamp>,
227    pub time_zone: Option<String>,
228    /// Default: 500; Maximum: 1000.
229    pub limit: Option<u64>,
230}
231
232#[derive(Debug, Deserialize, PartialEq)]
233pub struct Kline(
234    Timestamp, // Kline open time
235    Decimal,   // Open price
236    Decimal,   // High price
237    Decimal,   // Low price
238    Decimal,   // Close price
239    Decimal,   // Volume
240    Timestamp, // Kline Close time
241    Decimal,   // Quote asset volume
242    u64,       // Number of trades
243    Decimal,   // Taker buy base asset volume
244    Decimal,   // Taker buy quote asset volume
245    String,    // DEPRECATED: Unused field, ignore.
246);
247
248impl Kline {
249    /// Kline open time
250    pub fn time_open(&self) -> Timestamp {
251        self.0
252    }
253    /// Open price
254    pub fn open(&self) -> Decimal {
255        self.1
256    }
257    /// High price
258    pub fn high(&self) -> Decimal {
259        self.2
260    }
261    /// Low price
262    pub fn low(&self) -> Decimal {
263        self.3
264    }
265    /// Close price
266    pub fn close(&self) -> Decimal {
267        self.4
268    }
269    /// Volume
270    pub fn volume(&self) -> Decimal {
271        self.5
272    }
273    /// Kline Close time
274    pub fn time_close(&self) -> Timestamp {
275        self.6
276    }
277    /// Quote asset volume
278    pub fn quote_asset_volume(&self) -> Decimal {
279        self.7
280    }
281    /// Number of trades
282    pub fn id(&self) -> u64 {
283        self.8
284    }
285    /// Taker buy base asset volume
286    pub fn taker_buy_base_asset_volume(&self) -> Decimal {
287        self.9
288    }
289    /// Taker buy quote asset volume
290    pub fn taker_buy_quote_asset_volume(&self) -> Decimal {
291        self.10
292    }
293}
294
295#[derive(Debug, Serialize, PartialEq)]
296#[serde(rename_all = "camelCase")]
297pub struct GetCurrentAveragePriceParams {
298    pub symbol: String,
299}
300
301#[derive(Debug, Deserialize, PartialEq)]
302#[serde(rename_all = "camelCase")]
303pub struct CurrentAveragePrice {
304    /// Average price interval (in minutes)
305    pub mins: u64,
306    /// Average price
307    pub price: Decimal,
308    /// Last trade time
309    pub close_time: Timestamp,
310}
311
312#[cfg(test)]
313mod tests {
314    use rust_decimal::dec;
315
316    use crate::spot::serde::deserialize_str;
317
318    use super::*;
319
320    #[test]
321    fn deserialize_response_exchange_info() {
322        let json = r#"{
323            "timezone": "UTC",
324            "serverTime": 1565246363776,
325            "rateLimits": [],
326            "exchangeFilters": [],
327            "symbols": [
328                {
329                    "symbol": "ETHBTC",
330                    "status": "TRADING",
331                    "baseAsset": "ETH",
332                    "baseAssetPrecision": 8,
333                    "quoteAsset": "BTC",
334                    "quotePrecision": 8,
335                    "quoteAssetPrecision": 8,
336                    "baseCommissionPrecision": 8,
337                    "quoteCommissionPrecision": 8,
338                    "orderTypes": [
339                        "LIMIT",
340                        "LIMIT_MAKER",
341                        "MARKET",
342                        "STOP_LOSS",
343                        "STOP_LOSS_LIMIT",
344                        "TAKE_PROFIT",
345                        "TAKE_PROFIT_LIMIT"
346                    ],
347                    "icebergAllowed": true,
348                    "ocoAllowed": true,
349                    "otoAllowed": true,
350                    "quoteOrderQtyMarketAllowed": true,
351                    "allowTrailingStop": false,
352                    "cancelReplaceAllowed":false,
353                    "amendAllowed":false,
354                    "isSpotTradingAllowed": true,
355                    "isMarginTradingAllowed": true,
356                    "filters": [],
357                    "permissions": [],
358                    "permissionSets": [
359                        [
360                            "SPOT",
361                            "MARGIN"
362                        ]
363                    ],
364                    "defaultSelfTradePreventionMode": "NONE",
365                    "allowedSelfTradePreventionModes": [
366                        "NONE"
367                    ]
368                }
369            ],
370            "sors": [
371                {
372                    "baseAsset": "BTC",
373                    "symbols": [
374                        "BTCUSDT",
375                        "BTCUSDC"
376                    ]
377                }
378            ]
379        }"#;
380        let expected = ExchangeInfo {
381            timezone: String::from("UTC"),
382            server_time: 1565246363776,
383            rate_limits: vec![],
384            exchange_filters: vec![],
385            symbols: vec![SymbolInfo {
386                symbol: String::from("ETHBTC"),
387                status: SymbolStatus::Trading,
388                base_asset: String::from("ETH"),
389                base_asset_precision: 8,
390                quote_asset: String::from("BTC"),
391                quote_asset_precision: 8,
392                base_commission_precision: 8,
393                quote_commission_precision: 8,
394                order_types: vec![
395                    OrderType::Limit,
396                    OrderType::LimitMaker,
397                    OrderType::Market,
398                    OrderType::StopLoss,
399                    OrderType::StopLossLimit,
400                    OrderType::TakeProfit,
401                    OrderType::TakeProfitLimit,
402                ],
403                iceberg_allowed: true,
404                oco_allowed: true,
405                oto_allowed: true,
406                quote_order_qty_market_allowed: true,
407                allow_trailing_stop: false,
408                cancel_replace_allowed: false,
409                amend_allowed: false,
410                is_spot_trading_allowed: true,
411                is_margin_trading_allowed: true,
412                filters: vec![],
413                permissions: vec![],
414                permission_sets: vec![vec![String::from("SPOT"), String::from("MARGIN")]],
415                default_self_trade_prevention_mode: STPMode::None,
416                allowed_self_trade_prevention_modes: vec![STPMode::None],
417            }],
418            sors: Some(vec![SOR {
419                base_asset: String::from("BTC"),
420                symbols: vec![String::from("BTCUSDT"), String::from("BTCUSDC")],
421            }]),
422        };
423
424        let current = deserialize_str(json).unwrap();
425
426        assert_eq!(expected, current);
427    }
428
429    #[test]
430    fn deserialize_response_order_book() {
431        let json = r#"{
432            "lastUpdateId": 1027024,
433            "bids": [
434                [
435                "4.00000000",
436                "431.00000000"
437                ]
438            ],
439            "asks": [
440                [
441                "4.00000200",
442                "12.00000000"
443                ]
444            ]
445        }"#;
446        let expected = OrderBook {
447            last_update_id: 1027024,
448            bids: vec![OrderLevel(dec!(4.00000000), dec!(431.00000000))],
449            asks: vec![OrderLevel(dec!(4.00000200), dec!(12.00000000))],
450        };
451
452        let current = deserialize_str(json).unwrap();
453
454        assert_eq!(expected, current);
455    }
456}