use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BalanceUpdate {
pub channel: String,
#[serde(rename = "type")]
pub update_type: String,
pub data: Vec<BalanceData>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BalanceData {
#[serde(flatten)]
pub balances: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderUpdate {
pub channel: String,
#[serde(rename = "type")]
pub update_type: String,
pub data: Vec<OrderData>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderData {
#[serde(rename = "order_id")]
pub order_id: String,
pub symbol: String,
pub side: String,
#[serde(rename = "order_type")]
pub order_type: String,
#[serde(rename = "limit_price")]
pub limit_price: Option<String>,
#[serde(rename = "order_qty")]
pub order_qty: String,
#[serde(rename = "filled_qty", default)]
pub filled_qty: String,
pub status: String,
#[serde(default)]
pub timestamp: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionUpdate {
pub channel: String,
#[serde(rename = "type")]
pub update_type: String,
pub data: Vec<ExecutionData>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionData {
#[serde(rename = "exec_id")]
pub exec_id: String,
#[serde(rename = "order_id")]
pub order_id: String,
pub symbol: String,
pub side: String,
#[serde(rename = "exec_qty")]
pub exec_qty: String,
#[serde(rename = "exec_price")]
pub exec_price: String,
#[serde(default)]
pub timestamp: String,
#[serde(default)]
pub liquidity: String,
}
impl BalanceUpdate {
pub fn get_balance(&self, asset: &str) -> Option<&String> {
self.data.first()?.balances.get(asset)
}
pub fn assets(&self) -> Vec<String> {
self.data
.first()
.map(|d| d.balances.keys().cloned().collect())
.unwrap_or_default()
}
}
impl OrderUpdate {
pub fn is_open(&self) -> bool {
self.update_type == "update" && self.data.iter().any(|o| o.status == "open")
}
pub fn is_closed(&self) -> bool {
self.data
.iter()
.any(|o| o.status == "closed" || o.status == "cancelled")
}
}
impl ExecutionUpdate {
pub fn total_value(&self) -> Option<f64> {
self.data.first().and_then(|e| {
let qty: f64 = e.exec_qty.parse().ok()?;
let price: f64 = e.exec_price.parse().ok()?;
Some(qty * price)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_balance_update_parsing() {
let json = r#"{
"channel": "balances",
"type": "update",
"data": [{
"BTC": "1.5432",
"USD": "50000.00"
}]
}"#;
let update: BalanceUpdate = serde_json::from_str(json).unwrap();
assert_eq!(update.channel, "balances");
assert_eq!(update.get_balance("BTC"), Some(&"1.5432".to_string()));
assert_eq!(update.get_balance("USD"), Some(&"50000.00".to_string()));
}
#[test]
fn test_order_update_parsing() {
let json = r#"{
"channel": "orders",
"type": "update",
"data": [{
"order_id": "O12345",
"symbol": "BTC/USD",
"side": "buy",
"order_type": "limit",
"limit_price": "95000.00",
"order_qty": "0.5",
"filled_qty": "0.0",
"status": "open",
"timestamp": "2024-01-01T00:00:00Z"
}]
}"#;
let update: OrderUpdate = serde_json::from_str(json).unwrap();
assert_eq!(update.channel, "orders");
assert!(update.is_open());
assert!(!update.is_closed());
}
#[test]
fn test_execution_update_parsing() {
let json = r#"{
"channel": "executions",
"type": "update",
"data": [{
"exec_id": "E12345",
"order_id": "O12345",
"symbol": "BTC/USD",
"side": "buy",
"exec_qty": "0.5",
"exec_price": "95000.00",
"timestamp": "2024-01-01T00:00:00Z",
"liquidity": "taker"
}]
}"#;
let update: ExecutionUpdate = serde_json::from_str(json).unwrap();
assert_eq!(update.channel, "executions");
assert_eq!(update.total_value(), Some(47500.0));
}
}