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::common::enums::{BinanceOrderStatus, BinanceSide, BinanceTimeInForce};
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
30#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
31pub enum BinanceSpotExecutionType {
32 New,
34 Canceled,
36 Replaced,
38 Rejected,
40 Trade,
42 Expired,
44 TradePrevention,
46 #[serde(other)]
48 Unknown,
49}
50
51#[derive(Debug, Clone, Deserialize)]
59pub struct BinanceSpotExecutionReport {
60 #[serde(rename = "e")]
62 pub event_type: String,
63 #[serde(rename = "E")]
65 pub event_time: i64,
66 #[serde(rename = "s")]
68 pub symbol: Ustr,
69 #[serde(rename = "c")]
71 pub client_order_id: String,
72 #[serde(rename = "S")]
74 pub side: BinanceSide,
75 #[serde(rename = "o")]
77 pub order_type: String,
78 #[serde(rename = "f")]
80 pub time_in_force: BinanceTimeInForce,
81 #[serde(rename = "q")]
83 pub original_qty: String,
84 #[serde(rename = "p")]
86 pub price: String,
87 #[serde(rename = "P")]
89 pub stop_price: String,
90 #[serde(rename = "x")]
92 pub execution_type: BinanceSpotExecutionType,
93 #[serde(rename = "X")]
95 pub order_status: BinanceOrderStatus,
96 #[serde(rename = "r")]
98 pub reject_reason: String,
99 #[serde(rename = "i")]
101 pub order_id: i64,
102 #[serde(rename = "l")]
104 pub last_filled_qty: String,
105 #[serde(rename = "z")]
107 pub cumulative_filled_qty: String,
108 #[serde(rename = "L")]
110 pub last_filled_price: String,
111 #[serde(rename = "n")]
113 pub commission: String,
114 #[serde(rename = "N", default)]
116 pub commission_asset: Option<Ustr>,
117 #[serde(rename = "T")]
119 pub transaction_time: i64,
120 #[serde(rename = "t")]
122 pub trade_id: i64,
123 #[serde(rename = "w")]
125 pub is_working: bool,
126 #[serde(rename = "m")]
128 pub is_maker: bool,
129 #[serde(rename = "O")]
131 pub order_creation_time: i64,
132 #[serde(rename = "Z")]
134 pub cumulative_quote_qty: String,
135 #[serde(rename = "C", default)]
137 pub original_client_order_id: Option<String>,
138 #[serde(rename = "eR", default)]
140 pub expiry_reason: Option<String>,
141}
142
143impl BinanceSpotExecutionReport {
144 #[must_use]
150 pub fn order_client_order_id(&self) -> &str {
151 match self.original_client_order_id.as_deref() {
152 Some(id) if !id.is_empty() => id,
153 _ => &self.client_order_id,
154 }
155 }
156}
157
158#[derive(Debug, Clone, Deserialize)]
166pub struct BinanceSpotAccountPositionMsg {
167 #[serde(rename = "e")]
169 pub event_type: String,
170 #[serde(rename = "E")]
172 pub event_time: i64,
173 #[serde(rename = "u")]
175 pub last_update_time: i64,
176 #[serde(rename = "B")]
178 pub balances: Vec<BinanceSpotBalanceEntry>,
179}
180
181#[derive(Debug, Clone, Deserialize)]
183pub struct BinanceSpotBalanceEntry {
184 #[serde(rename = "a")]
186 pub asset: Ustr,
187 #[serde(rename = "f", deserialize_with = "deserialize_decimal_from_str")]
189 pub free: Decimal,
190 #[serde(rename = "l", deserialize_with = "deserialize_decimal_from_str")]
192 pub locked: Decimal,
193}
194
195#[derive(Debug, Clone, Deserialize)]
204pub struct BinanceSpotBalanceUpdateMsg {
205 #[serde(rename = "e")]
207 pub event_type: String,
208 #[serde(rename = "E")]
210 pub event_time: i64,
211 #[serde(rename = "a")]
213 pub asset: Ustr,
214 #[serde(rename = "d")]
216 pub delta: String,
217 #[serde(rename = "T")]
219 pub clear_time: i64,
220}
221
222#[cfg(test)]
223mod tests {
224 use rstest::rstest;
225
226 use super::*;
227 use crate::common::testing::{load_event_fixture, load_fixture_string};
228
229 #[rstest]
230 fn test_deserialize_execution_report_new() {
231 let json = load_event_fixture("spot/user_data_json/execution_report_wrapped.json");
232 let msg: BinanceSpotExecutionReport = serde_json::from_value(json).unwrap();
233
234 assert_eq!(msg.event_type, "executionReport");
235 assert_eq!(msg.symbol.as_str(), "ETHBTC");
236 assert_eq!(msg.execution_type, BinanceSpotExecutionType::New);
237 assert_eq!(msg.order_status, BinanceOrderStatus::New);
238 assert_eq!(msg.order_id, 4293153);
239 assert_eq!(msg.side, BinanceSide::Buy);
240 }
241
242 #[rstest]
243 fn test_deserialize_execution_report_trade() {
244 let json = load_fixture_string("spot/user_data_json/execution_report_trade.json");
245 let msg: BinanceSpotExecutionReport = serde_json::from_str(&json).unwrap();
246
247 assert_eq!(msg.execution_type, BinanceSpotExecutionType::Trade);
248 assert_eq!(msg.order_status, BinanceOrderStatus::Filled);
249 assert_eq!(msg.trade_id, 98765432);
250 assert_eq!(msg.last_filled_qty, "1.00000000");
251 assert_eq!(msg.last_filled_price, "2500.00000000");
252 assert!(msg.is_maker);
253 }
254
255 #[rstest]
256 fn test_deserialize_execution_report_canceled() {
257 let json = load_fixture_string("spot/user_data_json/execution_report_canceled.json");
258 let msg: BinanceSpotExecutionReport = serde_json::from_str(&json).unwrap();
259
260 assert_eq!(msg.execution_type, BinanceSpotExecutionType::Canceled);
261 assert_eq!(msg.order_status, BinanceOrderStatus::Canceled);
262 }
263
264 #[rstest]
265 fn test_deserialize_execution_report_expiry_reason() {
266 let json = load_fixture_string("spot/user_data_json/execution_report_expired.json");
267 let msg: BinanceSpotExecutionReport = serde_json::from_str(&json).unwrap();
268
269 assert_eq!(msg.execution_type, BinanceSpotExecutionType::Expired);
270 assert_eq!(msg.order_status, BinanceOrderStatus::Expired);
271 assert_eq!(msg.expiry_reason.as_deref(), Some("INSUFFICIENT_LIQUIDITY"));
272 }
273
274 #[rstest]
275 fn test_deserialize_account_position() {
276 let json = load_event_fixture("spot/user_data_json/account_position_wrapped.json");
277 let msg: BinanceSpotAccountPositionMsg = serde_json::from_value(json).unwrap();
278
279 assert_eq!(msg.event_type, "outboundAccountPosition");
280 assert!(!msg.balances.is_empty());
281 assert_eq!(msg.balances[0].asset.as_str(), "ETH");
282 }
283
284 #[rstest]
285 fn test_deserialize_balance_update() {
286 let json = load_event_fixture("spot/user_data_json/balance_update_wrapped.json");
287 let msg: BinanceSpotBalanceUpdateMsg = serde_json::from_value(json).unwrap();
288
289 assert_eq!(msg.event_type, "balanceUpdate");
290 assert_eq!(msg.asset.as_str(), "BTC");
291 assert_eq!(msg.delta, "100.00000000");
292 }
293}