use rust_decimal::Decimal;
use serde::Deserialize;
use crate::{
Timestamp,
margin::{OrderSide, OrderStatus, OrderType, TimeInForce},
ws::ReceivedMessage,
};
#[derive(PartialEq, Deserialize, Debug)]
#[serde(tag = "e")]
#[allow(clippy::large_enum_variant)]
pub enum IncomingMessage {
#[serde(rename = "outboundAccountPosition")]
OutboundAccountPosition(OutboundAccountPositionEvent),
#[serde(rename = "balanceUpdate")]
BalanceUpdate(BalanceUpdateEvent),
#[serde(rename = "executionReport")]
ExecutionReport(ExecutionReportEvent),
}
impl ReceivedMessage for IncomingMessage {
fn server_shutdown_event_time(&self) -> Option<u64> {
None
}
}
#[derive(PartialEq, Deserialize, Debug)]
pub struct OutboundAccountPositionEvent {
#[serde(rename = "E")]
pub event_time: Timestamp,
#[serde(rename = "u")]
pub last_account_update: Timestamp,
#[serde(rename = "B")]
pub balances: Vec<AccountBalance>,
}
#[derive(PartialEq, Deserialize, Debug)]
pub struct AccountBalance {
#[serde(rename = "a")]
pub asset: String,
#[serde(rename = "f")]
pub free: Decimal,
#[serde(rename = "l")]
pub locked: Decimal,
}
#[derive(PartialEq, Deserialize, Debug)]
pub struct BalanceUpdateEvent {
#[serde(rename = "E")]
pub event_time: Timestamp,
#[serde(rename = "a")]
pub asset: String,
#[serde(rename = "d")]
pub delta: Decimal,
#[serde(rename = "T")]
pub clear_time: Timestamp,
}
#[derive(PartialEq, Deserialize, Debug)]
pub struct ExecutionReportEvent {
#[serde(rename = "E")]
pub event_time: Timestamp,
#[serde(rename = "s")]
pub symbol: String,
#[serde(rename = "c")]
pub client_order_id: String,
#[serde(rename = "S")]
pub side: OrderSide,
#[serde(rename = "o")]
pub order_type: OrderType,
#[serde(rename = "f")]
pub time_in_force: TimeInForce,
#[serde(rename = "q")]
pub orig_qty: Decimal,
#[serde(rename = "p")]
pub price: Decimal,
#[serde(rename = "P")]
pub stop_price: Decimal,
#[serde(rename = "F")]
pub iceberg_qty: Decimal,
#[serde(rename = "C")]
pub orig_client_order_id: String,
#[serde(rename = "x")]
pub execution_type: String,
#[serde(rename = "X")]
pub order_status: OrderStatus,
#[serde(rename = "r")]
pub reject_reason: String,
#[serde(rename = "i")]
pub order_id: i64,
#[serde(rename = "l")]
pub last_executed_qty: Decimal,
#[serde(rename = "z")]
pub cumulative_filled_qty: Decimal,
#[serde(rename = "L")]
pub last_executed_price: Decimal,
#[serde(rename = "n")]
pub commission: Decimal,
#[serde(rename = "N")]
pub commission_asset: Option<String>,
#[serde(rename = "T")]
pub transaction_time: Timestamp,
#[serde(rename = "t")]
pub trade_id: i64,
#[serde(rename = "w")]
pub is_on_book: bool,
#[serde(rename = "m")]
pub is_maker: bool,
#[serde(rename = "O")]
pub order_creation_time: Timestamp,
#[serde(rename = "Z")]
pub cumulative_quote_qty: Decimal,
#[serde(rename = "Y")]
pub last_quote_qty: Decimal,
#[serde(rename = "Q")]
pub quote_order_qty: Decimal,
#[serde(rename = "isolatedSymbol", default)]
pub isolated_symbol: Option<String>,
}
#[cfg(test)]
mod tests {
use rust_decimal::dec;
use super::*;
use crate::serde::deserialize_json;
#[test]
fn deserialize_outbound_account_position() {
let json = r#"{
"e": "outboundAccountPosition",
"E": 1564034571105,
"u": 1564034571073,
"B": [
{"a": "ETH", "f": "10000.000000", "l": "0.000000"},
{"a": "USDT", "f": "1000.50", "l": "100.00"}
]
}"#;
let parsed: IncomingMessage = deserialize_json(json).unwrap();
let IncomingMessage::OutboundAccountPosition(event) = parsed else {
panic!("expected OutboundAccountPosition, got {parsed:?}");
};
assert_eq!(event.event_time, 1564034571105);
assert_eq!(event.last_account_update, 1564034571073);
assert_eq!(event.balances.len(), 2);
assert_eq!(event.balances[1].asset, "USDT");
assert_eq!(event.balances[1].free, dec!(1000.50));
assert_eq!(event.balances[1].locked, dec!(100.00));
}
#[test]
fn deserialize_balance_update() {
let json = r#"{
"e": "balanceUpdate",
"E": 1573200697110,
"a": "BTC",
"d": "100.00000000",
"T": 1573200697068
}"#;
let parsed: IncomingMessage = deserialize_json(json).unwrap();
let IncomingMessage::BalanceUpdate(event) = parsed else {
panic!("expected BalanceUpdate, got {parsed:?}");
};
assert_eq!(event.asset, "BTC");
assert_eq!(event.delta, dec!(100.0));
}
#[test]
fn deserialize_execution_report_cross_margin_omits_isolated_symbol() {
let json = r#"{
"e": "executionReport",
"E": 1499405658658,
"s": "ETHBTC",
"c": "myOrderId",
"S": "BUY",
"o": "LIMIT",
"f": "GTC",
"q": "1.00000000",
"p": "0.10264410",
"P": "0.00000000",
"F": "0.00000000",
"C": "",
"x": "NEW",
"X": "NEW",
"r": "NONE",
"i": 4293153,
"l": "0.00000000",
"z": "0.00000000",
"L": "0.00000000",
"n": "0",
"N": null,
"T": 1499405658657,
"t": -1,
"w": true,
"m": false,
"O": 1499405658657,
"Z": "0.00000000",
"Y": "0.00000000",
"Q": "0.00000000"
}"#;
let parsed: IncomingMessage = deserialize_json(json).unwrap();
let IncomingMessage::ExecutionReport(event) = parsed else {
panic!("expected ExecutionReport, got {parsed:?}");
};
assert_eq!(event.symbol, "ETHBTC");
assert_eq!(event.order_id, 4293153);
assert_eq!(event.order_status, OrderStatus::New);
assert_eq!(event.execution_type, "NEW");
assert_eq!(event.commission_asset, None);
assert_eq!(event.isolated_symbol, None);
}
#[test]
fn deserialize_execution_report_isolated_margin_carries_isolated_symbol() {
let json = r#"{
"e": "executionReport",
"E": 1499405658658,
"s": "ETHBTC",
"c": "myOrderId",
"S": "SELL",
"o": "MARKET",
"f": "IOC",
"q": "1.00000000",
"p": "0.00000000",
"P": "0.00000000",
"F": "0.00000000",
"C": "",
"x": "TRADE",
"X": "FILLED",
"r": "NONE",
"i": 4293153,
"l": "1.00000000",
"z": "1.00000000",
"L": "0.10264410",
"n": "0.00010264",
"N": "BTC",
"T": 1499405658657,
"t": 12345,
"w": false,
"m": true,
"O": 1499405658657,
"Z": "0.10264410",
"Y": "0.10264410",
"Q": "0.00000000",
"isolatedSymbol": "ETHBTC"
}"#;
let parsed: IncomingMessage = deserialize_json(json).unwrap();
let IncomingMessage::ExecutionReport(event) = parsed else {
panic!("expected ExecutionReport, got {parsed:?}");
};
assert_eq!(event.isolated_symbol.as_deref(), Some("ETHBTC"));
assert_eq!(event.commission_asset.as_deref(), Some("BTC"));
assert!(event.is_maker);
}
}