Skip to main content

asterdex_sdk/futures/models/
user_data.rs

1// Tech Lead mini-task (Wave 8): user data stream model types from 05_detail_design.md §2.3
2// Manual Deserialize impl for UserDataEvent is implemented in US-013.
3
4use serde::Deserialize;
5
6/// All user data stream event types received over the WebSocket user data stream.
7///
8/// Deserializes from the raw JSON received over a `UserDataStream`.
9/// Use `UserDataStream::next_event()` to receive typed events.
10///
11/// # Event Types
12/// - `ORDER_TRADE_UPDATE`     → `OrderUpdate`
13/// - `ACCOUNT_UPDATE`         → `AccountUpdate`
14/// - `ACCOUNT_CONFIG_UPDATE`  → `AccountConfigUpdate`
15/// - `MARGIN_CALL`            → `MarginCall`
16/// - `listenKeyExpired`       → `ListenKeyExpired`
17/// - `tradepro`               → `TradePro`
18/// - Unknown types            → `Unknown` (logged as warning, never panics)
19///
20/// Large variants (`OrderUpdate`, `AccountUpdate`) are boxed to reduce stack size.
21#[derive(Debug)]
22pub enum UserDataEvent {
23    /// Order trade update event (boxed to reduce enum stack size — US-016 clippy fix).
24    OrderUpdate(Box<OrderUpdateEvent>),
25    /// Account balance/position update event (boxed to reduce enum stack size — US-016 clippy fix).
26    AccountUpdate(Box<AccountUpdateEvent>),
27    /// Account configuration update event (leverage or multi-assets margin mode change).
28    AccountConfigUpdate(AccountConfigUpdateEvent),
29    /// Margin call risk warning event.
30    MarginCall(MarginCallEvent),
31    /// Listen key expired — consumer must create a new listen key to continue receiving events.
32    ListenKeyExpired(ListenKeyExpiredEvent),
33    /// Trade execution details with on-chain participant addresses.
34    TradePro(TradeProEvent),
35    /// Unknown event type — captured as raw JSON for forward compatibility.
36    Unknown(serde_json::Value),
37}
38
39/// Order trade update event — dispatched when `"e": "ORDER_TRADE_UPDATE"`.
40#[derive(Debug, Deserialize)]
41pub struct OrderUpdateEvent {
42    #[serde(rename = "e")] pub event_type: String,
43    #[serde(rename = "E")] pub event_time: u64,
44    #[serde(rename = "T")] pub transaction_time: u64,
45    #[serde(rename = "o")] pub order: OrderUpdateData,
46}
47
48/// Order details within an `OrderUpdateEvent`.
49#[derive(Debug, Deserialize)]
50pub struct OrderUpdateData {
51    #[serde(rename = "s")] pub symbol: String,
52    #[serde(rename = "c")] pub client_order_id: String,
53    #[serde(rename = "S")] pub side: String,
54    #[serde(rename = "o")] pub order_type: String,
55    #[serde(rename = "f")] pub time_in_force: String,
56    #[serde(rename = "q")] pub quantity: String,
57    #[serde(rename = "p")] pub price: String,
58    #[serde(rename = "ap")] pub avg_price: String,
59    #[serde(rename = "sp")] pub stop_price: String,
60    #[serde(rename = "x")] pub execution_type: String,
61    #[serde(rename = "X")] pub order_status: String,
62    #[serde(rename = "i")] pub order_id: i64,
63    #[serde(rename = "l")] pub last_filled_qty: String,
64    #[serde(rename = "z")] pub cumulative_filled_qty: String,
65    #[serde(rename = "L")] pub last_filled_price: String,
66    /// Commission asset — only present when commission is charged.
67    #[serde(rename = "N", default)] pub commission_asset: Option<String>,
68    /// Commission amount — only present when commission is charged.
69    #[serde(rename = "n", default)] pub commission: Option<String>,
70    #[serde(rename = "t")] pub trade_id: i64,
71    #[serde(rename = "b", default)] pub bids_notional: String,
72    #[serde(rename = "a", default)] pub ask_notional: String,
73    #[serde(rename = "m", default)] pub is_maker: bool,
74    #[serde(rename = "R", default)] pub is_reduce_only: bool,
75    #[serde(rename = "wt", default)] pub working_type: String,
76    #[serde(rename = "ot", default)] pub orig_order_type: String,
77    #[serde(rename = "ps")] pub position_side: String,
78    #[serde(rename = "cp", default)] pub close_position: bool,
79    /// Activation price — only present for TRAILING_STOP_MARKET orders.
80    #[serde(rename = "AP", default)] pub activation_price: Option<String>,
81    /// Callback rate — only present for TRAILING_STOP_MARKET orders.
82    #[serde(rename = "cr", default)] pub callback_rate: Option<String>,
83    #[serde(rename = "rp")] pub realized_profit: String,
84    #[serde(rename = "T")] pub order_trade_time: u64,
85}
86
87/// Account update event — dispatched when `"e": "ACCOUNT_UPDATE"`.
88#[derive(Debug, Deserialize)]
89pub struct AccountUpdateEvent {
90    #[serde(rename = "e")] pub event_type: String,
91    #[serde(rename = "E")] pub event_time: u64,
92    #[serde(rename = "T")] pub transaction_time: u64,
93    #[serde(rename = "a")] pub account: AccountUpdateData,
94}
95
96/// Account data within an `AccountUpdateEvent`.
97#[derive(Debug, Deserialize)]
98pub struct AccountUpdateData {
99    #[serde(rename = "m")] pub event_reason: String,
100    #[serde(rename = "B")] pub balances: Vec<BalanceDelta>,
101    #[serde(rename = "P")] pub positions: Vec<PositionDelta>,
102}
103
104/// Balance change within an `AccountUpdateData`.
105#[derive(Debug, Deserialize)]
106pub struct BalanceDelta {
107    #[serde(rename = "a")] pub asset: String,
108    #[serde(rename = "wb")] pub wallet_balance: String,
109    #[serde(rename = "cw")] pub cross_wallet_balance: String,
110    #[serde(rename = "bc")] pub balance_change: String,
111}
112
113/// Position change within an `AccountUpdateData`.
114#[derive(Debug, Deserialize)]
115pub struct PositionDelta {
116    #[serde(rename = "s")] pub symbol: String,
117    #[serde(rename = "pa")] pub position_amount: String,
118    #[serde(rename = "ep")] pub entry_price: String,
119    #[serde(rename = "cr")] pub accumulated_realized: String,
120    #[serde(rename = "up")] pub unrealized_pnl: String,
121    #[serde(rename = "mt")] pub margin_type: String,
122    #[serde(rename = "iw")] pub isolated_wallet: String,
123    #[serde(rename = "ps")] pub position_side: String,
124}
125
126/// Account configuration update event — dispatched when `"e": "ACCOUNT_CONFIG_UPDATE"`.
127///
128/// Contains either a leverage change (`ac`) or a multi-assets margin mode change (`ai`),
129/// but not both at the same time.
130#[derive(Debug, Deserialize)]
131pub struct AccountConfigUpdateEvent {
132    #[serde(rename = "e")] pub event_type: String,
133    #[serde(rename = "E")] pub event_time: u64,
134    #[serde(rename = "T")] pub transaction_time: u64,
135    #[serde(rename = "ac")] pub leverage_change: Option<LeverageChangeData>,
136    #[serde(rename = "ai")] pub multi_assets_margin: Option<MultiAssetsModeData>,
137}
138
139/// Leverage change within an `AccountConfigUpdateEvent`.
140#[derive(Debug, Deserialize)]
141pub struct LeverageChangeData {
142    #[serde(rename = "s")] pub symbol: String,
143    #[serde(rename = "l")] pub leverage: u32,
144}
145
146/// Multi-assets margin mode change within an `AccountConfigUpdateEvent`.
147#[derive(Debug, Deserialize)]
148pub struct MultiAssetsModeData {
149    #[serde(rename = "j")] pub is_enabled: bool,
150    /// Specified token fee deduction enabled.
151    #[serde(rename = "f", default)] pub fee_deduction: Option<bool>,
152    /// Position mode: `true` = dual-side (hedge), `false` = single-side (one-way).
153    #[serde(rename = "d", default)] pub position_mode_dual: Option<bool>,
154}
155
156/// Margin call risk warning event — dispatched when `"e": "MARGIN_CALL"`.
157#[derive(Debug, Deserialize)]
158pub struct MarginCallEvent {
159    #[serde(rename = "e")] pub event_type: String,
160    #[serde(rename = "E")] pub event_time: u64,
161    /// Cross wallet balance — only present when a crossed position triggers the call.
162    #[serde(rename = "cw", default)] pub cross_wallet_balance: Option<String>,
163    #[serde(rename = "p")] pub positions: Vec<MarginCallPosition>,
164}
165
166/// Position at risk within a `MarginCallEvent`.
167#[derive(Debug, Deserialize)]
168pub struct MarginCallPosition {
169    #[serde(rename = "s")] pub symbol: String,
170    #[serde(rename = "ps")] pub position_side: String,
171    #[serde(rename = "pa")] pub position_amount: String,
172    #[serde(rename = "mt")] pub margin_type: String,
173    #[serde(rename = "iw")] pub isolated_wallet: String,
174    #[serde(rename = "mp")] pub mark_price: String,
175    #[serde(rename = "up")] pub unrealized_pnl: String,
176    #[serde(rename = "mm")] pub maintenance_margin_required: String,
177}
178
179/// Trade execution event with on-chain details — dispatched when `"e": "tradepro"`.
180#[derive(Debug, Deserialize)]
181pub struct TradeProEvent {
182    #[serde(rename = "e")] pub event_type: String,
183    #[serde(rename = "E")] pub event_time: u64,
184    #[serde(rename = "T")] pub transaction_time: u64,
185    #[serde(rename = "s")] pub symbol: String,
186    #[serde(rename = "t")] pub trade_id: i64,
187    #[serde(rename = "p")] pub price: String,
188    #[serde(rename = "q")] pub quantity: String,
189    /// Transaction hash of the trade.
190    #[serde(rename = "h")] pub tx_hash: String,
191    /// Participant addresses: `[taker_address, maker_address]`.
192    #[serde(rename = "m")] pub participants: Vec<String>,
193}
194
195/// Listen key expired event — dispatched when `"e": "listenKeyExpired"`.
196///
197/// When this event is received, the consumer must create a new listen key
198/// (via `RestClient::create_listen_key`) and re-subscribe to the user data
199/// stream. No further events will be delivered on the current stream.
200#[derive(Debug, Deserialize)]
201pub struct ListenKeyExpiredEvent {
202    #[serde(rename = "e")] pub event_type: String,
203    #[serde(rename = "E")] pub event_time: u64,
204}
205
206// US-013: Manual Deserialize impl for UserDataEvent — two-pass approach.
207// `#[serde(tag = "e")]` with an `#[serde(other)]` variant that captures a Value is
208// not supported by serde. We deserialize to a raw Value first, dispatch on "e", then
209// deserialize the full Value into the correct typed struct.
210impl<'de> serde::Deserialize<'de> for UserDataEvent {
211    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
212        let v = serde_json::Value::deserialize(deserializer)?;
213        let event_type = v.get("e").and_then(|e| e.as_str()).unwrap_or("");
214        match event_type {
215            "ORDER_TRADE_UPDATE" => serde_json::from_value::<OrderUpdateEvent>(v)
216                .map(|e| UserDataEvent::OrderUpdate(Box::new(e)))
217                .map_err(serde::de::Error::custom),
218            "ACCOUNT_UPDATE" => serde_json::from_value::<AccountUpdateEvent>(v)
219                .map(|e| UserDataEvent::AccountUpdate(Box::new(e)))
220                .map_err(serde::de::Error::custom),
221            "ACCOUNT_CONFIG_UPDATE" => serde_json::from_value::<AccountConfigUpdateEvent>(v)
222                .map(UserDataEvent::AccountConfigUpdate)
223                .map_err(serde::de::Error::custom),
224            "MARGIN_CALL" => serde_json::from_value::<MarginCallEvent>(v)
225                .map(UserDataEvent::MarginCall)
226                .map_err(serde::de::Error::custom),
227            "listenKeyExpired" => serde_json::from_value::<ListenKeyExpiredEvent>(v)
228                .map(UserDataEvent::ListenKeyExpired)
229                .map_err(serde::de::Error::custom),
230            "tradepro" => serde_json::from_value::<TradeProEvent>(v)
231                .map(UserDataEvent::TradePro)
232                .map_err(serde::de::Error::custom),
233            _ => {
234                tracing::warn!(event_type = event_type, "Unknown user data event type");
235                Ok(UserDataEvent::Unknown(v))
236            }
237        }
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244
245    #[test]
246    fn order_update_event_deserializes() {
247        let json = r#"{
248            "e": "ORDER_TRADE_UPDATE",
249            "E": 1625184000000,
250            "T": 1625184000000,
251            "o": {
252                "s": "BTCUSDT",
253                "c": "client-id-1",
254                "S": "BUY",
255                "o": "LIMIT",
256                "f": "GTC",
257                "q": "0.001",
258                "p": "30000",
259                "ap": "0",
260                "sp": "0",
261                "x": "NEW",
262                "X": "NEW",
263                "i": 123456789,
264                "l": "0",
265                "z": "0",
266                "L": "0",
267                "t": -1,
268                "T": 1625184000000,
269                "ps": "BOTH",
270                "rp": "0"
271            }
272        }"#;
273        let event: UserDataEvent = serde_json::from_str(json).unwrap();
274        assert!(matches!(event, UserDataEvent::OrderUpdate(ref e) if e.order.symbol == "BTCUSDT"));
275    }
276
277    #[test]
278    fn account_update_event_deserializes() {
279        let json = r#"{
280            "e": "ACCOUNT_UPDATE",
281            "E": 1625184000000,
282            "T": 1625184000000,
283            "a": {
284                "m": "DEPOSIT",
285                "B": [{"a": "USDT", "wb": "1000.00", "cw": "1000.00", "bc": "100.00"}],
286                "P": []
287            }
288        }"#;
289        let event: UserDataEvent = serde_json::from_str(json).unwrap();
290        assert!(matches!(event, UserDataEvent::AccountUpdate(_)));
291        if let UserDataEvent::AccountUpdate(e) = event {
292            assert_eq!(e.account.balances[0].asset, "USDT");
293            assert_eq!(e.account.balances[0].wallet_balance, "1000.00");
294            assert!(e.account.positions.is_empty());
295        }
296    }
297
298    #[test]
299    fn unknown_event_type_maps_to_unknown() {
300        let json = r#"{"e":"NEW_FUTURE_EVENT","x":"data"}"#;
301        let event: UserDataEvent = serde_json::from_str(json).unwrap();
302        assert!(matches!(event, UserDataEvent::Unknown(_)));
303    }
304
305    #[test]
306    fn listen_key_expired_deserializes() {
307        let json = r#"{"e":"listenKeyExpired","E":1576653824250}"#;
308        let event: UserDataEvent = serde_json::from_str(json).unwrap();
309        assert!(matches!(event, UserDataEvent::ListenKeyExpired(_)));
310        if let UserDataEvent::ListenKeyExpired(e) = event {
311            assert_eq!(e.event_type, "listenKeyExpired");
312            assert_eq!(e.event_time, 1576653824250);
313        }
314    }
315
316    #[test]
317    fn account_config_update_leverage_deserializes() {
318        let json = r#"{
319            "e": "ACCOUNT_CONFIG_UPDATE",
320            "E": 1625184000000,
321            "T": 1625184000000,
322            "ac": { "s": "BTCUSDT", "l": 25 }
323        }"#;
324        let event: UserDataEvent = serde_json::from_str(json).unwrap();
325        assert!(matches!(event, UserDataEvent::AccountConfigUpdate(_)));
326        if let UserDataEvent::AccountConfigUpdate(e) = event {
327            assert!(e.leverage_change.is_some());
328            assert!(e.multi_assets_margin.is_none());
329        }
330    }
331
332    #[test]
333    fn account_config_update_multi_assets_deserializes() {
334        let json = r#"{
335            "e": "ACCOUNT_CONFIG_UPDATE",
336            "E": 1625184000000,
337            "T": 1625184000000,
338            "ai": { "j": true }
339        }"#;
340        let event: UserDataEvent = serde_json::from_str(json).unwrap();
341        assert!(matches!(event, UserDataEvent::AccountConfigUpdate(_)));
342        if let UserDataEvent::AccountConfigUpdate(e) = event {
343            assert!(e.multi_assets_margin.is_some());
344            assert!(e.leverage_change.is_none());
345        }
346    }
347}