use serde_json::Value;
use crate::core::types::{
ExchangeError, ExchangeResult,
Kline, OrderBook, OrderBookLevel, Ticker, Order, Balance, Position,
OrderSide, OrderType, OrderStatus, PositionSide,
FundingRate, PublicTrade, StreamEvent, TradeSide,
OrderUpdateEvent, BalanceUpdateEvent, PositionUpdateEvent,
BalanceChangeReason, PositionChangeReason,
UserTrade, FundingPayment,
};
pub struct ParadexParser;
impl ParadexParser {
pub fn extract_results(response: &Value) -> ExchangeResult<&Value> {
response.get("results")
.ok_or_else(|| ExchangeError::Parse("Missing 'results' field".to_string()))
}
fn parse_f64(value: &Value) -> Option<f64> {
value.as_str()
.and_then(|s| s.parse().ok())
.or_else(|| value.as_f64())
}
fn get_f64(data: &Value, key: &str) -> Option<f64> {
data.get(key).and_then(Self::parse_f64)
}
fn require_f64(data: &Value, key: &str) -> ExchangeResult<f64> {
Self::get_f64(data, key)
.ok_or_else(|| ExchangeError::Parse(format!("Missing or invalid '{}'", key)))
}
fn get_str<'a>(data: &'a Value, key: &str) -> Option<&'a str> {
data.get(key).and_then(|v| v.as_str())
}
fn require_str<'a>(data: &'a Value, key: &str) -> ExchangeResult<&'a str> {
Self::get_str(data, key)
.ok_or_else(|| ExchangeError::Parse(format!("Missing '{}'", key)))
}
fn get_i64(data: &Value, key: &str) -> Option<i64> {
data.get(key).and_then(|v| v.as_i64())
}
pub fn parse_price(response: &Value) -> ExchangeResult<f64> {
let results = Self::extract_results(response)?;
let arr = results.as_array()
.ok_or_else(|| ExchangeError::Parse("'results' is not an array".to_string()))?;
if arr.is_empty() {
return Err(ExchangeError::Parse("Empty results array".to_string()));
}
let data = &arr[0];
Self::require_f64(data, "last_traded_price")
}
pub fn parse_ticker(response: &Value) -> ExchangeResult<Ticker> {
let results = Self::extract_results(response)?;
let arr = results.as_array()
.ok_or_else(|| ExchangeError::Parse("'results' is not an array".to_string()))?;
if arr.is_empty() {
return Err(ExchangeError::Parse("Empty results array".to_string()));
}
let data = &arr[0];
Ok(Ticker {
symbol: Self::get_str(data, "market").unwrap_or("").to_string(),
last_price: Self::get_f64(data, "last_traded_price").unwrap_or(0.0),
bid_price: Self::get_f64(data, "best_bid"),
ask_price: Self::get_f64(data, "best_ask"),
high_24h: None,
low_24h: None,
volume_24h: Self::get_f64(data, "volume_24h"),
quote_volume_24h: None,
price_change_24h: None,
price_change_percent_24h: Self::get_f64(data, "price_change_rate_24h")
.map(|r| r * 100.0), timestamp: Self::get_i64(data, "created_at").unwrap_or(0),
})
}
pub fn parse_orderbook(response: &Value) -> ExchangeResult<OrderBook> {
let parse_levels = |key: &str| -> Vec<OrderBookLevel> {
response.get(key)
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|level| {
let pair = level.as_array()?;
if pair.len() < 2 { return None; }
let price = Self::parse_f64(&pair[0])?;
let size = Self::parse_f64(&pair[1])?;
Some(OrderBookLevel::new(price, size))
})
.collect()
})
.unwrap_or_default()
};
Ok(OrderBook {
timestamp: Self::get_i64(response, "last_updated_at").unwrap_or(0),
bids: parse_levels("bids"),
asks: parse_levels("asks"),
sequence: Self::get_i64(response, "seq_no").map(|n| n.to_string()),
last_update_id: None,
first_update_id: None,
prev_update_id: None,
event_time: None,
transaction_time: None,
checksum: None,
})
}
pub fn parse_klines(response: &Value) -> ExchangeResult<Vec<Kline>> {
let results = Self::extract_results(response)?;
let arr = results.as_array()
.ok_or_else(|| ExchangeError::Parse("'results' is not an array".to_string()))?;
let mut klines = Vec::with_capacity(arr.len());
for item in arr {
if let Some(candle) = item.as_array() {
if candle.len() < 6 {
continue;
}
let open_time = Self::parse_f64(&candle[0])
.map(|t| t as i64)
.unwrap_or(0);
klines.push(Kline {
open_time,
open: Self::parse_f64(&candle[1]).unwrap_or(0.0),
high: Self::parse_f64(&candle[2]).unwrap_or(0.0),
low: Self::parse_f64(&candle[3]).unwrap_or(0.0),
close: Self::parse_f64(&candle[4]).unwrap_or(0.0),
volume: Self::parse_f64(&candle[5]).unwrap_or(0.0),
quote_volume: None,
close_time: None,
trades: None,
});
}
}
Ok(klines)
}
pub fn parse_symbols(response: &Value) -> ExchangeResult<Vec<String>> {
let results = Self::extract_results(response)?;
let arr = results.as_array()
.ok_or_else(|| ExchangeError::Parse("'results' is not an array".to_string()))?;
let symbols: Vec<String> = arr.iter()
.filter_map(|item| Self::get_str(item, "symbol").map(String::from))
.collect();
Ok(symbols)
}
pub fn parse_funding_rate(response: &Value) -> ExchangeResult<FundingRate> {
let results = Self::extract_results(response)?;
let arr = results.as_array()
.ok_or_else(|| ExchangeError::Parse("'results' is not an array".to_string()))?;
if arr.is_empty() {
return Err(ExchangeError::Parse("Empty results array".to_string()));
}
let data = &arr[0];
Ok(FundingRate {
symbol: Self::get_str(data, "market").unwrap_or("").to_string(),
rate: Self::require_f64(data, "funding_rate")?,
next_funding_time: None,
timestamp: Self::get_i64(data, "created_at").unwrap_or(0),
})
}
pub fn parse_order(response: &Value) -> ExchangeResult<Order> {
Self::parse_order_data(response)
}
fn parse_order_data(data: &Value) -> ExchangeResult<Order> {
let status_str = Self::require_str(data, "status")?;
let status = Self::parse_order_status(status_str);
let side_str = Self::require_str(data, "side")?;
let side = Self::parse_order_side(side_str);
let type_str = Self::require_str(data, "type")?;
let order_type = Self::parse_order_type(type_str);
let size = Self::require_f64(data, "size")?;
let remaining_size = Self::get_f64(data, "remaining_size").unwrap_or(size);
let executed_qty = size - remaining_size;
Ok(Order {
id: Self::require_str(data, "id")?.to_string(),
client_order_id: Self::get_str(data, "client_id").map(String::from),
symbol: Self::require_str(data, "market")?.to_string(),
side,
order_type,
status,
price: Self::get_f64(data, "price"),
stop_price: None,
quantity: size,
filled_quantity: executed_qty,
average_price: Self::get_f64(data, "avg_fill_price"),
commission: None,
commission_asset: None,
created_at: Self::get_i64(data, "created_at").unwrap_or(0),
updated_at: Self::get_i64(data, "last_updated_at"),
time_in_force: crate::core::TimeInForce::Gtc,
})
}
pub fn parse_orders(response: &Value) -> ExchangeResult<Vec<Order>> {
let results = Self::extract_results(response)?;
let arr = results.as_array()
.ok_or_else(|| ExchangeError::Parse("'results' is not an array".to_string()))?;
arr.iter()
.map(Self::parse_order_data)
.collect()
}
fn parse_order_side(side_str: &str) -> OrderSide {
match side_str.to_uppercase().as_str() {
"BUY" => OrderSide::Buy,
"SELL" => OrderSide::Sell,
_ => OrderSide::Buy, }
}
fn parse_order_type(type_str: &str) -> OrderType {
match type_str.to_uppercase().as_str() {
"MARKET" => OrderType::Market,
"LIMIT" => OrderType::Limit { price: 0.0 },
"STOP_LIMIT" => OrderType::StopLimit { stop_price: 0.0, limit_price: 0.0 },
"STOP_MARKET" => OrderType::StopMarket { stop_price: 0.0 },
"TAKE_PROFIT_LIMIT" => OrderType::Limit { price: 0.0 },
"TAKE_PROFIT_MARKET" => OrderType::Limit { price: 0.0 },
_ => OrderType::Limit { price: 0.0 }, }
}
fn parse_order_status(status_str: &str) -> OrderStatus {
match status_str.to_uppercase().as_str() {
"NEW" => OrderStatus::New,
"UNTRIGGERED" => OrderStatus::New, "OPEN" => OrderStatus::Open,
"PARTIALLY_FILLED" => OrderStatus::PartiallyFilled,
"FILLED" => OrderStatus::Filled,
"CLOSED" => OrderStatus::Filled, "CANCELLED" => OrderStatus::Canceled,
"REJECTED" => OrderStatus::Rejected,
_ => OrderStatus::Open, }
}
pub fn parse_balances(response: &Value) -> ExchangeResult<Vec<Balance>> {
let results = Self::extract_results(response)?;
let arr = results.as_array()
.ok_or_else(|| ExchangeError::Parse("'results' is not an array".to_string()))?;
arr.iter()
.map(|item| {
let free = Self::get_f64(item, "available").unwrap_or(0.0);
let locked = Self::get_f64(item, "locked").unwrap_or(0.0);
Ok(Balance {
asset: Self::require_str(item, "currency")?.to_string(),
free,
locked,
total: free + locked,
})
})
.collect()
}
pub fn parse_account_info(_response: &Value) -> ExchangeResult<crate::core::types::AccountInfo> {
Ok(crate::core::types::AccountInfo {
account_type: crate::core::AccountType::FuturesCross,
can_trade: true,
can_withdraw: true,
can_deposit: true,
maker_commission: 0.0, taker_commission: 0.05, balances: vec![], })
}
pub fn parse_positions(response: &Value) -> ExchangeResult<Vec<Position>> {
let results = Self::extract_results(response)?;
let arr = results.as_array()
.ok_or_else(|| ExchangeError::Parse("'results' is not an array".to_string()))?;
Ok(arr.iter()
.filter_map(|item| Self::parse_position_data(item).ok())
.collect())
}
fn parse_position_data(data: &Value) -> ExchangeResult<Position> {
let side_str = Self::require_str(data, "side")?;
let side = match side_str.to_uppercase().as_str() {
"LONG" => PositionSide::Long,
"SHORT" => PositionSide::Short,
_ => PositionSide::Long,
};
let quantity = Self::require_f64(data, "size")?;
let entry_price = Self::get_f64(data, "average_entry_price").unwrap_or(0.0);
let unrealized_pnl = Self::get_f64(data, "unrealized_pnl").unwrap_or(0.0);
Ok(Position {
symbol: Self::require_str(data, "market")?.to_string(),
side,
quantity: quantity.abs(),
entry_price,
mark_price: Self::get_f64(data, "mark_price"),
unrealized_pnl,
realized_pnl: None,
liquidation_price: Self::get_f64(data, "liquidation_price"),
leverage: Self::get_f64(data, "leverage").map(|l| l as u32).unwrap_or(1),
margin_type: crate::core::MarginType::Cross,
margin: Self::get_f64(data, "cost"),
take_profit: None,
stop_loss: None,
})
}
pub fn parse_fills(response: &Value) -> ExchangeResult<Vec<UserTrade>> {
let results = Self::extract_results(response)?;
let arr = results.as_array()
.ok_or_else(|| ExchangeError::Parse("'results' is not an array".to_string()))?;
arr.iter()
.map(Self::parse_fill_item)
.collect()
}
fn parse_fill_item(data: &Value) -> ExchangeResult<UserTrade> {
let id = Self::require_str(data, "id")?.to_string();
let order_id = Self::get_str(data, "order_id").unwrap_or("").to_string();
let symbol = Self::require_str(data, "market")?.to_string();
let side_str = Self::require_str(data, "side")?;
let side = Self::parse_order_side(side_str);
let price = Self::require_f64(data, "price")?;
let quantity = Self::require_f64(data, "size")?;
let commission = Self::get_f64(data, "fee").unwrap_or(0.0);
let commission_asset = Self::get_str(data, "fee_currency")
.unwrap_or("USDC")
.to_string();
let liquidity = Self::get_str(data, "liquidity").unwrap_or("TAKER");
let is_maker = liquidity.eq_ignore_ascii_case("MAKER");
let timestamp = Self::get_i64(data, "created_at").unwrap_or(0);
Ok(UserTrade {
id,
order_id,
symbol,
side,
price,
quantity,
commission,
commission_asset,
is_maker,
timestamp,
})
}
pub fn parse_ws_message(text: &str, target_symbol: Option<&str>) -> ExchangeResult<StreamEvent> {
let msg: Value = serde_json::from_str(text)
.map_err(|e| ExchangeError::Parse(format!("Invalid JSON: {}", e)))?;
if let Some(method) = msg.get("method").and_then(|m| m.as_str()) {
if method == "subscription" {
return Self::parse_ws_subscription(&msg, target_symbol);
}
}
Err(ExchangeError::Parse("Not a stream event".to_string()))
}
fn parse_ws_subscription(msg: &Value, target_symbol: Option<&str>) -> ExchangeResult<StreamEvent> {
let params = msg.get("params")
.ok_or_else(|| ExchangeError::Parse("Missing 'params' in subscription".to_string()))?;
let channel = Self::require_str(params, "channel")?;
let data = params.get("data")
.ok_or_else(|| ExchangeError::Parse("Missing 'data' in subscription".to_string()))?;
if channel == "markets_summary" {
return Self::parse_ws_ticker(data, target_symbol);
}
if channel.starts_with("markets_summary.") {
return Self::parse_ws_ticker(data, target_symbol);
}
if channel.starts_with("order_book.") {
return Self::parse_ws_orderbook(data);
}
if channel.starts_with("bbo.") {
return Self::parse_ws_bbo(data);
}
if channel.starts_with("trades.") {
return Self::parse_ws_trades(data);
}
if channel.starts_with("funding_data.") {
return Self::parse_ws_funding_data(data);
}
if channel == "orders" || channel.starts_with("orders.") {
return Self::parse_ws_order_update(data);
}
if channel == "positions" {
return Self::parse_ws_position_update(data);
}
if channel == "account" || channel == "balance_events" {
return Self::parse_ws_balance_update(data);
}
if channel.starts_with("fills.") {
return Self::parse_ws_fill(data);
}
Err(ExchangeError::Parse(format!("Unknown channel: {}", channel)))
}
fn parse_ws_ticker(data: &Value, target_symbol: Option<&str>) -> ExchangeResult<StreamEvent> {
let market = Self::get_str(data, "market").unwrap_or("");
if let Some(target) = target_symbol {
if !market.eq_ignore_ascii_case(target) {
return Err(ExchangeError::Parse(format!(
"Skipping markets_summary update for '{}' (subscribed to '{}')",
market, target
)));
}
}
let ticker = Ticker {
symbol: market.to_string(),
last_price: Self::get_f64(data, "last_traded_price").unwrap_or(0.0),
bid_price: Self::get_f64(data, "best_bid"),
ask_price: Self::get_f64(data, "best_ask"),
high_24h: None,
low_24h: None,
volume_24h: Self::get_f64(data, "volume_24h"),
quote_volume_24h: None,
price_change_24h: None,
price_change_percent_24h: Self::get_f64(data, "price_change_rate_24h")
.map(|r| r * 100.0),
timestamp: chrono::Utc::now().timestamp_millis(),
};
Ok(StreamEvent::Ticker(ticker))
}
fn parse_ws_orderbook(data: &Value) -> ExchangeResult<StreamEvent> {
let parse_levels = |key: &str| -> Vec<OrderBookLevel> {
data.get(key)
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|level| {
let pair = level.as_array()?;
if pair.len() < 2 { return None; }
let price = Self::parse_f64(&pair[0])?;
let size = Self::parse_f64(&pair[1])?;
Some(OrderBookLevel::new(price, size))
})
.collect()
})
.unwrap_or_default()
};
let orderbook = OrderBook {
timestamp: Self::get_i64(data, "timestamp").unwrap_or(0),
bids: parse_levels("bids"),
asks: parse_levels("asks"),
sequence: Self::get_i64(data, "seq_no").map(|n| n.to_string()),
last_update_id: None,
first_update_id: None,
prev_update_id: None,
event_time: None,
transaction_time: None,
checksum: None,
};
Ok(StreamEvent::OrderbookSnapshot(orderbook))
}
fn parse_ws_bbo(data: &Value) -> ExchangeResult<StreamEvent> {
let orderbook = OrderBook {
timestamp: Self::get_i64(data, "timestamp").unwrap_or(0),
bids: vec![OrderBookLevel::new(
Self::get_f64(data, "bid").unwrap_or(0.0),
Self::get_f64(data, "bid_size").unwrap_or(0.0),
)],
asks: vec![OrderBookLevel::new(
Self::get_f64(data, "ask").unwrap_or(0.0),
Self::get_f64(data, "ask_size").unwrap_or(0.0),
)],
sequence: Self::get_i64(data, "seq_no").map(|n| n.to_string()),
last_update_id: None,
first_update_id: None,
prev_update_id: None,
event_time: None,
transaction_time: None,
checksum: None,
};
Ok(StreamEvent::OrderbookSnapshot(orderbook))
}
fn parse_ws_trades(data: &Value) -> ExchangeResult<StreamEvent> {
let side_str = Self::get_str(data, "side").unwrap_or("BUY");
let side = match side_str.to_uppercase().as_str() {
"BUY" => TradeSide::Buy,
"SELL" => TradeSide::Sell,
_ => TradeSide::Buy,
};
let trade = PublicTrade {
id: Self::get_str(data, "id").unwrap_or("").to_string(),
symbol: Self::get_str(data, "market").unwrap_or("").to_string(),
price: Self::get_f64(data, "price").unwrap_or(0.0),
quantity: Self::get_f64(data, "size").unwrap_or(0.0),
side,
timestamp: Self::get_i64(data, "timestamp").unwrap_or(0),
};
Ok(StreamEvent::Trade(trade))
}
fn parse_ws_order_update(data: &Value) -> ExchangeResult<StreamEvent> {
let status_str = Self::require_str(data, "status")?;
let status = Self::parse_order_status(status_str);
let side_str = Self::require_str(data, "side")?;
let side = Self::parse_order_side(side_str);
let type_str = Self::get_str(data, "type").unwrap_or("LIMIT");
let order_type = Self::parse_order_type(type_str);
let size = Self::require_f64(data, "size")?;
let remaining_size = Self::get_f64(data, "remaining_size").unwrap_or(size);
let filled_qty = size - remaining_size;
let event = OrderUpdateEvent {
order_id: Self::require_str(data, "id")?.to_string(),
client_order_id: Self::get_str(data, "client_id").map(String::from),
symbol: Self::require_str(data, "market")?.to_string(),
side,
order_type,
status,
price: Self::get_f64(data, "price"),
quantity: size,
filled_quantity: filled_qty,
average_price: Self::get_f64(data, "avg_fill_price"),
last_fill_price: None,
last_fill_quantity: None,
last_fill_commission: None,
commission_asset: None,
trade_id: None,
timestamp: Self::get_i64(data, "last_updated_at").unwrap_or(0),
};
Ok(StreamEvent::OrderUpdate(event))
}
fn parse_ws_position_update(data: &Value) -> ExchangeResult<StreamEvent> {
let side_str = Self::require_str(data, "side")?;
let side = match side_str.to_uppercase().as_str() {
"LONG" => PositionSide::Long,
"SHORT" => PositionSide::Short,
_ => PositionSide::Long,
};
let quantity = Self::require_f64(data, "size")?;
let entry_price = Self::get_f64(data, "average_entry_price").unwrap_or(0.0);
let unrealized_pnl = Self::get_f64(data, "unrealized_pnl").unwrap_or(0.0);
let event = PositionUpdateEvent {
symbol: Self::require_str(data, "market")?.to_string(),
side,
quantity: quantity.abs(),
entry_price,
mark_price: None,
unrealized_pnl,
realized_pnl: None,
liquidation_price: Self::get_f64(data, "liquidation_price"),
leverage: Self::get_f64(data, "leverage").map(|l| l as u32),
margin_type: Some(crate::core::MarginType::Cross),
reason: Some(PositionChangeReason::Trade),
timestamp: Self::get_i64(data, "last_updated_at").unwrap_or(0),
};
Ok(StreamEvent::PositionUpdate(event))
}
fn parse_ws_balance_update(data: &Value) -> ExchangeResult<StreamEvent> {
let free = Self::get_f64(data, "free_collateral").unwrap_or(0.0);
let locked = 0.0;
let event = BalanceUpdateEvent {
asset: "USDC".to_string(), free,
locked,
total: free + locked,
delta: None,
reason: Some(BalanceChangeReason::Trade),
timestamp: Self::get_i64(data, "updated_at").unwrap_or(0),
};
Ok(StreamEvent::BalanceUpdate(event))
}
fn parse_ws_funding_data(data: &Value) -> ExchangeResult<StreamEvent> {
let symbol = Self::get_str(data, "market")
.unwrap_or("")
.to_string();
let rate = Self::get_f64(data, "funding_rate")
.or_else(|| Self::get_f64(data, "funding_index"))
.unwrap_or(0.0);
let next_funding_time = Self::get_i64(data, "next_funding_at")
.or_else(|| Self::get_i64(data, "next_funding_time"));
let timestamp = Self::get_i64(data, "timestamp")
.or_else(|| Self::get_i64(data, "created_at"))
.unwrap_or(0);
Ok(StreamEvent::FundingRate {
symbol,
rate,
next_funding_time,
timestamp,
})
}
fn parse_ws_fill(data: &Value) -> ExchangeResult<StreamEvent> {
let side_str = Self::get_str(data, "side").unwrap_or("BUY");
let side = Self::parse_order_side(side_str);
let size = Self::get_f64(data, "size").unwrap_or(0.0);
let fill_price = Self::get_f64(data, "price");
let fee = Self::get_f64(data, "fee");
let event = OrderUpdateEvent {
order_id: Self::get_str(data, "order_id").unwrap_or("").to_string(),
client_order_id: Self::get_str(data, "client_id").map(String::from),
symbol: Self::get_str(data, "market").unwrap_or("").to_string(),
side,
order_type: crate::core::types::OrderType::Market, status: crate::core::types::OrderStatus::Filled,
price: fill_price,
quantity: size,
filled_quantity: size,
average_price: fill_price,
last_fill_price: fill_price,
last_fill_quantity: Some(size),
last_fill_commission: fee,
commission_asset: Self::get_str(data, "fee_currency").map(String::from),
trade_id: Self::get_str(data, "id").map(String::from),
timestamp: Self::get_i64(data, "created_at").unwrap_or(0),
};
Ok(StreamEvent::OrderUpdate(event))
}
pub fn parse_funding_payments(response: &Value) -> ExchangeResult<Vec<FundingPayment>> {
let list = response.get("results")
.and_then(|v| v.as_array())
.ok_or_else(|| ExchangeError::Parse(
"Missing 'results' in funding payments response".to_string(),
))?;
let mut payments = Vec::with_capacity(list.len());
for item in list {
let symbol = Self::get_str(item, "market")
.unwrap_or("")
.to_string();
let funding_rate = Self::get_str(item, "funding_rate")
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(0.0);
let position_size = Self::get_str(item, "size")
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(0.0);
let payment = Self::get_str(item, "payment")
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(0.0);
let asset = "USDC".to_string();
let timestamp = Self::get_i64(item, "created_at").unwrap_or(0);
payments.push(FundingPayment {
symbol,
funding_rate,
position_size,
payment,
asset,
timestamp,
});
}
Ok(payments)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_price() {
let json = r#"{"results":[{"market":"BTC-USD-PERP","last_traded_price":"65432.3"}]}"#;
let response: Value = serde_json::from_str(json).unwrap();
let price = ParadexParser::parse_price(&response).unwrap();
assert_eq!(price, 65432.3);
}
#[test]
fn test_parse_ticker() {
let json = r#"{
"results": [{
"market": "BTC-USD-PERP",
"best_bid": "65432.1",
"best_ask": "65432.5",
"last_traded_price": "65432.3",
"volume_24h": "123456789.50",
"price_change_rate_24h": "0.0234",
"created_at": 1681759756789
}]
}"#;
let response: Value = serde_json::from_str(json).unwrap();
let ticker = ParadexParser::parse_ticker(&response).unwrap();
assert_eq!(ticker.symbol, "BTC-USD-PERP");
assert_eq!(ticker.last_price, 65432.3);
}
#[test]
fn test_parse_orderbook() {
let json = r#"{
"market": "BTC-USD-PERP",
"asks": [["65432.5", "1.234"], ["65432.6", "2.456"]],
"bids": [["65432.4", "1.111"], ["65432.3", "2.222"]],
"last_updated_at": 1681759756789,
"seq_no": 12345678
}"#;
let response: Value = serde_json::from_str(json).unwrap();
let orderbook = ParadexParser::parse_orderbook(&response).unwrap();
assert_eq!(orderbook.bids.len(), 2);
assert_eq!(orderbook.asks.len(), 2);
assert_eq!(orderbook.bids[0].price, 65432.4);
}
}