use serde_json::Value;
use crate::core::types::{
ExchangeError, ExchangeResult, AccountType,
Kline, OrderBook, Ticker, Order, Balance, Position,
OrderSide, OrderType, OrderStatus, PositionSide,
FundingRate, SymbolInfo, BracketResponse, UserTrade,
FundingPayment,
};
use super::endpoints::{unscale_price, unscale_value};
pub struct PhemexParser;
impl PhemexParser {
pub fn extract_data(response: &Value) -> ExchangeResult<&Value> {
if let Some(code) = response.get("code").and_then(|c| c.as_i64()) {
if code != 0 {
let msg = response.get("msg")
.and_then(|m| m.as_str())
.unwrap_or("Unknown error");
return Err(ExchangeError::Api {
code: code as i32,
message: msg.to_string(),
});
}
}
if let Some(data) = response.get("data") {
if let Some(biz_error) = data.get("bizError").and_then(|e| e.as_i64()) {
if biz_error != 0 {
return Err(ExchangeError::Api {
code: biz_error as i32,
message: "Business error".to_string(),
});
}
}
Ok(data)
} else {
Err(ExchangeError::Parse("Missing 'data' field".to_string()))
}
}
pub fn extract_result(response: &Value) -> ExchangeResult<&Value> {
if let Some(error) = response.get("error") {
if !error.is_null() {
return Err(ExchangeError::Api {
code: -1,
message: format!("Error: {:?}", error),
});
}
}
response.get("result")
.ok_or_else(|| ExchangeError::Parse("Missing 'result' 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_i64(data: &Value, key: &str) -> Option<i64> {
data.get(key).and_then(|v| v.as_i64())
}
fn _require_i64(data: &Value, key: &str) -> ExchangeResult<i64> {
Self::get_i64(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)))
}
pub fn parse_server_time(response: &Value) -> ExchangeResult<i64> {
let result = Self::extract_result(response)?;
result.as_i64()
.map(|ns| ns / 1_000_000) .ok_or_else(|| ExchangeError::Parse("Invalid server time".to_string()))
}
pub fn parse_orderbook(response: &Value, price_scale: u8) -> ExchangeResult<OrderBook> {
let result = Self::extract_result(response)?;
let book = result.get("book")
.ok_or_else(|| ExchangeError::Parse("Missing 'book' field".to_string()))?;
let parse_levels = |key: &str| -> Vec<(f64, f64)> {
book.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_ep = pair[0].as_i64()?;
let size = Self::parse_f64(&pair[1])?;
let price = unscale_price(price_ep, price_scale);
Some((price, size))
})
.collect()
})
.unwrap_or_default()
};
Ok(OrderBook {
timestamp: result.get("timestamp")
.and_then(|t| t.as_i64())
.map(|ns| ns / 1_000_000) .unwrap_or(0),
bids: parse_levels("bids"),
asks: parse_levels("asks"),
sequence: result.get("sequence")
.and_then(|s| s.as_i64())
.map(|n| n.to_string()),
})
}
pub fn parse_ticker(response: &Value, price_scale: u8, account_type: AccountType) -> ExchangeResult<Ticker> {
let result = Self::extract_result(response)?;
let (last_price, bid_price, ask_price, high_24h, low_24h, open_price) = match account_type {
AccountType::Spot => {
let last_ep = Self::get_i64(result, "lastEp").unwrap_or(0);
let bid_ep = Self::get_i64(result, "bidEp");
let ask_ep = Self::get_i64(result, "askEp");
let high_ep = Self::get_i64(result, "highEp");
let low_ep = Self::get_i64(result, "lowEp");
let open_ep = Self::get_i64(result, "openEp");
(
unscale_price(last_ep, price_scale),
bid_ep.map(|p| unscale_price(p, price_scale)),
ask_ep.map(|p| unscale_price(p, price_scale)),
high_ep.map(|p| unscale_price(p, price_scale)),
low_ep.map(|p| unscale_price(p, price_scale)),
open_ep.map(|p| unscale_price(p, price_scale)),
)
}
_ => {
let close_ep = Self::get_i64(result, "close").unwrap_or(0);
let high_ep = Self::get_i64(result, "high");
let low_ep = Self::get_i64(result, "low");
let open_ep = Self::get_i64(result, "open");
(
unscale_price(close_ep, price_scale),
None, None, high_ep.map(|p| unscale_price(p, price_scale)),
low_ep.map(|p| unscale_price(p, price_scale)),
open_ep.map(|p| unscale_price(p, price_scale)),
)
}
};
Ok(Ticker {
symbol: Self::get_str(result, "symbol").unwrap_or("").to_string(),
last_price,
bid_price,
ask_price,
high_24h,
low_24h,
volume_24h: Self::get_f64(result, "volume"),
quote_volume_24h: None,
price_change_24h: open_price.map(|o| last_price - o),
price_change_percent_24h: open_price.map(|o| {
if o > 0.0 {
((last_price - o) / o) * 100.0
} else {
0.0
}
}),
timestamp: result.get("timestamp")
.and_then(|t| t.as_i64())
.map(|ns| ns / 1_000_000)
.unwrap_or(0),
})
}
pub fn parse_klines(response: &Value, price_scale: u8) -> ExchangeResult<Vec<Kline>> {
let data = Self::extract_data(response)?;
let rows = data.get("rows")
.and_then(|r| r.as_array())
.ok_or_else(|| ExchangeError::Parse("Missing 'rows' field".to_string()))?;
let mut klines = Vec::with_capacity(rows.len());
for row in rows {
let arr = row.as_array()
.ok_or_else(|| ExchangeError::Parse("Kline is not an array".to_string()))?;
if arr.len() < 8 {
continue;
}
let open_time = arr[0].as_i64().unwrap_or(0) * 1000; let open_ep = arr[2].as_i64().unwrap_or(0);
let close_ep = arr[3].as_i64().unwrap_or(0);
let high_ep = arr[4].as_i64().unwrap_or(0);
let low_ep = arr[5].as_i64().unwrap_or(0);
klines.push(Kline {
open_time,
open: unscale_price(open_ep, price_scale),
close: unscale_price(close_ep, price_scale),
high: unscale_price(high_ep, price_scale),
low: unscale_price(low_ep, price_scale),
volume: Self::parse_f64(&arr[6]).unwrap_or(0.0),
quote_volume: None,
close_time: None,
trades: None,
});
}
Ok(klines)
}
pub fn parse_funding_rate(response: &Value) -> ExchangeResult<FundingRate> {
let data = Self::extract_data(response)?;
let rate_er = Self::get_i64(data, "fundingRateEr").unwrap_or(0);
let rate = rate_er as f64 / 100_000_000.0;
Ok(FundingRate {
symbol: Self::get_str(data, "symbol").unwrap_or("").to_string(),
rate,
next_funding_time: None,
timestamp: data.get("timestamp")
.and_then(|t| t.as_i64())
.map(|ns| ns / 1_000_000)
.unwrap_or(0),
})
}
pub fn parse_order(response: &Value, symbol: &str, price_scale: u8) -> ExchangeResult<Order> {
let data = Self::extract_data(response)?;
Self::parse_order_data(data, symbol, price_scale)
}
pub fn parse_order_data(data: &Value, symbol: &str, price_scale: u8) -> ExchangeResult<Order> {
let side = match Self::get_str(data, "side").unwrap_or("Buy") {
"Sell" => OrderSide::Sell,
_ => OrderSide::Buy,
};
let order_type = match Self::get_str(data, "ordType")
.or_else(|| Self::get_str(data, "orderType"))
.unwrap_or("Limit")
{
"Market" => OrderType::Market,
_ => OrderType::Limit { price: 0.0 },
};
let status = Self::parse_order_status(data);
let price_ep = Self::get_i64(data, "priceEp");
let stop_price_ep = Self::get_i64(data, "stopPxEp");
let quantity = Self::get_i64(data, "orderQty")
.map(|q| q as f64)
.or_else(|| Self::get_i64(data, "baseQtyEv").map(|q| unscale_value(q, 8)))
.unwrap_or(0.0);
let filled_quantity = Self::get_i64(data, "cumQty")
.map(|q| q as f64)
.unwrap_or(0.0);
Ok(Order {
id: Self::get_str(data, "orderID")
.unwrap_or("")
.to_string(),
client_order_id: Self::get_str(data, "clOrdID").map(String::from),
symbol: Self::get_str(data, "symbol").unwrap_or(symbol).to_string(),
side,
order_type,
status,
price: price_ep.map(|p| unscale_price(p, price_scale)),
stop_price: stop_price_ep.map(|p| unscale_price(p, price_scale)),
quantity,
filled_quantity,
average_price: None,
commission: None,
commission_asset: None,
created_at: data.get("createTimeNs")
.or_else(|| data.get("actionTimeNs"))
.and_then(|t| t.as_i64())
.map(|ns| ns / 1_000_000)
.unwrap_or(0),
updated_at: data.get("transactTimeNs")
.and_then(|t| t.as_i64())
.map(|ns| ns / 1_000_000),
time_in_force: crate::core::TimeInForce::Gtc,
})
}
fn parse_order_status(data: &Value) -> OrderStatus {
match Self::get_str(data, "ordStatus").unwrap_or("New") {
"New" | "Untriggered" => OrderStatus::New,
"PartiallyFilled" => OrderStatus::PartiallyFilled,
"Filled" => OrderStatus::Filled,
"Canceled" | "Cancelled" => OrderStatus::Canceled,
"Rejected" => OrderStatus::Rejected,
"Triggered" => OrderStatus::New, _ => OrderStatus::New,
}
}
pub fn parse_orders(response: &Value, price_scale: u8) -> ExchangeResult<Vec<Order>> {
let data = Self::extract_data(response)?;
let orders_array = data.get("rows")
.or_else(|| data.get("data"))
.and_then(|v| v.as_array())
.ok_or_else(|| ExchangeError::Parse("Expected array of orders".to_string()))?;
orders_array.iter()
.map(|item| Self::parse_order_data(item, "", price_scale))
.collect()
}
pub fn parse_order_id(response: &Value) -> ExchangeResult<String> {
let data = Self::extract_data(response)?;
Self::require_str(data, "orderID").map(String::from)
}
pub fn parse_bracket_order(
response: &Value,
symbol: &str,
price_scale: u8,
) -> ExchangeResult<BracketResponse> {
let data = Self::extract_data(response)?;
let entry_order = Self::parse_order_data(data, symbol, price_scale)?;
let tp_order = if let Some(tp_data) = data.get("takeProfitOrder") {
Self::parse_order_data(tp_data, symbol, price_scale)
.unwrap_or_else(|_| Self::synthetic_tp_from_entry(data, &entry_order, price_scale))
} else {
Self::synthetic_tp_from_entry(data, &entry_order, price_scale)
};
let sl_order = if let Some(sl_data) = data.get("stopLossOrder") {
Self::parse_order_data(sl_data, symbol, price_scale)
.unwrap_or_else(|_| Self::synthetic_sl_from_entry(data, &entry_order, price_scale))
} else {
Self::synthetic_sl_from_entry(data, &entry_order, price_scale)
};
Ok(BracketResponse {
entry_order,
tp_order,
sl_order,
})
}
fn synthetic_tp_from_entry(data: &Value, entry: &Order, price_scale: u8) -> Order {
let tp_price_ep = Self::get_i64(data, "takeProfitEp").unwrap_or(0);
let tp_price = unscale_price(tp_price_ep, price_scale);
Order {
id: "tp_pending".to_string(),
client_order_id: None,
symbol: entry.symbol.clone(),
side: entry.side.opposite(),
order_type: OrderType::Limit { price: tp_price },
status: OrderStatus::New,
price: Some(tp_price),
stop_price: None,
quantity: entry.quantity,
filled_quantity: 0.0,
average_price: None,
commission: None,
commission_asset: None,
created_at: entry.created_at,
updated_at: None,
time_in_force: crate::core::TimeInForce::Gtc,
}
}
fn synthetic_sl_from_entry(data: &Value, entry: &Order, price_scale: u8) -> Order {
let sl_price_ep = Self::get_i64(data, "stopLossEp").unwrap_or(0);
let sl_price = unscale_price(sl_price_ep, price_scale);
Order {
id: "sl_pending".to_string(),
client_order_id: None,
symbol: entry.symbol.clone(),
side: entry.side.opposite(),
order_type: OrderType::StopMarket { stop_price: sl_price },
status: OrderStatus::New,
price: None,
stop_price: Some(sl_price),
quantity: entry.quantity,
filled_quantity: 0.0,
average_price: None,
commission: None,
commission_asset: None,
created_at: entry.created_at,
updated_at: None,
time_in_force: crate::core::TimeInForce::Gtc,
}
}
pub fn parse_spot_balances(response: &Value, value_scale: u8) -> ExchangeResult<Vec<Balance>> {
let data = Self::extract_data(response)?;
let balances_array = data.get("balances")
.and_then(|b| b.as_array())
.ok_or_else(|| ExchangeError::Parse("Expected balances array".to_string()))?;
let mut balances = Vec::new();
for item in balances_array {
let asset = Self::get_str(item, "currency").unwrap_or("").to_string();
if asset.is_empty() { continue; }
let balance_ev = Self::get_i64(item, "balanceEv").unwrap_or(0);
let locked_ev = Self::get_i64(item, "lockedTradingBalanceEv").unwrap_or(0);
let total = unscale_value(balance_ev, value_scale);
let locked = unscale_value(locked_ev, value_scale);
balances.push(Balance {
asset,
free: total - locked,
locked,
total,
});
}
Ok(balances)
}
pub fn parse_contract_account(response: &Value, value_scale: u8) -> ExchangeResult<Vec<Balance>> {
let data = Self::extract_data(response)?;
let account = data.get("account")
.ok_or_else(|| ExchangeError::Parse("Missing account field".to_string()))?;
let currency = Self::get_str(account, "currency").unwrap_or("BTC").to_string();
let balance_ev = Self::get_i64(account, "accountBalanceEv").unwrap_or(0);
let used_ev = Self::get_i64(account, "totalUsedBalanceEv").unwrap_or(0);
let total = unscale_value(balance_ev, value_scale);
let used = unscale_value(used_ev, value_scale);
Ok(vec![Balance {
asset: currency,
free: total - used,
locked: used,
total,
}])
}
pub fn parse_positions(response: &Value, price_scale: u8, value_scale: u8) -> ExchangeResult<Vec<Position>> {
let data = Self::extract_data(response)?;
let positions_array = data.get("positions")
.and_then(|p| p.as_array())
.ok_or_else(|| ExchangeError::Parse("Expected positions array".to_string()))?;
let mut positions = Vec::new();
for item in positions_array {
if let Some(pos) = Self::parse_position_data(item, price_scale, value_scale) {
positions.push(pos);
}
}
Ok(positions)
}
fn parse_position_data(data: &Value, price_scale: u8, value_scale: u8) -> Option<Position> {
let symbol = Self::get_str(data, "symbol")?.to_string();
let size = Self::get_i64(data, "size").unwrap_or(0);
if size == 0 {
return None;
}
let side_str = Self::get_str(data, "side").unwrap_or("Buy");
let side = if side_str == "Sell" {
PositionSide::Short
} else {
PositionSide::Long
};
let entry_price_ep = Self::get_i64(data, "avgEntryPriceEp").unwrap_or(0);
let mark_price_ep = Self::get_i64(data, "markPriceEp");
let liq_price_ep = Self::get_i64(data, "liquidationPriceEp");
let unrealized_pnl_ev = Self::get_i64(data, "unrealisedPnlEv").unwrap_or(0);
let leverage_er = Self::get_i64(data, "leverageEr").unwrap_or(0);
let margin_type = if leverage_er > 0 {
crate::core::MarginType::Isolated
} else {
crate::core::MarginType::Cross
};
Some(Position {
symbol,
side,
quantity: size.abs() as f64,
entry_price: unscale_price(entry_price_ep, price_scale),
mark_price: mark_price_ep.map(|p| unscale_price(p, price_scale)),
unrealized_pnl: unscale_value(unrealized_pnl_ev, value_scale),
realized_pnl: None,
leverage: (leverage_er.abs() as f64 / 100_000_000.0) as u32,
liquidation_price: liq_price_ep.map(|p| unscale_price(p, price_scale)),
margin: None,
margin_type,
take_profit: None,
stop_loss: None,
})
}
pub fn parse_user_trades(response: &Value, price_scale: u8) -> ExchangeResult<Vec<UserTrade>> {
let data = Self::extract_data(response)?;
let rows = data.get("rows")
.and_then(|r| r.as_array())
.ok_or_else(|| ExchangeError::Parse("Missing 'rows' in trade history".to_string()))?;
let mut trades = Vec::with_capacity(rows.len());
for item in rows {
let id = Self::get_str(item, "tradeId")
.unwrap_or("")
.to_string();
let order_id = Self::get_str(item, "orderId")
.unwrap_or("")
.to_string();
let symbol = Self::get_str(item, "symbol")
.unwrap_or("")
.to_string();
let side = match Self::get_str(item, "side").unwrap_or("Buy") {
"Sell" => OrderSide::Sell,
_ => OrderSide::Buy,
};
let price_ep = Self::get_i64(item, "priceEp").unwrap_or(0);
let price = unscale_price(price_ep, price_scale);
let qty_ep = Self::get_i64(item, "qtyEp").unwrap_or(0);
let quantity = unscale_value(qty_ep, 8);
let fee_ep = Self::get_i64(item, "execFeeEp").unwrap_or(0);
let commission = unscale_value(fee_ep.abs(), 8);
let commission_asset = {
if symbol.ends_with("USD") {
"USD".to_string()
} else if symbol.ends_with("USDT") {
"USDT".to_string()
} else {
"USD".to_string()
}
};
let is_maker = Self::get_str(item, "execStatus")
.map(|s| s == "MakerFill")
.unwrap_or(false);
let timestamp_ns = Self::get_i64(item, "transactTimeNs").unwrap_or(0);
let timestamp = timestamp_ns / 1_000_000;
trades.push(UserTrade {
id,
order_id,
symbol,
side,
price,
quantity,
commission,
commission_asset,
is_maker,
timestamp,
});
}
Ok(trades)
}
pub fn parse_exchange_info(response: &Value, account_type: AccountType) -> ExchangeResult<Vec<SymbolInfo>> {
let data = response.get("data")
.ok_or_else(|| ExchangeError::Parse("Missing 'data' field".to_string()))?;
let mut symbols = Vec::new();
for key in &["products", "spotProducts"] {
if let Some(items) = data.get(key).and_then(|v| v.as_array()) {
for item in items {
let status = item.get("status").and_then(|v| v.as_str()).unwrap_or("Listed");
if status != "Listed" && status != "Trading" {
continue;
}
let symbol = match item.get("symbol").and_then(|v| v.as_str()) {
Some(s) => s.to_string(),
None => continue,
};
let base_asset = item.get("baseCurrency")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let quote_asset = item.get("quoteCurrency")
.and_then(|v| v.as_str())
.unwrap_or("USD")
.to_string();
if base_asset.is_empty() {
continue;
}
let price_precision = item.get("priceScale")
.and_then(|v| v.as_u64())
.unwrap_or(4) as u8;
let min_quantity = item.get("lotSize")
.and_then(|v| v.as_f64())
.or_else(|| item.get("qtyStep").and_then(|v| v.as_f64()));
let step_size = item.get("qtyStep")
.and_then(|v| v.as_f64());
let min_notional = item.get("minOrderValue")
.and_then(|v| v.as_f64());
let tick_size = item.get("tickSize")
.and_then(|v| {
v.as_str()
.and_then(|s| s.parse::<f64>().ok())
.or_else(|| v.as_f64())
})
.or_else(|| {
Some(10f64.powi(-(price_precision as i32)))
});
symbols.push(SymbolInfo {
symbol,
base_asset,
quote_asset,
status: "TRADING".to_string(),
price_precision,
quantity_precision: 8,
min_quantity,
max_quantity: None,
tick_size,
step_size,
min_notional,
account_type,
});
}
}
}
Ok(symbols)
}
pub fn parse_funding_payments(response: &Value) -> ExchangeResult<Vec<FundingPayment>> {
let data = Self::extract_data(response)?;
let rows = data.get("rows")
.and_then(|v| v.as_array())
.ok_or_else(|| ExchangeError::Parse(
"Missing 'rows' in funding-fees response".to_string(),
))?;
const SCALE: u8 = 8;
let mut payments = Vec::with_capacity(rows.len());
for item in rows {
let symbol = item.get("symbol")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let funding_rate_er = item.get("fundingRateEr")
.and_then(|v| v.as_i64())
.unwrap_or(0);
let funding_rate = unscale_value(funding_rate_er, SCALE);
let position_size = item.get("size")
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
let exec_fee_ev = item.get("execFeeEv")
.and_then(|v| v.as_i64())
.unwrap_or(0);
let payment = unscale_value(exec_fee_ev, SCALE);
let asset = item.get("currency")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let transact_ns = item.get("transactTimeNs")
.and_then(|v| v.as_i64())
.unwrap_or(0);
let timestamp = transact_ns / 1_000_000;
payments.push(FundingPayment {
symbol,
funding_rate,
position_size,
payment,
asset,
timestamp,
});
}
Ok(payments)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_parse_server_time() {
let response = json!({
"error": null,
"id": 0,
"result": 1234567890000000i64
});
let time = PhemexParser::parse_server_time(&response).unwrap();
assert_eq!(time, 1234567890);
}
#[test]
fn test_parse_orderbook() {
let response = json!({
"error": null,
"id": 0,
"result": {
"book": {
"asks": [[87705000i64, 1000000], [87710000i64, 500000]],
"bids": [[87700000i64, 2000000], [87695000i64, 1000000]]
},
"depth": 30,
"sequence": 123456789i64,
"timestamp": 1234567890000000000i64,
"symbol": "BTCUSD"
}
});
let orderbook = PhemexParser::parse_orderbook(&response, 4).unwrap();
assert_eq!(orderbook.bids.len(), 2);
assert_eq!(orderbook.asks.len(), 2);
assert!((orderbook.bids[0].0 - 8770.0).abs() < 0.1);
assert!((orderbook.asks[0].0 - 8770.5).abs() < 0.1);
}
#[test]
fn test_unscale_price() {
let price = unscale_price(87700000, 4);
assert!((price - 8770.0).abs() < f64::EPSILON);
}
#[test]
fn test_unscale_value() {
let value = unscale_value(100000000, 8);
assert!((value - 1.0).abs() < f64::EPSILON);
}
}