use serde::Deserialize;
#[derive(Debug)]
pub enum UserDataEvent {
OrderUpdate(Box<OrderUpdateEvent>),
AccountUpdate(Box<AccountUpdateEvent>),
AccountConfigUpdate(AccountConfigUpdateEvent),
MarginCall(MarginCallEvent),
ListenKeyExpired(ListenKeyExpiredEvent),
TradePro(TradeProEvent),
Unknown(serde_json::Value),
}
#[derive(Debug, Deserialize)]
pub struct OrderUpdateEvent {
#[serde(rename = "e")] pub event_type: String,
#[serde(rename = "E")] pub event_time: u64,
#[serde(rename = "T")] pub transaction_time: u64,
#[serde(rename = "o")] pub order: OrderUpdateData,
}
#[derive(Debug, Deserialize)]
pub struct OrderUpdateData {
#[serde(rename = "s")] pub symbol: String,
#[serde(rename = "c")] pub client_order_id: String,
#[serde(rename = "S")] pub side: String,
#[serde(rename = "o")] pub order_type: String,
#[serde(rename = "f")] pub time_in_force: String,
#[serde(rename = "q")] pub quantity: String,
#[serde(rename = "p")] pub price: String,
#[serde(rename = "ap")] pub avg_price: String,
#[serde(rename = "sp")] pub stop_price: String,
#[serde(rename = "x")] pub execution_type: String,
#[serde(rename = "X")] pub order_status: String,
#[serde(rename = "i")] pub order_id: i64,
#[serde(rename = "l")] pub last_filled_qty: String,
#[serde(rename = "z")] pub cumulative_filled_qty: String,
#[serde(rename = "L")] pub last_filled_price: String,
#[serde(rename = "N", default)] pub commission_asset: Option<String>,
#[serde(rename = "n", default)] pub commission: Option<String>,
#[serde(rename = "t")] pub trade_id: i64,
#[serde(rename = "b", default)] pub bids_notional: String,
#[serde(rename = "a", default)] pub ask_notional: String,
#[serde(rename = "m", default)] pub is_maker: bool,
#[serde(rename = "R", default)] pub is_reduce_only: bool,
#[serde(rename = "wt", default)] pub working_type: String,
#[serde(rename = "ot", default)] pub orig_order_type: String,
#[serde(rename = "ps")] pub position_side: String,
#[serde(rename = "cp", default)] pub close_position: bool,
#[serde(rename = "AP", default)] pub activation_price: Option<String>,
#[serde(rename = "cr", default)] pub callback_rate: Option<String>,
#[serde(rename = "rp")] pub realized_profit: String,
#[serde(rename = "T")] pub order_trade_time: u64,
}
#[derive(Debug, Deserialize)]
pub struct AccountUpdateEvent {
#[serde(rename = "e")] pub event_type: String,
#[serde(rename = "E")] pub event_time: u64,
#[serde(rename = "T")] pub transaction_time: u64,
#[serde(rename = "a")] pub account: AccountUpdateData,
}
#[derive(Debug, Deserialize)]
pub struct AccountUpdateData {
#[serde(rename = "m")] pub event_reason: String,
#[serde(rename = "B")] pub balances: Vec<BalanceDelta>,
#[serde(rename = "P")] pub positions: Vec<PositionDelta>,
}
#[derive(Debug, Deserialize)]
pub struct BalanceDelta {
#[serde(rename = "a")] pub asset: String,
#[serde(rename = "wb")] pub wallet_balance: String,
#[serde(rename = "cw")] pub cross_wallet_balance: String,
#[serde(rename = "bc")] pub balance_change: String,
}
#[derive(Debug, Deserialize)]
pub struct PositionDelta {
#[serde(rename = "s")] pub symbol: String,
#[serde(rename = "pa")] pub position_amount: String,
#[serde(rename = "ep")] pub entry_price: String,
#[serde(rename = "cr")] pub accumulated_realized: String,
#[serde(rename = "up")] pub unrealized_pnl: String,
#[serde(rename = "mt")] pub margin_type: String,
#[serde(rename = "iw")] pub isolated_wallet: String,
#[serde(rename = "ps")] pub position_side: String,
}
#[derive(Debug, Deserialize)]
pub struct AccountConfigUpdateEvent {
#[serde(rename = "e")] pub event_type: String,
#[serde(rename = "E")] pub event_time: u64,
#[serde(rename = "T")] pub transaction_time: u64,
#[serde(rename = "ac")] pub leverage_change: Option<LeverageChangeData>,
#[serde(rename = "ai")] pub multi_assets_margin: Option<MultiAssetsModeData>,
}
#[derive(Debug, Deserialize)]
pub struct LeverageChangeData {
#[serde(rename = "s")] pub symbol: String,
#[serde(rename = "l")] pub leverage: u32,
}
#[derive(Debug, Deserialize)]
pub struct MultiAssetsModeData {
#[serde(rename = "j")] pub is_enabled: bool,
#[serde(rename = "f", default)] pub fee_deduction: Option<bool>,
#[serde(rename = "d", default)] pub position_mode_dual: Option<bool>,
}
#[derive(Debug, Deserialize)]
pub struct MarginCallEvent {
#[serde(rename = "e")] pub event_type: String,
#[serde(rename = "E")] pub event_time: u64,
#[serde(rename = "cw", default)] pub cross_wallet_balance: Option<String>,
#[serde(rename = "p")] pub positions: Vec<MarginCallPosition>,
}
#[derive(Debug, Deserialize)]
pub struct MarginCallPosition {
#[serde(rename = "s")] pub symbol: String,
#[serde(rename = "ps")] pub position_side: String,
#[serde(rename = "pa")] pub position_amount: String,
#[serde(rename = "mt")] pub margin_type: String,
#[serde(rename = "iw")] pub isolated_wallet: String,
#[serde(rename = "mp")] pub mark_price: String,
#[serde(rename = "up")] pub unrealized_pnl: String,
#[serde(rename = "mm")] pub maintenance_margin_required: String,
}
#[derive(Debug, Deserialize)]
pub struct TradeProEvent {
#[serde(rename = "e")] pub event_type: String,
#[serde(rename = "E")] pub event_time: u64,
#[serde(rename = "T")] pub transaction_time: u64,
#[serde(rename = "s")] pub symbol: String,
#[serde(rename = "t")] pub trade_id: i64,
#[serde(rename = "p")] pub price: String,
#[serde(rename = "q")] pub quantity: String,
#[serde(rename = "h")] pub tx_hash: String,
#[serde(rename = "m")] pub participants: Vec<String>,
}
#[derive(Debug, Deserialize)]
pub struct ListenKeyExpiredEvent {
#[serde(rename = "e")] pub event_type: String,
#[serde(rename = "E")] pub event_time: u64,
}
impl<'de> serde::Deserialize<'de> for UserDataEvent {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let v = serde_json::Value::deserialize(deserializer)?;
let event_type = v.get("e").and_then(|e| e.as_str()).unwrap_or("");
match event_type {
"ORDER_TRADE_UPDATE" => serde_json::from_value::<OrderUpdateEvent>(v)
.map(|e| UserDataEvent::OrderUpdate(Box::new(e)))
.map_err(serde::de::Error::custom),
"ACCOUNT_UPDATE" => serde_json::from_value::<AccountUpdateEvent>(v)
.map(|e| UserDataEvent::AccountUpdate(Box::new(e)))
.map_err(serde::de::Error::custom),
"ACCOUNT_CONFIG_UPDATE" => serde_json::from_value::<AccountConfigUpdateEvent>(v)
.map(UserDataEvent::AccountConfigUpdate)
.map_err(serde::de::Error::custom),
"MARGIN_CALL" => serde_json::from_value::<MarginCallEvent>(v)
.map(UserDataEvent::MarginCall)
.map_err(serde::de::Error::custom),
"listenKeyExpired" => serde_json::from_value::<ListenKeyExpiredEvent>(v)
.map(UserDataEvent::ListenKeyExpired)
.map_err(serde::de::Error::custom),
"tradepro" => serde_json::from_value::<TradeProEvent>(v)
.map(UserDataEvent::TradePro)
.map_err(serde::de::Error::custom),
_ => {
tracing::warn!(event_type = event_type, "Unknown user data event type");
Ok(UserDataEvent::Unknown(v))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn order_update_event_deserializes() {
let json = r#"{
"e": "ORDER_TRADE_UPDATE",
"E": 1625184000000,
"T": 1625184000000,
"o": {
"s": "BTCUSDT",
"c": "client-id-1",
"S": "BUY",
"o": "LIMIT",
"f": "GTC",
"q": "0.001",
"p": "30000",
"ap": "0",
"sp": "0",
"x": "NEW",
"X": "NEW",
"i": 123456789,
"l": "0",
"z": "0",
"L": "0",
"t": -1,
"T": 1625184000000,
"ps": "BOTH",
"rp": "0"
}
}"#;
let event: UserDataEvent = serde_json::from_str(json).unwrap();
assert!(matches!(event, UserDataEvent::OrderUpdate(ref e) if e.order.symbol == "BTCUSDT"));
}
#[test]
fn account_update_event_deserializes() {
let json = r#"{
"e": "ACCOUNT_UPDATE",
"E": 1625184000000,
"T": 1625184000000,
"a": {
"m": "DEPOSIT",
"B": [{"a": "USDT", "wb": "1000.00", "cw": "1000.00", "bc": "100.00"}],
"P": []
}
}"#;
let event: UserDataEvent = serde_json::from_str(json).unwrap();
assert!(matches!(event, UserDataEvent::AccountUpdate(_)));
if let UserDataEvent::AccountUpdate(e) = event {
assert_eq!(e.account.balances[0].asset, "USDT");
assert_eq!(e.account.balances[0].wallet_balance, "1000.00");
assert!(e.account.positions.is_empty());
}
}
#[test]
fn unknown_event_type_maps_to_unknown() {
let json = r#"{"e":"NEW_FUTURE_EVENT","x":"data"}"#;
let event: UserDataEvent = serde_json::from_str(json).unwrap();
assert!(matches!(event, UserDataEvent::Unknown(_)));
}
#[test]
fn listen_key_expired_deserializes() {
let json = r#"{"e":"listenKeyExpired","E":1576653824250}"#;
let event: UserDataEvent = serde_json::from_str(json).unwrap();
assert!(matches!(event, UserDataEvent::ListenKeyExpired(_)));
if let UserDataEvent::ListenKeyExpired(e) = event {
assert_eq!(e.event_type, "listenKeyExpired");
assert_eq!(e.event_time, 1576653824250);
}
}
#[test]
fn account_config_update_leverage_deserializes() {
let json = r#"{
"e": "ACCOUNT_CONFIG_UPDATE",
"E": 1625184000000,
"T": 1625184000000,
"ac": { "s": "BTCUSDT", "l": 25 }
}"#;
let event: UserDataEvent = serde_json::from_str(json).unwrap();
assert!(matches!(event, UserDataEvent::AccountConfigUpdate(_)));
if let UserDataEvent::AccountConfigUpdate(e) = event {
assert!(e.leverage_change.is_some());
assert!(e.multi_assets_margin.is_none());
}
}
#[test]
fn account_config_update_multi_assets_deserializes() {
let json = r#"{
"e": "ACCOUNT_CONFIG_UPDATE",
"E": 1625184000000,
"T": 1625184000000,
"ai": { "j": true }
}"#;
let event: UserDataEvent = serde_json::from_str(json).unwrap();
assert!(matches!(event, UserDataEvent::AccountConfigUpdate(_)));
if let UserDataEvent::AccountConfigUpdate(e) = event {
assert!(e.multi_assets_margin.is_some());
assert!(e.leverage_change.is_none());
}
}
}