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/// - Unknown types → `Unknown` (logged as warning, never panics)
16///
17/// Large variants (`OrderUpdate`, `AccountUpdate`) are boxed to reduce stack size.
18#[derive(Debug)]
19pub enum UserDataEvent {
20    /// Order trade update event (boxed to reduce enum stack size — US-016 clippy fix).
21    OrderUpdate(Box<OrderUpdateEvent>),
22    /// Account balance/position update event (boxed to reduce enum stack size — US-016 clippy fix).
23    AccountUpdate(Box<AccountUpdateEvent>),
24    /// Account configuration update event (leverage or multi-assets margin mode change).
25    AccountConfigUpdate(AccountConfigUpdateEvent),
26    /// Unknown event type — captured as raw JSON for forward compatibility.
27    Unknown(serde_json::Value),
28}
29
30/// Order trade update event — dispatched when `"e": "ORDER_TRADE_UPDATE"`.
31#[derive(Debug, Deserialize)]
32pub struct OrderUpdateEvent {
33    #[serde(rename = "E")] pub event_time: u64,
34    #[serde(rename = "T")] pub transaction_time: u64,
35    #[serde(rename = "o")] pub order: OrderUpdateData,
36}
37
38/// Order details within an `OrderUpdateEvent`.
39#[derive(Debug, Deserialize)]
40pub struct OrderUpdateData {
41    #[serde(rename = "s")] pub symbol: String,
42    #[serde(rename = "c")] pub client_order_id: String,
43    #[serde(rename = "S")] pub side: String,
44    #[serde(rename = "o")] pub order_type: String,
45    #[serde(rename = "f")] pub time_in_force: String,
46    #[serde(rename = "q")] pub quantity: String,
47    #[serde(rename = "p")] pub price: String,
48    #[serde(rename = "ap")] pub avg_price: String,
49    #[serde(rename = "sp")] pub stop_price: String,
50    #[serde(rename = "x")] pub execution_type: String,
51    #[serde(rename = "X")] pub order_status: String,
52    #[serde(rename = "i")] pub order_id: i64,
53    #[serde(rename = "l")] pub last_filled_qty: String,
54    #[serde(rename = "z")] pub cumulative_filled_qty: String,
55    #[serde(rename = "L")] pub last_filled_price: String,
56    #[serde(rename = "t")] pub trade_id: i64,
57    #[serde(rename = "T")] pub order_trade_time: u64,
58    #[serde(rename = "ps")] pub position_side: String,
59    #[serde(rename = "rp")] pub realized_profit: String,
60}
61
62/// Account update event — dispatched when `"e": "ACCOUNT_UPDATE"`.
63#[derive(Debug, Deserialize)]
64pub struct AccountUpdateEvent {
65    #[serde(rename = "E")] pub event_time: u64,
66    #[serde(rename = "T")] pub transaction_time: u64,
67    #[serde(rename = "a")] pub account: AccountUpdateData,
68}
69
70/// Account data within an `AccountUpdateEvent`.
71#[derive(Debug, Deserialize)]
72pub struct AccountUpdateData {
73    #[serde(rename = "m")] pub event_reason: String,
74    #[serde(rename = "B")] pub balances: Vec<BalanceDelta>,
75    #[serde(rename = "P")] pub positions: Vec<PositionDelta>,
76}
77
78/// Balance change within an `AccountUpdateData`.
79#[derive(Debug, Deserialize)]
80pub struct BalanceDelta {
81    #[serde(rename = "a")] pub asset: String,
82    #[serde(rename = "wb")] pub wallet_balance: String,
83    #[serde(rename = "cw")] pub cross_wallet_balance: String,
84    #[serde(rename = "bc")] pub balance_change: String,
85}
86
87/// Position change within an `AccountUpdateData`.
88#[derive(Debug, Deserialize)]
89pub struct PositionDelta {
90    #[serde(rename = "s")] pub symbol: String,
91    #[serde(rename = "pa")] pub position_amount: String,
92    #[serde(rename = "ep")] pub entry_price: String,
93    #[serde(rename = "cr")] pub accumulated_realized: String,
94    #[serde(rename = "up")] pub unrealized_pnl: String,
95    #[serde(rename = "mt")] pub margin_type: String,
96    #[serde(rename = "iw")] pub isolated_wallet: String,
97    #[serde(rename = "ps")] pub position_side: String,
98}
99
100/// Account configuration update event — dispatched when `"e": "ACCOUNT_CONFIG_UPDATE"`.
101///
102/// Contains either a leverage change (`ac`) or a multi-assets margin mode change (`ai`),
103/// but not both at the same time.
104#[derive(Debug, Deserialize)]
105pub struct AccountConfigUpdateEvent {
106    #[serde(rename = "E")] pub event_time: u64,
107    #[serde(rename = "T")] pub transaction_time: u64,
108    #[serde(rename = "ac")] pub leverage_change: Option<LeverageChangeData>,
109    #[serde(rename = "ai")] pub multi_assets_margin: Option<MultiAssetsModeData>,
110}
111
112/// Leverage change within an `AccountConfigUpdateEvent`.
113#[derive(Debug, Deserialize)]
114pub struct LeverageChangeData {
115    #[serde(rename = "s")] pub symbol: String,
116    #[serde(rename = "l")] pub leverage: u32,
117}
118
119/// Multi-assets margin mode change within an `AccountConfigUpdateEvent`.
120#[derive(Debug, Deserialize)]
121pub struct MultiAssetsModeData {
122    #[serde(rename = "j")] pub is_enabled: bool,
123}
124
125// US-013: Manual Deserialize impl for UserDataEvent — two-pass approach.
126// `#[serde(tag = "e")]` with an `#[serde(other)]` variant that captures a Value is
127// not supported by serde. We deserialize to a raw Value first, dispatch on "e", then
128// deserialize the full Value into the correct typed struct.
129impl<'de> serde::Deserialize<'de> for UserDataEvent {
130    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
131        let v = serde_json::Value::deserialize(deserializer)?;
132        let event_type = v.get("e").and_then(|e| e.as_str()).unwrap_or("");
133        match event_type {
134            "ORDER_TRADE_UPDATE" => serde_json::from_value::<OrderUpdateEvent>(v)
135                .map(|e| UserDataEvent::OrderUpdate(Box::new(e)))
136                .map_err(serde::de::Error::custom),
137            "ACCOUNT_UPDATE" => serde_json::from_value::<AccountUpdateEvent>(v)
138                .map(|e| UserDataEvent::AccountUpdate(Box::new(e)))
139                .map_err(serde::de::Error::custom),
140            "ACCOUNT_CONFIG_UPDATE" => serde_json::from_value::<AccountConfigUpdateEvent>(v)
141                .map(UserDataEvent::AccountConfigUpdate)
142                .map_err(serde::de::Error::custom),
143            _ => {
144                tracing::warn!(event_type = event_type, "Unknown user data event type");
145                Ok(UserDataEvent::Unknown(v))
146            }
147        }
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn order_update_event_deserializes() {
157        let json = r#"{
158            "e": "ORDER_TRADE_UPDATE",
159            "E": 1625184000000,
160            "T": 1625184000000,
161            "o": {
162                "s": "BTCUSDT",
163                "c": "client-id-1",
164                "S": "BUY",
165                "o": "LIMIT",
166                "f": "GTC",
167                "q": "0.001",
168                "p": "30000",
169                "ap": "0",
170                "sp": "0",
171                "x": "NEW",
172                "X": "NEW",
173                "i": 123456789,
174                "l": "0",
175                "z": "0",
176                "L": "0",
177                "t": -1,
178                "T": 1625184000000,
179                "ps": "BOTH",
180                "rp": "0"
181            }
182        }"#;
183        let event: UserDataEvent = serde_json::from_str(json).unwrap();
184        assert!(matches!(event, UserDataEvent::OrderUpdate(ref e) if e.order.symbol == "BTCUSDT"));
185    }
186
187    #[test]
188    fn account_update_event_deserializes() {
189        let json = r#"{
190            "e": "ACCOUNT_UPDATE",
191            "E": 1625184000000,
192            "T": 1625184000000,
193            "a": {
194                "m": "DEPOSIT",
195                "B": [{"a": "USDT", "wb": "1000.00", "cw": "1000.00", "bc": "100.00"}],
196                "P": []
197            }
198        }"#;
199        let event: UserDataEvent = serde_json::from_str(json).unwrap();
200        assert!(matches!(event, UserDataEvent::AccountUpdate(_)));
201        if let UserDataEvent::AccountUpdate(e) = event {
202            assert_eq!(e.account.balances[0].asset, "USDT");
203            assert_eq!(e.account.balances[0].wallet_balance, "1000.00");
204            assert!(e.account.positions.is_empty());
205        }
206    }
207
208    #[test]
209    fn unknown_event_type_maps_to_unknown() {
210        let json = r#"{"e":"NEW_FUTURE_EVENT","x":"data"}"#;
211        let event: UserDataEvent = serde_json::from_str(json).unwrap();
212        assert!(matches!(event, UserDataEvent::Unknown(_)));
213    }
214
215    #[test]
216    fn account_config_update_leverage_deserializes() {
217        let json = r#"{
218            "e": "ACCOUNT_CONFIG_UPDATE",
219            "E": 1625184000000,
220            "T": 1625184000000,
221            "ac": { "s": "BTCUSDT", "l": 25 }
222        }"#;
223        let event: UserDataEvent = serde_json::from_str(json).unwrap();
224        assert!(matches!(event, UserDataEvent::AccountConfigUpdate(_)));
225        if let UserDataEvent::AccountConfigUpdate(e) = event {
226            assert!(e.leverage_change.is_some());
227            assert!(e.multi_assets_margin.is_none());
228        }
229    }
230
231    #[test]
232    fn account_config_update_multi_assets_deserializes() {
233        let json = r#"{
234            "e": "ACCOUNT_CONFIG_UPDATE",
235            "E": 1625184000000,
236            "T": 1625184000000,
237            "ai": { "j": true }
238        }"#;
239        let event: UserDataEvent = serde_json::from_str(json).unwrap();
240        assert!(matches!(event, UserDataEvent::AccountConfigUpdate(_)));
241        if let UserDataEvent::AccountConfigUpdate(e) = event {
242            assert!(e.multi_assets_margin.is_some());
243            assert!(e.leverage_change.is_none());
244        }
245    }
246}