Skip to main content

sandbox_quant/binance/
types.rs

1use serde::Deserialize;
2
3/// Deserialize Binance string-encoded numbers to f64.
4pub fn string_to_f64<'de, D>(deserializer: D) -> Result<f64, D::Error>
5where
6    D: serde::Deserializer<'de>,
7{
8    let s = String::deserialize(deserializer)?;
9    s.parse::<f64>().map_err(serde::de::Error::custom)
10}
11
12pub fn string_or_number_to_f64_default<'de, D>(deserializer: D) -> Result<f64, D::Error>
13where
14    D: serde::Deserializer<'de>,
15{
16    let v = serde_json::Value::deserialize(deserializer)?;
17    match v {
18        serde_json::Value::Null => Ok(0.0),
19        serde_json::Value::String(s) => s.parse::<f64>().map_err(serde::de::Error::custom),
20        serde_json::Value::Number(n) => n
21            .as_f64()
22            .ok_or_else(|| serde::de::Error::custom("invalid number")),
23        _ => Err(serde::de::Error::custom("invalid numeric value")),
24    }
25}
26
27/// Binance trade stream event (symbol@trade).
28#[derive(Debug, Deserialize)]
29pub struct BinanceTradeEvent {
30    #[serde(rename = "e")]
31    pub event_type: String,
32    #[serde(rename = "E")]
33    pub event_time: u64,
34    #[serde(rename = "s")]
35    pub symbol: String,
36    #[serde(rename = "t")]
37    pub trade_id: u64,
38    #[serde(rename = "p", deserialize_with = "string_to_f64")]
39    pub price: f64,
40    #[serde(rename = "q", deserialize_with = "string_to_f64")]
41    pub qty: f64,
42    #[serde(rename = "m")]
43    pub is_buyer_maker: bool,
44}
45
46/// Binance order response (newOrderRespType=FULL).
47#[derive(Debug, Deserialize)]
48#[serde(rename_all = "camelCase")]
49pub struct BinanceOrderResponse {
50    pub symbol: String,
51    pub order_id: u64,
52    pub client_order_id: String,
53    #[serde(deserialize_with = "string_to_f64")]
54    pub price: f64,
55    #[serde(deserialize_with = "string_to_f64")]
56    pub orig_qty: f64,
57    #[serde(deserialize_with = "string_to_f64")]
58    pub executed_qty: f64,
59    pub status: String,
60    pub r#type: String,
61    pub side: String,
62    #[serde(default)]
63    pub fills: Vec<BinanceFill>,
64}
65
66#[derive(Debug, Deserialize)]
67#[serde(rename_all = "camelCase")]
68pub struct BinanceFill {
69    #[serde(deserialize_with = "string_to_f64")]
70    pub price: f64,
71    #[serde(deserialize_with = "string_to_f64")]
72    pub qty: f64,
73    #[serde(deserialize_with = "string_to_f64")]
74    pub commission: f64,
75    pub commission_asset: String,
76}
77
78/// Binance all orders response item (GET /api/v3/allOrders).
79#[derive(Debug, Deserialize, Clone)]
80#[serde(rename_all = "camelCase")]
81pub struct BinanceAllOrder {
82    pub symbol: String,
83    pub order_id: u64,
84    pub client_order_id: String,
85    #[serde(deserialize_with = "string_to_f64")]
86    pub price: f64,
87    #[serde(deserialize_with = "string_to_f64")]
88    pub orig_qty: f64,
89    #[serde(deserialize_with = "string_to_f64")]
90    pub executed_qty: f64,
91    #[serde(deserialize_with = "string_to_f64")]
92    pub cummulative_quote_qty: f64,
93    pub status: String,
94    pub r#type: String,
95    pub side: String,
96    pub time: u64,
97    pub update_time: u64,
98}
99
100/// Binance my trades response item (GET /api/v3/myTrades).
101#[derive(Debug, Deserialize, Clone)]
102#[serde(rename_all = "camelCase")]
103pub struct BinanceMyTrade {
104    pub symbol: String,
105    pub id: u64,
106    pub order_id: u64,
107    #[serde(deserialize_with = "string_to_f64")]
108    pub price: f64,
109    #[serde(deserialize_with = "string_to_f64")]
110    pub qty: f64,
111    #[serde(deserialize_with = "string_to_f64")]
112    pub commission: f64,
113    pub commission_asset: String,
114    pub time: u64,
115    pub is_buyer: bool,
116    pub is_maker: bool,
117    #[serde(default, deserialize_with = "string_or_number_to_f64_default")]
118    pub realized_pnl: f64,
119}
120
121/// Binance API error response.
122#[derive(Debug, Deserialize)]
123pub struct BinanceApiErrorResponse {
124    pub code: i64,
125    pub msg: String,
126}
127
128/// Binance server time response.
129#[derive(Debug, Deserialize)]
130#[serde(rename_all = "camelCase")]
131pub struct ServerTimeResponse {
132    pub server_time: u64,
133}
134
135/// Binance account info response (GET /api/v3/account).
136#[derive(Debug, Deserialize)]
137pub struct AccountInfo {
138    pub balances: Vec<AccountBalance>,
139}
140
141#[derive(Debug, Deserialize)]
142pub struct AccountBalance {
143    pub asset: String,
144    #[serde(deserialize_with = "string_to_f64")]
145    pub free: f64,
146    #[serde(deserialize_with = "string_to_f64")]
147    pub locked: f64,
148}
149
150/// Binance futures order response (POST /fapi/v1/order, RESULT/FULL-like fields).
151#[derive(Debug, Deserialize)]
152#[serde(rename_all = "camelCase")]
153pub struct BinanceFuturesOrderResponse {
154    pub symbol: String,
155    pub order_id: u64,
156    pub client_order_id: String,
157    #[serde(default, deserialize_with = "string_to_f64")]
158    pub price: f64,
159    #[serde(default, deserialize_with = "string_to_f64")]
160    pub orig_qty: f64,
161    #[serde(default, deserialize_with = "string_to_f64")]
162    pub executed_qty: f64,
163    #[serde(default, deserialize_with = "string_to_f64")]
164    pub avg_price: f64,
165    pub status: String,
166    pub r#type: String,
167    pub side: String,
168}
169
170#[derive(Debug, Deserialize, Clone)]
171#[serde(rename_all = "camelCase")]
172pub struct BinanceFuturesAllOrder {
173    pub symbol: String,
174    pub order_id: u64,
175    pub client_order_id: String,
176    #[serde(deserialize_with = "string_or_number_to_f64_default")]
177    pub price: f64,
178    #[serde(deserialize_with = "string_or_number_to_f64_default")]
179    pub orig_qty: f64,
180    #[serde(deserialize_with = "string_or_number_to_f64_default")]
181    pub executed_qty: f64,
182    #[serde(default, deserialize_with = "string_or_number_to_f64_default")]
183    pub cum_quote: f64,
184    #[serde(default, deserialize_with = "string_or_number_to_f64_default")]
185    pub avg_price: f64,
186    pub status: String,
187    pub r#type: String,
188    pub side: String,
189    pub time: u64,
190    pub update_time: u64,
191}
192
193#[derive(Debug, Deserialize, Clone)]
194#[serde(rename_all = "camelCase")]
195pub struct BinanceFuturesUserTrade {
196    pub symbol: String,
197    pub id: u64,
198    pub order_id: u64,
199    #[serde(deserialize_with = "string_or_number_to_f64_default")]
200    pub price: f64,
201    #[serde(deserialize_with = "string_or_number_to_f64_default")]
202    pub qty: f64,
203    #[serde(default, deserialize_with = "string_or_number_to_f64_default")]
204    pub commission: f64,
205    #[serde(default)]
206    pub commission_asset: String,
207    pub time: u64,
208    #[serde(default)]
209    pub buyer: bool,
210    #[serde(default)]
211    pub maker: bool,
212    #[serde(default, deserialize_with = "string_or_number_to_f64_default")]
213    pub realized_pnl: f64,
214}
215
216/// Binance futures account info (GET /fapi/v2/account).
217#[derive(Debug, Deserialize)]
218#[serde(rename_all = "camelCase")]
219pub struct BinanceFuturesAccountInfo {
220    #[serde(default)]
221    pub assets: Vec<BinanceFuturesAssetBalance>,
222}
223
224#[derive(Debug, Deserialize)]
225#[serde(rename_all = "camelCase")]
226pub struct BinanceFuturesAssetBalance {
227    pub asset: String,
228    #[serde(default, deserialize_with = "string_to_f64")]
229    pub wallet_balance: f64,
230    #[serde(default, deserialize_with = "string_to_f64")]
231    pub available_balance: f64,
232}
233
234/// Binance futures position risk row (GET /fapi/v2/positionRisk).
235#[derive(Debug, Deserialize, Clone)]
236#[serde(rename_all = "camelCase")]
237pub struct BinanceFuturesPositionRisk {
238    pub symbol: String,
239    #[serde(default, deserialize_with = "string_or_number_to_f64_default")]
240    pub position_amt: f64,
241    #[serde(default, deserialize_with = "string_or_number_to_f64_default")]
242    pub entry_price: f64,
243    #[serde(default, deserialize_with = "string_or_number_to_f64_default")]
244    pub mark_price: f64,
245    #[serde(
246        default,
247        rename = "unRealizedProfit",
248        alias = "unrealizedProfit",
249        deserialize_with = "string_or_number_to_f64_default"
250    )]
251    pub unrealized_profit: f64,
252}
253
254#[derive(Debug, Deserialize)]
255pub struct BinanceListenKeyResponse {
256    #[serde(rename = "listenKey")]
257    pub listen_key: String,
258}
259
260#[derive(Debug, Deserialize)]
261#[serde(tag = "e")]
262pub enum BinanceFuturesUserDataEvent {
263    #[serde(rename = "ACCOUNT_UPDATE")]
264    AccountUpdate(BinanceFuturesAccountUpdateEvent),
265    #[serde(other)]
266    Unknown,
267}
268
269#[derive(Debug, Deserialize)]
270pub struct BinanceFuturesAccountUpdateEvent {
271    #[serde(rename = "E")]
272    pub event_time: u64,
273    #[serde(rename = "a")]
274    pub account: BinanceFuturesAccountUpdateData,
275}
276
277#[derive(Debug, Deserialize)]
278pub struct BinanceFuturesAccountUpdateData {
279    #[serde(rename = "P", default)]
280    pub positions: Vec<BinanceFuturesAccountUpdatePosition>,
281}
282
283#[derive(Debug, Deserialize, Clone)]
284pub struct BinanceFuturesAccountUpdatePosition {
285    #[serde(rename = "s")]
286    pub symbol: String,
287    #[serde(rename = "pa", deserialize_with = "string_or_number_to_f64_default")]
288    pub position_amt: f64,
289    #[serde(rename = "ep", deserialize_with = "string_or_number_to_f64_default")]
290    pub entry_price: f64,
291    #[serde(rename = "up", deserialize_with = "string_or_number_to_f64_default")]
292    pub unrealized_pnl: f64,
293}
294
295#[derive(Debug, Deserialize)]
296#[serde(tag = "e")]
297pub enum BinanceSpotUserDataEvent {
298    #[serde(rename = "executionReport")]
299    ExecutionReport(BinanceSpotExecutionReportEvent),
300    #[serde(rename = "outboundAccountPosition")]
301    OutboundAccountPosition(BinanceSpotOutboundAccountPositionEvent),
302    #[serde(other)]
303    Unknown,
304}
305
306#[derive(Debug, Deserialize)]
307pub struct BinanceSpotExecutionReportEvent {
308    #[serde(rename = "s")]
309    pub symbol: String,
310}
311
312#[derive(Debug, Deserialize)]
313pub struct BinanceSpotOutboundAccountPositionEvent {}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    #[test]
320    fn deserialize_trade_event() {
321        let json = r#"{
322            "e": "trade",
323            "E": 1672515782136,
324            "s": "BTCUSDT",
325            "t": 12345,
326            "p": "42000.50",
327            "q": "0.001",
328            "T": 1672515782136,
329            "m": false
330        }"#;
331        let event: BinanceTradeEvent = serde_json::from_str(json).unwrap();
332        assert_eq!(event.symbol, "BTCUSDT");
333        assert!((event.price - 42000.50).abs() < f64::EPSILON);
334        assert!((event.qty - 0.001).abs() < f64::EPSILON);
335        assert_eq!(event.trade_id, 12345);
336        assert!(!event.is_buyer_maker);
337    }
338
339    #[test]
340    fn deserialize_order_response() {
341        let json = r#"{
342            "symbol": "BTCUSDT",
343            "orderId": 12345,
344            "clientOrderId": "sq-test",
345            "price": "0.00000000",
346            "origQty": "0.00100000",
347            "executedQty": "0.00100000",
348            "status": "FILLED",
349            "type": "MARKET",
350            "side": "BUY",
351            "fills": [
352                {
353                    "price": "42000.50000000",
354                    "qty": "0.00100000",
355                    "commission": "0.00000100",
356                    "commissionAsset": "BTC"
357                }
358            ]
359        }"#;
360        let resp: BinanceOrderResponse = serde_json::from_str(json).unwrap();
361        assert_eq!(resp.status, "FILLED");
362        assert_eq!(resp.fills.len(), 1);
363        assert!((resp.fills[0].price - 42000.50).abs() < 0.01);
364    }
365
366    #[test]
367    fn deserialize_all_order_item() {
368        let json = r#"{
369            "symbol": "BTCUSDT",
370            "orderId": 28,
371            "clientOrderId": "sq-abc12345",
372            "price": "0.00000000",
373            "origQty": "0.00100000",
374            "executedQty": "0.00100000",
375            "cummulativeQuoteQty": "42.50000000",
376            "status": "FILLED",
377            "timeInForce": "GTC",
378            "type": "MARKET",
379            "side": "BUY",
380            "time": 1700000000000,
381            "updateTime": 1700000001000,
382            "isWorking": true,
383            "workingTime": 1700000001000,
384            "origQuoteOrderQty": "0.00000000",
385            "selfTradePreventionMode": "NONE"
386        }"#;
387        let order: BinanceAllOrder = serde_json::from_str(json).unwrap();
388        assert_eq!(order.symbol, "BTCUSDT");
389        assert_eq!(order.order_id, 28);
390        assert_eq!(order.status, "FILLED");
391        assert!((order.executed_qty - 0.001).abs() < f64::EPSILON);
392    }
393
394    #[test]
395    fn deserialize_my_trade_item() {
396        let json = r#"{
397            "symbol": "BTCUSDT",
398            "id": 28457,
399            "orderId": 100234,
400            "price": "42000.50000000",
401            "qty": "0.00100000",
402            "commission": "0.00000100",
403            "commissionAsset": "BTC",
404            "time": 1700000001000,
405            "isBuyer": true,
406            "isMaker": false,
407            "isBestMatch": true
408        }"#;
409        let trade: BinanceMyTrade = serde_json::from_str(json).unwrap();
410        assert_eq!(trade.symbol, "BTCUSDT");
411        assert_eq!(trade.order_id, 100234);
412        assert!(trade.is_buyer);
413        assert!(!trade.is_maker);
414        assert!((trade.price - 42000.50).abs() < 0.01);
415    }
416}