use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::models::common::{PriceLevel, TotalStats, TradeInfo};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "event", rename_all = "lowercase")]
pub enum StreamMessage {
Authenticated,
Subscribed {
#[serde(flatten)]
data: SubscribedData,
},
Snapshot {
id: String,
channel: String,
#[serde(flatten)]
payload: SnapshotPayload,
},
Data {
id: String,
channel: String,
#[serde(flatten)]
payload: DataPayload,
},
Error {
#[serde(flatten)]
data: ErrorData,
},
Pong {
#[serde(default)]
state: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubscribedData {
pub id: String,
#[serde(default)]
pub channel: Option<String>,
#[serde(default)]
pub symbol: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SnapshotPayload {
pub data: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DataPayload {
pub data: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorData {
#[serde(default)]
pub code: Option<i32>,
#[serde(default)]
pub message: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TradesData {
pub symbol: String,
#[serde(rename = "type", default)]
pub data_type: Option<String>,
#[serde(default)]
pub exchange: Option<String>,
#[serde(default)]
pub market: Option<String>,
#[serde(default)]
pub trades: Vec<StreamTrade>,
#[serde(default)]
pub total: Option<TotalStats>,
#[serde(default)]
pub time: Option<i64>,
#[serde(default)]
pub serial: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamTrade {
pub price: f64,
pub size: i64,
#[serde(default)]
pub bid: Option<f64>,
#[serde(default)]
pub ask: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CandlesSnapshot {
pub symbol: String,
pub date: String,
#[serde(default)]
pub timeframe: Option<String>,
pub data: Vec<CandleHistoryItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CandleHistoryItem {
pub date: String,
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
pub volume: i64,
#[serde(default)]
pub average: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CandleData {
pub symbol: String,
pub date: String,
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
pub volume: i64,
#[serde(default)]
pub average: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BooksData {
pub symbol: String,
#[serde(default)]
pub bids: Vec<PriceLevel>,
#[serde(default)]
pub asks: Vec<PriceLevel>,
#[serde(default)]
pub time: Option<i64>,
#[serde(default)]
pub serial: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AggregatesData {
pub symbol: String,
#[serde(rename = "type", default)]
pub data_type: Option<String>,
#[serde(default)]
pub exchange: Option<String>,
#[serde(default)]
pub market: Option<String>,
#[serde(default)]
pub date: Option<String>,
#[serde(rename = "referencePrice", default)]
pub reference_price: Option<f64>,
#[serde(rename = "previousClose", default)]
pub previous_close: Option<f64>,
#[serde(rename = "openPrice", default)]
pub open_price: Option<f64>,
#[serde(rename = "highPrice", default)]
pub high_price: Option<f64>,
#[serde(rename = "lowPrice", default)]
pub low_price: Option<f64>,
#[serde(rename = "closePrice", default)]
pub close_price: Option<f64>,
#[serde(rename = "avgPrice", default)]
pub avg_price: Option<f64>,
#[serde(rename = "lastPrice", default)]
pub last_price: Option<f64>,
#[serde(rename = "lastSize", default)]
pub last_size: Option<i64>,
#[serde(default)]
pub bids: Vec<PriceLevel>,
#[serde(default)]
pub asks: Vec<PriceLevel>,
#[serde(default)]
pub total: Option<TotalStats>,
#[serde(rename = "lastTrade", default)]
pub last_trade: Option<TradeInfo>,
#[serde(default)]
pub time: Option<i64>,
#[serde(default)]
pub serial: Option<i64>,
#[serde(rename = "lastUpdated", default)]
pub last_updated: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndicesData {
pub symbol: String,
#[serde(rename = "type", default)]
pub data_type: Option<String>,
#[serde(default)]
pub exchange: Option<String>,
#[serde(default)]
pub index: Option<f64>,
#[serde(default)]
pub time: Option<i64>,
#[serde(default)]
pub serial: Option<i64>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_authenticated_event() {
let json = r#"{"event": "authenticated"}"#;
let msg: StreamMessage = serde_json::from_str(json).unwrap();
assert!(matches!(msg, StreamMessage::Authenticated));
}
#[test]
fn test_parse_subscribed_event() {
let json = r#"{
"event": "subscribed",
"id": "sub-123",
"channel": "trades",
"symbol": "2330"
}"#;
let msg: StreamMessage = serde_json::from_str(json).unwrap();
if let StreamMessage::Subscribed { data } = msg {
assert_eq!(data.id, "sub-123");
assert_eq!(data.channel.as_deref(), Some("trades"));
assert_eq!(data.symbol.as_deref(), Some("2330"));
} else {
panic!("Expected Subscribed event");
}
}
#[test]
fn test_parse_snapshot_event() {
let json = r#"{
"event": "snapshot",
"id": "sub-123",
"channel": "trades",
"data": {
"symbol": "2330",
"trades": [{"price": 583.0, "size": 100}],
"time": 1704067200123456
}
}"#;
let msg: StreamMessage = serde_json::from_str(json).unwrap();
if let StreamMessage::Snapshot { id, channel, payload } = msg {
assert_eq!(id, "sub-123");
assert_eq!(channel, "trades");
assert!(payload.data.is_object());
} else {
panic!("Expected Snapshot event");
}
}
#[test]
fn test_parse_data_event() {
let json = r#"{
"event": "data",
"id": "sub-123",
"channel": "candles",
"data": {
"symbol": "2330",
"date": "2026-01-30T09:00:00.000+08:00",
"open": 580.0,
"high": 585.0,
"low": 578.0,
"close": 583.0,
"volume": 12345
}
}"#;
let msg: StreamMessage = serde_json::from_str(json).unwrap();
if let StreamMessage::Data { id, channel, payload } = msg {
assert_eq!(id, "sub-123");
assert_eq!(channel, "candles");
assert!(payload.data.is_object());
} else {
panic!("Expected Data event");
}
}
#[test]
fn test_parse_error_event() {
let json = r#"{
"event": "error",
"code": 4001,
"message": "Invalid symbol"
}"#;
let msg: StreamMessage = serde_json::from_str(json).unwrap();
if let StreamMessage::Error { data } = msg {
assert_eq!(data.code, Some(4001));
assert_eq!(data.message.as_deref(), Some("Invalid symbol"));
} else {
panic!("Expected Error event");
}
}
#[test]
fn test_parse_pong_event() {
let json = r#"{"event": "pong", "state": "ok"}"#;
let msg: StreamMessage = serde_json::from_str(json).unwrap();
if let StreamMessage::Pong { state } = msg {
assert_eq!(state.as_deref(), Some("ok"));
} else {
panic!("Expected Pong event");
}
}
#[test]
fn test_parse_pong_without_state() {
let json = r#"{"event": "pong"}"#;
let msg: StreamMessage = serde_json::from_str(json).unwrap();
if let StreamMessage::Pong { state } = msg {
assert!(state.is_none());
} else {
panic!("Expected Pong event");
}
}
#[test]
fn test_parse_candles_snapshot() {
let json = r#"{
"symbol": "2330",
"date": "2026-01-30",
"timeframe": "1",
"data": [
{"date": "2026-01-30T09:00:00.000+08:00", "open": 580.0, "high": 581.0, "low": 579.0, "close": 580.5, "volume": 1000},
{"date": "2026-01-30T09:01:00.000+08:00", "open": 580.5, "high": 582.0, "low": 580.0, "close": 581.5, "volume": 1500}
]
}"#;
let snapshot: CandlesSnapshot = serde_json::from_str(json).unwrap();
assert_eq!(snapshot.symbol, "2330");
assert_eq!(snapshot.date, "2026-01-30");
assert_eq!(snapshot.timeframe.as_deref(), Some("1"));
assert_eq!(snapshot.data.len(), 2);
assert_eq!(snapshot.data[0].open, 580.0);
assert_eq!(snapshot.data[1].volume, 1500);
}
#[test]
fn test_parse_trades_data() {
let json = r#"{
"symbol": "2330",
"type": "EQUITY",
"trades": [{"price": 583.0, "size": 100, "bid": 582.0, "ask": 583.0}],
"total": {"tradeVolume": 12345678, "tradeValue": 7201234567.0}
}"#;
let trades: TradesData = serde_json::from_str(json).unwrap();
assert_eq!(trades.symbol, "2330");
assert_eq!(trades.data_type.as_deref(), Some("EQUITY"));
assert_eq!(trades.trades.len(), 1);
assert_eq!(trades.trades[0].price, 583.0);
assert_eq!(trades.trades[0].bid, Some(582.0));
assert!(trades.total.is_some());
}
#[test]
fn test_parse_candle_data() {
let json = r#"{
"symbol": "2330",
"date": "2026-01-30T09:15:00.000+08:00",
"open": 580.0,
"high": 585.0,
"low": 578.0,
"close": 583.0,
"volume": 12345,
"average": 581.5
}"#;
let candle: CandleData = serde_json::from_str(json).unwrap();
assert_eq!(candle.symbol, "2330");
assert_eq!(candle.open, 580.0);
assert_eq!(candle.close, 583.0);
assert_eq!(candle.average, Some(581.5));
}
#[test]
fn test_parse_books_data() {
let json = r#"{
"symbol": "2330",
"bids": [{"price": 582.0, "size": 100}],
"asks": [{"price": 583.0, "size": 50}],
"time": 1704067200123456
}"#;
let books: BooksData = serde_json::from_str(json).unwrap();
assert_eq!(books.symbol, "2330");
assert_eq!(books.bids.len(), 1);
assert_eq!(books.asks.len(), 1);
assert_eq!(books.bids[0].price, 582.0);
assert_eq!(books.asks[0].size, 50);
}
#[test]
fn test_parse_indices_data() {
let json = r#"{
"symbol": "IX0001",
"type": "INDEX",
"index": 17500.5,
"time": 1704067200123456
}"#;
let indices: IndicesData = serde_json::from_str(json).unwrap();
assert_eq!(indices.symbol, "IX0001");
assert_eq!(indices.data_type.as_deref(), Some("INDEX"));
assert_eq!(indices.index, Some(17500.5));
}
#[test]
fn test_parse_aggregates_data() {
let json = r#"{
"symbol": "2330",
"type": "EQUITY",
"openPrice": 580.0,
"highPrice": 590.0,
"lowPrice": 578.0,
"closePrice": 585.0,
"lastPrice": 585.0,
"lastSize": 100,
"bids": [{"price": 584.0, "size": 500}],
"asks": [{"price": 585.0, "size": 300}]
}"#;
let agg: AggregatesData = serde_json::from_str(json).unwrap();
assert_eq!(agg.symbol, "2330");
assert_eq!(agg.open_price, Some(580.0));
assert_eq!(agg.last_price, Some(585.0));
assert_eq!(agg.bids.len(), 1);
assert_eq!(agg.asks.len(), 1);
}
}