use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize)]
pub struct ChallengeRequest {
pub event: &'static str,
pub api_key: String,
}
impl ChallengeRequest {
pub fn new(api_key: impl Into<String>) -> Self {
Self {
event: "challenge",
api_key: api_key.into(),
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct SubscribeRequest {
pub event: &'static str,
pub feed: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub product_ids: Option<Vec<String>>,
}
impl SubscribeRequest {
pub fn public(feed: impl Into<String>, product_ids: Vec<String>) -> Self {
Self {
event: "subscribe",
feed: feed.into(),
product_ids: if product_ids.is_empty() {
None
} else {
Some(product_ids)
},
}
}
pub fn all(feed: impl Into<String>) -> Self {
Self {
event: "subscribe",
feed: feed.into(),
product_ids: None,
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct PrivateSubscribeRequest {
pub event: &'static str,
pub feed: String,
pub original_challenge: String,
pub signed_challenge: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub product_ids: Option<Vec<String>>,
}
impl PrivateSubscribeRequest {
pub fn new(
feed: impl Into<String>,
original_challenge: String,
signed_challenge: String,
) -> Self {
Self {
event: "subscribe",
feed: feed.into(),
original_challenge,
signed_challenge,
product_ids: None,
}
}
pub fn with_product_ids(mut self, product_ids: Vec<String>) -> Self {
self.product_ids = Some(product_ids);
self
}
}
#[derive(Debug, Clone, Serialize)]
pub struct UnsubscribeRequest {
pub event: &'static str,
pub feed: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub product_ids: Option<Vec<String>>,
}
impl UnsubscribeRequest {
pub fn new(feed: impl Into<String>, product_ids: Vec<String>) -> Self {
Self {
event: "unsubscribe",
feed: feed.into(),
product_ids: if product_ids.is_empty() {
None
} else {
Some(product_ids)
},
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct ChallengeResponse {
pub event: String,
pub message: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct SubscribedResponse {
pub event: String,
pub feed: String,
#[serde(default)]
pub product_ids: Option<Vec<String>>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct UnsubscribedResponse {
pub event: String,
pub feed: String,
#[serde(default)]
pub product_ids: Option<Vec<String>>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ErrorResponse {
pub event: String,
pub message: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct InfoResponse {
pub event: String,
pub message: String,
#[serde(default)]
pub version: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct BookMessage {
pub feed: String,
pub product_id: String,
#[serde(default)]
pub seq: Option<u64>,
#[serde(default)]
pub timestamp: Option<u64>,
#[serde(default)]
pub bids: Vec<BookLevel>,
#[serde(default)]
pub asks: Vec<BookLevel>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct BookSnapshotMessage {
pub feed: String,
pub product_id: String,
#[serde(default)]
pub seq: Option<u64>,
#[serde(default)]
pub timestamp: Option<u64>,
#[serde(default)]
pub bids: Vec<BookLevel>,
#[serde(default)]
pub asks: Vec<BookLevel>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct BookLevel {
pub price: Decimal,
pub qty: Decimal,
}
#[derive(Debug, Clone, Deserialize)]
pub struct TickerMessage {
pub feed: String,
pub product_id: String,
#[serde(default)]
pub time: Option<u64>,
#[serde(default)]
pub bid: Option<Decimal>,
#[serde(default)]
pub bid_size: Option<Decimal>,
#[serde(default)]
pub ask: Option<Decimal>,
#[serde(default)]
pub ask_size: Option<Decimal>,
#[serde(default)]
pub last: Option<Decimal>,
#[serde(default)]
pub last_size: Option<Decimal>,
#[serde(default)]
pub volume: Option<Decimal>,
#[serde(default, rename = "markPrice")]
pub mark_price: Option<Decimal>,
#[serde(default, rename = "openInterest")]
pub open_interest: Option<Decimal>,
#[serde(default)]
pub funding_rate: Option<Decimal>,
#[serde(default)]
pub funding_rate_prediction: Option<Decimal>,
#[serde(default)]
pub change: Option<Decimal>,
#[serde(default)]
pub premium: Option<Decimal>,
#[serde(default)]
pub index: Option<Decimal>,
#[serde(default)]
pub post_only: Option<bool>,
#[serde(default)]
pub suspended: Option<bool>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct TradeMessage {
pub feed: String,
pub product_id: String,
#[serde(default)]
pub uid: Option<String>,
#[serde(default)]
pub side: Option<String>,
#[serde(rename = "type", default)]
pub trade_type: Option<String>,
#[serde(default)]
pub price: Option<Decimal>,
#[serde(default)]
pub qty: Option<Decimal>,
#[serde(default)]
pub time: Option<u64>,
#[serde(default)]
pub seq: Option<u64>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct TradesSnapshotMessage {
pub feed: String,
pub product_id: String,
pub trades: Vec<TradeItem>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct TradeItem {
#[serde(default)]
pub uid: Option<String>,
pub side: String,
#[serde(rename = "type", default)]
pub trade_type: Option<String>,
pub price: Decimal,
pub qty: Decimal,
pub time: u64,
#[serde(default)]
pub seq: Option<u64>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct OpenOrdersMessage {
pub feed: String,
#[serde(default)]
pub orders: Option<Vec<WsOrder>>,
#[serde(flatten)]
pub order: Option<WsOrder>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct WsOrder {
#[serde(default)]
pub order_id: Option<String>,
#[serde(default)]
pub cli_ord_id: Option<String>,
#[serde(default)]
pub instrument: Option<String>,
#[serde(default)]
pub side: Option<String>,
#[serde(default)]
pub order_type: Option<String>,
#[serde(default)]
pub limit_price: Option<Decimal>,
#[serde(default)]
pub stop_price: Option<Decimal>,
#[serde(default)]
pub qty: Option<Decimal>,
#[serde(default)]
pub filled: Option<Decimal>,
#[serde(default)]
pub reduce_only: Option<bool>,
#[serde(default)]
pub time: Option<u64>,
#[serde(default)]
pub last_update_time: Option<u64>,
#[serde(default)]
pub status: Option<String>,
#[serde(default)]
pub reason: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct FillsMessage {
pub feed: String,
#[serde(default)]
pub fills: Option<Vec<WsFill>>,
#[serde(flatten)]
pub fill: Option<WsFill>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct WsFill {
#[serde(default)]
pub fill_id: Option<String>,
#[serde(default)]
pub order_id: Option<String>,
#[serde(default)]
pub cli_ord_id: Option<String>,
#[serde(default)]
pub instrument: Option<String>,
#[serde(default)]
pub side: Option<String>,
#[serde(default)]
pub price: Option<Decimal>,
#[serde(default)]
pub qty: Option<Decimal>,
#[serde(default)]
pub fill_type: Option<String>,
#[serde(default)]
pub fee_paid: Option<Decimal>,
#[serde(default)]
pub fee_currency: Option<String>,
#[serde(default)]
pub time: Option<u64>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct OpenPositionsMessage {
pub feed: String,
#[serde(default)]
pub account: Option<String>,
#[serde(default)]
pub positions: Option<Vec<WsPosition>>,
#[serde(flatten)]
pub position: Option<WsPosition>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct WsPosition {
#[serde(default)]
pub instrument: Option<String>,
#[serde(default)]
pub balance: Option<Decimal>,
#[serde(default)]
pub entry_price: Option<Decimal>,
#[serde(default)]
pub mark_price: Option<Decimal>,
#[serde(default)]
pub index_price: Option<Decimal>,
#[serde(default)]
pub pnl: Option<Decimal>,
#[serde(default)]
pub effective_leverage: Option<Decimal>,
#[serde(default)]
pub initial_margin: Option<Decimal>,
#[serde(default)]
pub maintenance_margin: Option<Decimal>,
#[serde(default)]
pub return_on_equity: Option<Decimal>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct BalancesMessage {
pub feed: String,
#[serde(default)]
pub account: Option<String>,
#[serde(default)]
pub seq: Option<u64>,
#[serde(default)]
pub balance: Option<Decimal>,
#[serde(default)]
pub available: Option<Decimal>,
#[serde(default)]
pub margin: Option<Decimal>,
#[serde(default)]
pub pnl: Option<Decimal>,
#[serde(default)]
pub flex_futures: Option<FlexFuturesBalance>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct FlexFuturesBalance {
#[serde(default)]
pub currencies: Option<serde_json::Value>,
#[serde(default)]
pub portfolio_value: Option<Decimal>,
#[serde(default)]
pub available_margin: Option<Decimal>,
#[serde(default)]
pub initial_margin: Option<Decimal>,
#[serde(default)]
pub maintenance_margin: Option<Decimal>,
#[serde(default)]
pub unrealized_pnl: Option<Decimal>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_challenge_request_serialization() {
let req = ChallengeRequest::new("my_api_key");
let json = serde_json::to_string(&req).unwrap();
assert!(json.contains("\"event\":\"challenge\""));
assert!(json.contains("\"api_key\":\"my_api_key\""));
}
#[test]
fn test_subscribe_request_serialization() {
let req = SubscribeRequest::public("book", vec!["PI_XBTUSD".into()]);
let json = serde_json::to_string(&req).unwrap();
assert!(json.contains("\"event\":\"subscribe\""));
assert!(json.contains("\"feed\":\"book\""));
assert!(json.contains("\"product_ids\":[\"PI_XBTUSD\"]"));
}
#[test]
fn test_challenge_response_deserialization() {
let json = r#"{"event":"challenge","message":"123e4567-e89b-12d3-a456-426614174000"}"#;
let resp: ChallengeResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.event, "challenge");
assert_eq!(resp.message, "123e4567-e89b-12d3-a456-426614174000");
}
#[test]
fn test_book_message_deserialization() {
let json = r#"{
"feed": "book",
"product_id": "PI_XBTUSD",
"seq": 1234,
"timestamp": 1640000000000,
"bids": [{"price": "50000.0", "qty": "1.5"}],
"asks": [{"price": "50001.0", "qty": "2.0"}]
}"#;
let msg: BookMessage = serde_json::from_str(json).unwrap();
assert_eq!(msg.feed, "book");
assert_eq!(msg.product_id, "PI_XBTUSD");
assert_eq!(msg.seq, Some(1234));
assert_eq!(msg.bids.len(), 1);
assert_eq!(msg.asks.len(), 1);
}
#[test]
fn test_ticker_message_deserialization() {
let json = r#"{
"feed": "ticker",
"product_id": "PI_XBTUSD",
"bid": "50000.0",
"ask": "50001.0",
"last": "50000.5",
"volume": "1000.0",
"funding_rate": "0.0001"
}"#;
let msg: TickerMessage = serde_json::from_str(json).unwrap();
assert_eq!(msg.feed, "ticker");
assert_eq!(msg.product_id, "PI_XBTUSD");
assert!(msg.bid.is_some());
assert!(msg.ask.is_some());
}
#[test]
fn test_private_subscribe_request() {
let req = PrivateSubscribeRequest::new(
"open_orders",
"challenge-uuid".to_string(),
"signed-challenge".to_string(),
);
let json = serde_json::to_string(&req).unwrap();
assert!(json.contains("\"event\":\"subscribe\""));
assert!(json.contains("\"feed\":\"open_orders\""));
assert!(json.contains("\"original_challenge\":\"challenge-uuid\""));
assert!(json.contains("\"signed_challenge\":\"signed-challenge\""));
}
}