Skip to main content

nautilus_binance/spot/websocket/trading/
user_data.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Binance Spot User Data Stream message types.
17//!
18//! Pure venue types with no Nautilus model imports. These structs map directly
19//! to the JSON payloads from the Binance Spot user data stream WebSocket.
20
21use 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/// Spot-specific execution type for order updates.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
30#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
31pub enum BinanceSpotExecutionType {
32    /// New order accepted.
33    New,
34    /// Order canceled.
35    Canceled,
36    /// Order replaced (cancel-replace).
37    Replaced,
38    /// Order rejected.
39    Rejected,
40    /// Trade (partial or full fill).
41    Trade,
42    /// Order expired (IOC/FOK not filled, or GTD expiration).
43    Expired,
44    /// Self-trade prevention triggered.
45    TradePrevention,
46}
47
48/// Execution report event (`executionReport`) from the Spot user data stream.
49///
50/// Contains all fields needed to determine order lifecycle state and fill details.
51///
52/// # References
53///
54/// - <https://developers.binance.com/docs/binance-spot-api-docs/user-data-stream/event-order-update>
55#[derive(Debug, Clone, Deserialize)]
56pub struct BinanceSpotExecutionReport {
57    /// Event type ("executionReport").
58    #[serde(rename = "e")]
59    pub event_type: String,
60    /// Event time in milliseconds.
61    #[serde(rename = "E")]
62    pub event_time: i64,
63    /// Symbol.
64    #[serde(rename = "s")]
65    pub symbol: Ustr,
66    /// Client order ID.
67    #[serde(rename = "c")]
68    pub client_order_id: String,
69    /// Side.
70    #[serde(rename = "S")]
71    pub side: BinanceSide,
72    /// Order type (LIMIT, MARKET, STOP_LOSS, etc.).
73    #[serde(rename = "o")]
74    pub order_type: String,
75    /// Time in force.
76    #[serde(rename = "f")]
77    pub time_in_force: BinanceTimeInForce,
78    /// Original quantity.
79    #[serde(rename = "q")]
80    pub original_qty: String,
81    /// Original price.
82    #[serde(rename = "p")]
83    pub price: String,
84    /// Stop price.
85    #[serde(rename = "P")]
86    pub stop_price: String,
87    /// Current execution type.
88    #[serde(rename = "x")]
89    pub execution_type: BinanceSpotExecutionType,
90    /// Current order status.
91    #[serde(rename = "X")]
92    pub order_status: BinanceOrderStatus,
93    /// Order reject reason (only for Rejected).
94    #[serde(rename = "r")]
95    pub reject_reason: String,
96    /// Order ID.
97    #[serde(rename = "i")]
98    pub order_id: i64,
99    /// Last executed quantity.
100    #[serde(rename = "l")]
101    pub last_filled_qty: String,
102    /// Cumulative filled quantity.
103    #[serde(rename = "z")]
104    pub cumulative_filled_qty: String,
105    /// Last executed price.
106    #[serde(rename = "L")]
107    pub last_filled_price: String,
108    /// Commission amount.
109    #[serde(rename = "n")]
110    pub commission: String,
111    /// Commission asset.
112    #[serde(rename = "N", default)]
113    pub commission_asset: Option<Ustr>,
114    /// Transaction time in milliseconds.
115    #[serde(rename = "T")]
116    pub transaction_time: i64,
117    /// Trade ID (-1 if not a trade).
118    #[serde(rename = "t")]
119    pub trade_id: i64,
120    /// Is the order on the book?
121    #[serde(rename = "w")]
122    pub is_working: bool,
123    /// Is this a maker trade?
124    #[serde(rename = "m")]
125    pub is_maker: bool,
126    /// Order creation time in milliseconds.
127    #[serde(rename = "O")]
128    pub order_creation_time: i64,
129    /// Cumulative quote asset transacted quantity.
130    #[serde(rename = "Z")]
131    pub cumulative_quote_qty: String,
132    /// Original client order ID (for cancel-replace).
133    #[serde(rename = "C", default)]
134    pub original_client_order_id: Option<String>,
135    /// Expiry reason for expired orders.
136    #[serde(rename = "eR", default)]
137    pub expiry_reason: Option<String>,
138}
139
140/// Account position update event (`outboundAccountPosition`).
141///
142/// Sent whenever there is a balance change (not associated with an order).
143///
144/// # References
145///
146/// - <https://developers.binance.com/docs/binance-spot-api-docs/user-data-stream/event-outbound-account-position>
147#[derive(Debug, Clone, Deserialize)]
148pub struct BinanceSpotAccountPositionMsg {
149    /// Event type ("outboundAccountPosition").
150    #[serde(rename = "e")]
151    pub event_type: String,
152    /// Event time in milliseconds.
153    #[serde(rename = "E")]
154    pub event_time: i64,
155    /// Last account update time.
156    #[serde(rename = "u")]
157    pub last_update_time: i64,
158    /// Account balances.
159    #[serde(rename = "B")]
160    pub balances: Vec<BinanceSpotBalanceEntry>,
161}
162
163/// Individual balance entry within an account position update.
164#[derive(Debug, Clone, Deserialize)]
165pub struct BinanceSpotBalanceEntry {
166    /// Asset name.
167    #[serde(rename = "a")]
168    pub asset: Ustr,
169    /// Free balance.
170    #[serde(rename = "f", deserialize_with = "deserialize_decimal_from_str")]
171    pub free: Decimal,
172    /// Locked balance.
173    #[serde(rename = "l", deserialize_with = "deserialize_decimal_from_str")]
174    pub locked: Decimal,
175}
176
177/// Balance update event (`balanceUpdate`).
178///
179/// Sent when a deposit or withdrawal is processed, or when balances change
180/// outside of trading (e.g., interest, fees).
181///
182/// # References
183///
184/// - <https://developers.binance.com/docs/binance-spot-api-docs/user-data-stream/event-balance-update>
185#[derive(Debug, Clone, Deserialize)]
186pub struct BinanceSpotBalanceUpdateMsg {
187    /// Event type ("balanceUpdate").
188    #[serde(rename = "e")]
189    pub event_type: String,
190    /// Event time in milliseconds.
191    #[serde(rename = "E")]
192    pub event_time: i64,
193    /// Asset.
194    #[serde(rename = "a")]
195    pub asset: Ustr,
196    /// Balance delta.
197    #[serde(rename = "d")]
198    pub delta: String,
199    /// Clear time in milliseconds.
200    #[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}