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}
47
48#[derive(Debug, Clone, Deserialize)]
56pub struct BinanceSpotExecutionReport {
57 #[serde(rename = "e")]
59 pub event_type: String,
60 #[serde(rename = "E")]
62 pub event_time: i64,
63 #[serde(rename = "s")]
65 pub symbol: Ustr,
66 #[serde(rename = "c")]
68 pub client_order_id: String,
69 #[serde(rename = "S")]
71 pub side: BinanceSide,
72 #[serde(rename = "o")]
74 pub order_type: String,
75 #[serde(rename = "f")]
77 pub time_in_force: BinanceTimeInForce,
78 #[serde(rename = "q")]
80 pub original_qty: String,
81 #[serde(rename = "p")]
83 pub price: String,
84 #[serde(rename = "P")]
86 pub stop_price: String,
87 #[serde(rename = "x")]
89 pub execution_type: BinanceSpotExecutionType,
90 #[serde(rename = "X")]
92 pub order_status: BinanceOrderStatus,
93 #[serde(rename = "r")]
95 pub reject_reason: String,
96 #[serde(rename = "i")]
98 pub order_id: i64,
99 #[serde(rename = "l")]
101 pub last_filled_qty: String,
102 #[serde(rename = "z")]
104 pub cumulative_filled_qty: String,
105 #[serde(rename = "L")]
107 pub last_filled_price: String,
108 #[serde(rename = "n")]
110 pub commission: String,
111 #[serde(rename = "N", default)]
113 pub commission_asset: Option<Ustr>,
114 #[serde(rename = "T")]
116 pub transaction_time: i64,
117 #[serde(rename = "t")]
119 pub trade_id: i64,
120 #[serde(rename = "w")]
122 pub is_working: bool,
123 #[serde(rename = "m")]
125 pub is_maker: bool,
126 #[serde(rename = "O")]
128 pub order_creation_time: i64,
129 #[serde(rename = "Z")]
131 pub cumulative_quote_qty: String,
132 #[serde(rename = "C", default)]
134 pub original_client_order_id: Option<String>,
135 #[serde(rename = "eR", default)]
137 pub expiry_reason: Option<String>,
138}
139
140#[derive(Debug, Clone, Deserialize)]
148pub struct BinanceSpotAccountPositionMsg {
149 #[serde(rename = "e")]
151 pub event_type: String,
152 #[serde(rename = "E")]
154 pub event_time: i64,
155 #[serde(rename = "u")]
157 pub last_update_time: i64,
158 #[serde(rename = "B")]
160 pub balances: Vec<BinanceSpotBalanceEntry>,
161}
162
163#[derive(Debug, Clone, Deserialize)]
165pub struct BinanceSpotBalanceEntry {
166 #[serde(rename = "a")]
168 pub asset: Ustr,
169 #[serde(rename = "f", deserialize_with = "deserialize_decimal_from_str")]
171 pub free: Decimal,
172 #[serde(rename = "l", deserialize_with = "deserialize_decimal_from_str")]
174 pub locked: Decimal,
175}
176
177#[derive(Debug, Clone, Deserialize)]
186pub struct BinanceSpotBalanceUpdateMsg {
187 #[serde(rename = "e")]
189 pub event_type: String,
190 #[serde(rename = "E")]
192 pub event_time: i64,
193 #[serde(rename = "a")]
195 pub asset: Ustr,
196 #[serde(rename = "d")]
198 pub delta: String,
199 #[serde(rename = "T")]
201 pub clear_time: i64,
202}
203
204#[cfg(test)]
205mod tests {
206 use rstest::rstest;
207
208 use super::*;
209 use crate::common::testing::{load_event_fixture, load_fixture_string};
210
211 #[rstest]
212 fn test_deserialize_execution_report_new() {
213 let json = load_event_fixture("spot/user_data_json/execution_report_wrapped.json");
214 let msg: BinanceSpotExecutionReport = serde_json::from_value(json).unwrap();
215
216 assert_eq!(msg.event_type, "executionReport");
217 assert_eq!(msg.symbol.as_str(), "ETHBTC");
218 assert_eq!(msg.execution_type, BinanceSpotExecutionType::New);
219 assert_eq!(msg.order_status, BinanceOrderStatus::New);
220 assert_eq!(msg.order_id, 4293153);
221 assert_eq!(msg.side, BinanceSide::Buy);
222 }
223
224 #[rstest]
225 fn test_deserialize_execution_report_trade() {
226 let json = load_fixture_string("spot/user_data_json/execution_report_trade.json");
227 let msg: BinanceSpotExecutionReport = serde_json::from_str(&json).unwrap();
228
229 assert_eq!(msg.execution_type, BinanceSpotExecutionType::Trade);
230 assert_eq!(msg.order_status, BinanceOrderStatus::Filled);
231 assert_eq!(msg.trade_id, 98765432);
232 assert_eq!(msg.last_filled_qty, "1.00000000");
233 assert_eq!(msg.last_filled_price, "2500.00000000");
234 assert!(msg.is_maker);
235 }
236
237 #[rstest]
238 fn test_deserialize_execution_report_canceled() {
239 let json = load_fixture_string("spot/user_data_json/execution_report_canceled.json");
240 let msg: BinanceSpotExecutionReport = serde_json::from_str(&json).unwrap();
241
242 assert_eq!(msg.execution_type, BinanceSpotExecutionType::Canceled);
243 assert_eq!(msg.order_status, BinanceOrderStatus::Canceled);
244 }
245
246 #[rstest]
247 fn test_deserialize_execution_report_expiry_reason() {
248 let json = load_fixture_string("spot/user_data_json/execution_report_expired.json");
249 let msg: BinanceSpotExecutionReport = serde_json::from_str(&json).unwrap();
250
251 assert_eq!(msg.execution_type, BinanceSpotExecutionType::Expired);
252 assert_eq!(msg.order_status, BinanceOrderStatus::Expired);
253 assert_eq!(msg.expiry_reason.as_deref(), Some("INSUFFICIENT_LIQUIDITY"));
254 }
255
256 #[rstest]
257 fn test_deserialize_account_position() {
258 let json = load_event_fixture("spot/user_data_json/account_position_wrapped.json");
259 let msg: BinanceSpotAccountPositionMsg = serde_json::from_value(json).unwrap();
260
261 assert_eq!(msg.event_type, "outboundAccountPosition");
262 assert!(!msg.balances.is_empty());
263 assert_eq!(msg.balances[0].asset.as_str(), "ETH");
264 }
265
266 #[rstest]
267 fn test_deserialize_balance_update() {
268 let json = load_event_fixture("spot/user_data_json/balance_update_wrapped.json");
269 let msg: BinanceSpotBalanceUpdateMsg = serde_json::from_value(json).unwrap();
270
271 assert_eq!(msg.event_type, "balanceUpdate");
272 assert_eq!(msg.asset.as_str(), "BTC");
273 assert_eq!(msg.delta, "100.00000000");
274 }
275}