1use rust_decimal::Decimal;
9use serde::Deserialize;
10
11use crate::{
12 Timestamp,
13 margin::{OrderSide, OrderStatus, OrderType, TimeInForce},
14 ws::ReceivedMessage,
15};
16
17#[derive(PartialEq, Deserialize, Debug)]
20#[serde(tag = "e")]
21#[allow(clippy::large_enum_variant)]
22pub enum IncomingMessage {
23 #[serde(rename = "outboundAccountPosition")]
24 OutboundAccountPosition(OutboundAccountPositionEvent),
25 #[serde(rename = "balanceUpdate")]
26 BalanceUpdate(BalanceUpdateEvent),
27 #[serde(rename = "executionReport")]
28 ExecutionReport(ExecutionReportEvent),
29}
30
31impl ReceivedMessage for IncomingMessage {
32 fn server_shutdown_event_time(&self) -> Option<u64> {
33 None
38 }
39}
40
41#[derive(PartialEq, Deserialize, Debug)]
46pub struct OutboundAccountPositionEvent {
47 #[serde(rename = "E")]
49 pub event_time: Timestamp,
50 #[serde(rename = "u")]
52 pub last_account_update: Timestamp,
53 #[serde(rename = "B")]
54 pub balances: Vec<AccountBalance>,
55}
56
57#[derive(PartialEq, Deserialize, Debug)]
58pub struct AccountBalance {
59 #[serde(rename = "a")]
60 pub asset: String,
61 #[serde(rename = "f")]
62 pub free: Decimal,
63 #[serde(rename = "l")]
64 pub locked: Decimal,
65}
66
67#[derive(PartialEq, Deserialize, Debug)]
69pub struct BalanceUpdateEvent {
70 #[serde(rename = "E")]
71 pub event_time: Timestamp,
72 #[serde(rename = "a")]
73 pub asset: String,
74 #[serde(rename = "d")]
76 pub delta: Decimal,
77 #[serde(rename = "T")]
79 pub clear_time: Timestamp,
80}
81
82#[derive(PartialEq, Deserialize, Debug)]
88pub struct ExecutionReportEvent {
89 #[serde(rename = "E")]
90 pub event_time: Timestamp,
91 #[serde(rename = "s")]
92 pub symbol: String,
93 #[serde(rename = "c")]
95 pub client_order_id: String,
96 #[serde(rename = "S")]
97 pub side: OrderSide,
98 #[serde(rename = "o")]
99 pub order_type: OrderType,
100 #[serde(rename = "f")]
101 pub time_in_force: TimeInForce,
102 #[serde(rename = "q")]
104 pub orig_qty: Decimal,
105 #[serde(rename = "p")]
106 pub price: Decimal,
107 #[serde(rename = "P")]
109 pub stop_price: Decimal,
110 #[serde(rename = "F")]
112 pub iceberg_qty: Decimal,
113 #[serde(rename = "C")]
115 pub orig_client_order_id: String,
116 #[serde(rename = "x")]
120 pub execution_type: String,
121 #[serde(rename = "X")]
122 pub order_status: OrderStatus,
123 #[serde(rename = "r")]
125 pub reject_reason: String,
126 #[serde(rename = "i")]
127 pub order_id: i64,
128 #[serde(rename = "l")]
130 pub last_executed_qty: Decimal,
131 #[serde(rename = "z")]
133 pub cumulative_filled_qty: Decimal,
134 #[serde(rename = "L")]
136 pub last_executed_price: Decimal,
137 #[serde(rename = "n")]
139 pub commission: Decimal,
140 #[serde(rename = "N")]
142 pub commission_asset: Option<String>,
143 #[serde(rename = "T")]
144 pub transaction_time: Timestamp,
145 #[serde(rename = "t")]
147 pub trade_id: i64,
148 #[serde(rename = "w")]
150 pub is_on_book: bool,
151 #[serde(rename = "m")]
153 pub is_maker: bool,
154 #[serde(rename = "O")]
155 pub order_creation_time: Timestamp,
156 #[serde(rename = "Z")]
158 pub cumulative_quote_qty: Decimal,
159 #[serde(rename = "Y")]
161 pub last_quote_qty: Decimal,
162 #[serde(rename = "Q")]
164 pub quote_order_qty: Decimal,
165 #[serde(rename = "isolatedSymbol", default)]
168 pub isolated_symbol: Option<String>,
169}
170
171#[cfg(test)]
172mod tests {
173 use rust_decimal::dec;
174
175 use super::*;
176 use crate::serde::deserialize_json;
177
178 #[test]
179 fn deserialize_outbound_account_position() {
180 let json = r#"{
181 "e": "outboundAccountPosition",
182 "E": 1564034571105,
183 "u": 1564034571073,
184 "B": [
185 {"a": "ETH", "f": "10000.000000", "l": "0.000000"},
186 {"a": "USDT", "f": "1000.50", "l": "100.00"}
187 ]
188 }"#;
189 let parsed: IncomingMessage = deserialize_json(json).unwrap();
190 let IncomingMessage::OutboundAccountPosition(event) = parsed else {
191 panic!("expected OutboundAccountPosition, got {parsed:?}");
192 };
193 assert_eq!(event.event_time, 1564034571105);
194 assert_eq!(event.last_account_update, 1564034571073);
195 assert_eq!(event.balances.len(), 2);
196 assert_eq!(event.balances[1].asset, "USDT");
197 assert_eq!(event.balances[1].free, dec!(1000.50));
198 assert_eq!(event.balances[1].locked, dec!(100.00));
199 }
200
201 #[test]
202 fn deserialize_balance_update() {
203 let json = r#"{
204 "e": "balanceUpdate",
205 "E": 1573200697110,
206 "a": "BTC",
207 "d": "100.00000000",
208 "T": 1573200697068
209 }"#;
210 let parsed: IncomingMessage = deserialize_json(json).unwrap();
211 let IncomingMessage::BalanceUpdate(event) = parsed else {
212 panic!("expected BalanceUpdate, got {parsed:?}");
213 };
214 assert_eq!(event.asset, "BTC");
215 assert_eq!(event.delta, dec!(100.0));
216 }
217
218 #[test]
219 fn deserialize_execution_report_cross_margin_omits_isolated_symbol() {
220 let json = r#"{
221 "e": "executionReport",
222 "E": 1499405658658,
223 "s": "ETHBTC",
224 "c": "myOrderId",
225 "S": "BUY",
226 "o": "LIMIT",
227 "f": "GTC",
228 "q": "1.00000000",
229 "p": "0.10264410",
230 "P": "0.00000000",
231 "F": "0.00000000",
232 "C": "",
233 "x": "NEW",
234 "X": "NEW",
235 "r": "NONE",
236 "i": 4293153,
237 "l": "0.00000000",
238 "z": "0.00000000",
239 "L": "0.00000000",
240 "n": "0",
241 "N": null,
242 "T": 1499405658657,
243 "t": -1,
244 "w": true,
245 "m": false,
246 "O": 1499405658657,
247 "Z": "0.00000000",
248 "Y": "0.00000000",
249 "Q": "0.00000000"
250 }"#;
251 let parsed: IncomingMessage = deserialize_json(json).unwrap();
252 let IncomingMessage::ExecutionReport(event) = parsed else {
253 panic!("expected ExecutionReport, got {parsed:?}");
254 };
255 assert_eq!(event.symbol, "ETHBTC");
256 assert_eq!(event.order_id, 4293153);
257 assert_eq!(event.order_status, OrderStatus::New);
258 assert_eq!(event.execution_type, "NEW");
259 assert_eq!(event.commission_asset, None);
260 assert_eq!(event.isolated_symbol, None);
261 }
262
263 #[test]
264 fn deserialize_execution_report_isolated_margin_carries_isolated_symbol() {
265 let json = r#"{
267 "e": "executionReport",
268 "E": 1499405658658,
269 "s": "ETHBTC",
270 "c": "myOrderId",
271 "S": "SELL",
272 "o": "MARKET",
273 "f": "IOC",
274 "q": "1.00000000",
275 "p": "0.00000000",
276 "P": "0.00000000",
277 "F": "0.00000000",
278 "C": "",
279 "x": "TRADE",
280 "X": "FILLED",
281 "r": "NONE",
282 "i": 4293153,
283 "l": "1.00000000",
284 "z": "1.00000000",
285 "L": "0.10264410",
286 "n": "0.00010264",
287 "N": "BTC",
288 "T": 1499405658657,
289 "t": 12345,
290 "w": false,
291 "m": true,
292 "O": 1499405658657,
293 "Z": "0.10264410",
294 "Y": "0.10264410",
295 "Q": "0.00000000",
296 "isolatedSymbol": "ETHBTC"
297 }"#;
298 let parsed: IncomingMessage = deserialize_json(json).unwrap();
299 let IncomingMessage::ExecutionReport(event) = parsed else {
300 panic!("expected ExecutionReport, got {parsed:?}");
301 };
302 assert_eq!(event.isolated_symbol.as_deref(), Some("ETHBTC"));
303 assert_eq!(event.commission_asset.as_deref(), Some("BTC"));
304 assert!(event.is_maker);
305 }
306}