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}
136
137/// Account position update event (`outboundAccountPosition`).
138///
139/// Sent whenever there is a balance change (not associated with an order).
140///
141/// # References
142///
143/// - <https://developers.binance.com/docs/binance-spot-api-docs/user-data-stream/event-outbound-account-position>
144#[derive(Debug, Clone, Deserialize)]
145pub struct BinanceSpotAccountPositionMsg {
146    /// Event type ("outboundAccountPosition").
147    #[serde(rename = "e")]
148    pub event_type: String,
149    /// Event time in milliseconds.
150    #[serde(rename = "E")]
151    pub event_time: i64,
152    /// Last account update time.
153    #[serde(rename = "u")]
154    pub last_update_time: i64,
155    /// Account balances.
156    #[serde(rename = "B")]
157    pub balances: Vec<BinanceSpotBalanceEntry>,
158}
159
160/// Individual balance entry within an account position update.
161#[derive(Debug, Clone, Deserialize)]
162pub struct BinanceSpotBalanceEntry {
163    /// Asset name.
164    #[serde(rename = "a")]
165    pub asset: Ustr,
166    /// Free balance.
167    #[serde(rename = "f", deserialize_with = "deserialize_decimal_from_str")]
168    pub free: Decimal,
169    /// Locked balance.
170    #[serde(rename = "l", deserialize_with = "deserialize_decimal_from_str")]
171    pub locked: Decimal,
172}
173
174/// Balance update event (`balanceUpdate`).
175///
176/// Sent when a deposit or withdrawal is processed, or when balances change
177/// outside of trading (e.g., interest, fees).
178///
179/// # References
180///
181/// - <https://developers.binance.com/docs/binance-spot-api-docs/user-data-stream/event-balance-update>
182#[derive(Debug, Clone, Deserialize)]
183pub struct BinanceSpotBalanceUpdateMsg {
184    /// Event type ("balanceUpdate").
185    #[serde(rename = "e")]
186    pub event_type: String,
187    /// Event time in milliseconds.
188    #[serde(rename = "E")]
189    pub event_time: i64,
190    /// Asset.
191    #[serde(rename = "a")]
192    pub asset: Ustr,
193    /// Balance delta.
194    #[serde(rename = "d")]
195    pub delta: String,
196    /// Clear time in milliseconds.
197    #[serde(rename = "T")]
198    pub clear_time: i64,
199}
200
201#[cfg(test)]
202mod tests {
203    use rstest::rstest;
204
205    use super::*;
206    use crate::common::testing::{load_event_fixture, load_fixture_string};
207
208    #[rstest]
209    fn test_deserialize_execution_report_new() {
210        let json = load_event_fixture("spot/user_data_json/execution_report_wrapped.json");
211        let msg: BinanceSpotExecutionReport = serde_json::from_value(json).unwrap();
212
213        assert_eq!(msg.event_type, "executionReport");
214        assert_eq!(msg.symbol.as_str(), "ETHBTC");
215        assert_eq!(msg.execution_type, BinanceSpotExecutionType::New);
216        assert_eq!(msg.order_status, BinanceOrderStatus::New);
217        assert_eq!(msg.order_id, 4293153);
218        assert_eq!(msg.side, BinanceSide::Buy);
219    }
220
221    #[rstest]
222    fn test_deserialize_execution_report_trade() {
223        let json = load_fixture_string("spot/user_data_json/execution_report_trade.json");
224        let msg: BinanceSpotExecutionReport = serde_json::from_str(&json).unwrap();
225
226        assert_eq!(msg.execution_type, BinanceSpotExecutionType::Trade);
227        assert_eq!(msg.order_status, BinanceOrderStatus::Filled);
228        assert_eq!(msg.trade_id, 98765432);
229        assert_eq!(msg.last_filled_qty, "1.00000000");
230        assert_eq!(msg.last_filled_price, "2500.00000000");
231        assert!(msg.is_maker);
232    }
233
234    #[rstest]
235    fn test_deserialize_execution_report_canceled() {
236        let json = load_fixture_string("spot/user_data_json/execution_report_canceled.json");
237        let msg: BinanceSpotExecutionReport = serde_json::from_str(&json).unwrap();
238
239        assert_eq!(msg.execution_type, BinanceSpotExecutionType::Canceled);
240        assert_eq!(msg.order_status, BinanceOrderStatus::Canceled);
241    }
242
243    #[rstest]
244    fn test_deserialize_account_position() {
245        let json = load_event_fixture("spot/user_data_json/account_position_wrapped.json");
246        let msg: BinanceSpotAccountPositionMsg = serde_json::from_value(json).unwrap();
247
248        assert_eq!(msg.event_type, "outboundAccountPosition");
249        assert!(!msg.balances.is_empty());
250        assert_eq!(msg.balances[0].asset.as_str(), "ETH");
251    }
252
253    #[rstest]
254    fn test_deserialize_balance_update() {
255        let json = load_event_fixture("spot/user_data_json/balance_update_wrapped.json");
256        let msg: BinanceSpotBalanceUpdateMsg = serde_json::from_value(json).unwrap();
257
258        assert_eq!(msg.event_type, "balanceUpdate");
259        assert_eq!(msg.asset.as_str(), "BTC");
260        assert_eq!(msg.delta, "100.00000000");
261    }
262}