nautilus_binance/spot/websocket/trading/
user_data.rs1use nautilus_core::serialization::deserialize_decimal_from_str;
22use rust_decimal::Decimal;
23use serde::Deserialize;
24use ustr::Ustr;
25
26use crate::{
27 common::enums::{BinanceOrderStatus, BinanceSide, BinanceTimeInForce},
28 spot::sbe::spot::{
29 contingency_type::ContingencyType, list_order_status::ListOrderStatus,
30 list_status_type::ListStatusType,
31 },
32};
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
36#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
37pub enum BinanceSpotExecutionType {
38 New,
40 Canceled,
42 Replaced,
44 Rejected,
46 Trade,
48 Expired,
50 TradePrevention,
52 #[serde(other)]
54 Unknown,
55}
56
57#[derive(Debug, Clone, Deserialize)]
65pub struct BinanceSpotExecutionReport {
66 #[serde(rename = "e")]
68 pub event_type: String,
69 #[serde(rename = "E")]
71 pub event_time: i64,
72 #[serde(rename = "s")]
74 pub symbol: Ustr,
75 #[serde(rename = "c")]
77 pub client_order_id: String,
78 #[serde(rename = "S")]
80 pub side: BinanceSide,
81 #[serde(rename = "o")]
83 pub order_type: String,
84 #[serde(rename = "f")]
86 pub time_in_force: BinanceTimeInForce,
87 #[serde(rename = "q")]
89 pub original_qty: String,
90 #[serde(rename = "p")]
92 pub price: String,
93 #[serde(rename = "P")]
95 pub stop_price: String,
96 #[serde(rename = "x")]
98 pub execution_type: BinanceSpotExecutionType,
99 #[serde(rename = "X")]
101 pub order_status: BinanceOrderStatus,
102 #[serde(rename = "r")]
104 pub reject_reason: String,
105 #[serde(rename = "i")]
107 pub order_id: i64,
108 #[serde(rename = "l")]
110 pub last_filled_qty: String,
111 #[serde(rename = "z")]
113 pub cumulative_filled_qty: String,
114 #[serde(rename = "L")]
116 pub last_filled_price: String,
117 #[serde(rename = "n")]
119 pub commission: String,
120 #[serde(rename = "N", default)]
122 pub commission_asset: Option<Ustr>,
123 #[serde(rename = "T")]
125 pub transaction_time: i64,
126 #[serde(rename = "t")]
128 pub trade_id: i64,
129 #[serde(rename = "w")]
131 pub is_working: bool,
132 #[serde(rename = "m")]
134 pub is_maker: bool,
135 #[serde(rename = "O")]
137 pub order_creation_time: i64,
138 #[serde(rename = "Z")]
140 pub cumulative_quote_qty: String,
141 #[serde(rename = "C", default)]
143 pub original_client_order_id: Option<String>,
144 #[serde(rename = "eR", default)]
146 pub expiry_reason: Option<String>,
147}
148
149impl BinanceSpotExecutionReport {
150 #[must_use]
156 pub fn order_client_order_id(&self) -> &str {
157 match self.original_client_order_id.as_deref() {
158 Some(id) if !id.is_empty() => id,
159 _ => &self.client_order_id,
160 }
161 }
162}
163
164#[derive(Debug, Clone, PartialEq, Eq)]
174pub struct BinanceSpotListStatus {
175 pub event_time_us: i64,
176 pub transaction_time_us: i64,
177 pub order_list_id: i64,
178 pub contingency_type: ContingencyType,
179 pub list_status_type: ListStatusType,
180 pub list_order_status: ListOrderStatus,
181 pub subscription_id: Option<u16>,
182 pub orders: Vec<BinanceSpotListOrder>,
183 pub symbol: Ustr,
184 pub list_client_order_id: String,
185 pub reject_reason: String,
186}
187
188#[derive(Debug, Clone, PartialEq, Eq)]
190pub struct BinanceSpotListOrder {
191 pub order_id: i64,
192 pub symbol: Ustr,
193 pub client_order_id: String,
194}
195
196#[derive(Debug, Clone, Deserialize)]
204pub struct BinanceSpotAccountPositionMsg {
205 #[serde(rename = "e")]
207 pub event_type: String,
208 #[serde(rename = "E")]
210 pub event_time: i64,
211 #[serde(rename = "u")]
213 pub last_update_time: i64,
214 #[serde(rename = "B")]
216 pub balances: Vec<BinanceSpotBalanceEntry>,
217}
218
219#[derive(Debug, Clone, Deserialize)]
221pub struct BinanceSpotBalanceEntry {
222 #[serde(rename = "a")]
224 pub asset: Ustr,
225 #[serde(rename = "f", deserialize_with = "deserialize_decimal_from_str")]
227 pub free: Decimal,
228 #[serde(rename = "l", deserialize_with = "deserialize_decimal_from_str")]
230 pub locked: Decimal,
231}
232
233#[derive(Debug, Clone, Deserialize)]
242pub struct BinanceSpotBalanceUpdateMsg {
243 #[serde(rename = "e")]
245 pub event_type: String,
246 #[serde(rename = "E")]
248 pub event_time: i64,
249 #[serde(rename = "a")]
251 pub asset: Ustr,
252 #[serde(rename = "d")]
254 pub delta: String,
255 #[serde(rename = "T")]
257 pub clear_time: i64,
258}
259
260#[cfg(test)]
261mod tests {
262 use rstest::rstest;
263
264 use super::*;
265 use crate::common::testing::{load_event_fixture, load_fixture_string};
266
267 #[rstest]
268 fn test_deserialize_execution_report_new() {
269 let json = load_event_fixture("spot/user_data_json/execution_report_wrapped.json");
270 let msg: BinanceSpotExecutionReport = serde_json::from_value(json).unwrap();
271
272 assert_eq!(msg.event_type, "executionReport");
273 assert_eq!(msg.symbol.as_str(), "ETHBTC");
274 assert_eq!(msg.execution_type, BinanceSpotExecutionType::New);
275 assert_eq!(msg.order_status, BinanceOrderStatus::New);
276 assert_eq!(msg.order_id, 4293153);
277 assert_eq!(msg.side, BinanceSide::Buy);
278 }
279
280 #[rstest]
281 fn test_deserialize_execution_report_trade() {
282 let json = load_fixture_string("spot/user_data_json/execution_report_trade.json");
283 let msg: BinanceSpotExecutionReport = serde_json::from_str(&json).unwrap();
284
285 assert_eq!(msg.execution_type, BinanceSpotExecutionType::Trade);
286 assert_eq!(msg.order_status, BinanceOrderStatus::Filled);
287 assert_eq!(msg.trade_id, 98765432);
288 assert_eq!(msg.last_filled_qty, "1.00000000");
289 assert_eq!(msg.last_filled_price, "2500.00000000");
290 assert!(msg.is_maker);
291 }
292
293 #[rstest]
294 fn test_deserialize_execution_report_canceled() {
295 let json = load_fixture_string("spot/user_data_json/execution_report_canceled.json");
296 let msg: BinanceSpotExecutionReport = serde_json::from_str(&json).unwrap();
297
298 assert_eq!(msg.execution_type, BinanceSpotExecutionType::Canceled);
299 assert_eq!(msg.order_status, BinanceOrderStatus::Canceled);
300 }
301
302 #[rstest]
303 fn test_deserialize_execution_report_expiry_reason() {
304 let json = load_fixture_string("spot/user_data_json/execution_report_expired.json");
305 let msg: BinanceSpotExecutionReport = serde_json::from_str(&json).unwrap();
306
307 assert_eq!(msg.execution_type, BinanceSpotExecutionType::Expired);
308 assert_eq!(msg.order_status, BinanceOrderStatus::Expired);
309 assert_eq!(msg.expiry_reason.as_deref(), Some("INSUFFICIENT_LIQUIDITY"));
310 }
311
312 #[rstest]
313 fn test_deserialize_account_position() {
314 let json = load_event_fixture("spot/user_data_json/account_position_wrapped.json");
315 let msg: BinanceSpotAccountPositionMsg = serde_json::from_value(json).unwrap();
316
317 assert_eq!(msg.event_type, "outboundAccountPosition");
318 assert!(!msg.balances.is_empty());
319 assert_eq!(msg.balances[0].asset.as_str(), "ETH");
320 }
321
322 #[rstest]
323 fn test_deserialize_balance_update() {
324 let json = load_event_fixture("spot/user_data_json/balance_update_wrapped.json");
325 let msg: BinanceSpotBalanceUpdateMsg = serde_json::from_value(json).unwrap();
326
327 assert_eq!(msg.event_type, "balanceUpdate");
328 assert_eq!(msg.asset.as_str(), "BTC");
329 assert_eq!(msg.delta, "100.00000000");
330 }
331}