use serde_json::Value;
use crate::core::types::{
ExchangeError, ExchangeResult, AccountType, OrderBook, Ticker, Order, Balance, Position,
OrderSide, OrderType, OrderStatus, PositionSide,
FundingRate, PublicTrade, StreamEvent, TradeSide,
OrderUpdateEvent, SymbolInfo, Kline, BracketResponse,
UserTrade,
};
pub struct DeribitParser;
impl DeribitParser {
pub fn extract_result(response: &Value) -> ExchangeResult<&Value> {
if let Some(error) = response.get("error") {
let code = error.get("code").and_then(|c| c.as_i64()).unwrap_or(0);
let message = error.get("message").and_then(|m| m.as_str()).unwrap_or("unknown error");
let reason = error.get("data")
.and_then(|d| d.get("reason"))
.and_then(|r| r.as_str())
.unwrap_or("");
let error_msg = if reason.is_empty() {
message.to_string()
} else {
format!("{} - {}", message, reason)
};
return Err(ExchangeError::Api {
code: code as i32,
message: error_msg,
});
}
response.get("result")
.ok_or_else(|| ExchangeError::Parse("Missing 'result' field in JSON-RPC response".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_auth(response: &Value) -> ExchangeResult<(String, String, u64)> {
let result = Self::extract_result(response)?;
let access_token = Self::require_str(result, "access_token")?.to_string();
let refresh_token = Self::require_str(result, "refresh_token")?.to_string();
let expires_in = result.get("expires_in")
.and_then(|v| v.as_u64())
.unwrap_or(900);
Ok((access_token, refresh_token, expires_in))
}
pub fn parse_price(response: &Value) -> ExchangeResult<f64> {
let result = Self::extract_result(response)?;
Self::get_f64(result, "last_price")
.or_else(|| Self::get_f64(result, "mark_price"))
.or_else(|| Self::get_f64(result, "index_price"))
.ok_or_else(|| ExchangeError::Parse("No price field found".to_string()))
}
pub fn parse_orderbook(response: &Value) -> ExchangeResult<OrderBook> {
let result = Self::extract_result(response)?;
let parse_levels = |key: &str| -> Vec<(f64, f64)> {
result.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, size) = if pair.len() >= 3 {
(Self::parse_f64(&pair[1])?, Self::parse_f64(&pair[2])?)
} else {
(Self::parse_f64(&pair[0])?, Self::parse_f64(&pair[1])?)
};
Some((price, size))
})
.collect()
})
.unwrap_or_default()
};
Ok(OrderBook {
timestamp: Self::get_i64(result, "timestamp").unwrap_or(0),
bids: parse_levels("bids"),
asks: parse_levels("asks"),
sequence: Self::get_i64(result, "change_id").map(|id| id.to_string()),
})
}
pub fn parse_ticker(response: &Value) -> ExchangeResult<Ticker> {
let result = Self::extract_result(response)?;
Ok(Ticker {
symbol: Self::get_str(result, "instrument_name").unwrap_or("").to_string(),
last_price: Self::get_f64(result, "last_price").unwrap_or(0.0),
bid_price: Self::get_f64(result, "best_bid_price"),
ask_price: Self::get_f64(result, "best_ask_price"),
high_24h: result.get("stats")
.and_then(|s| s.get("high"))
.and_then(Self::parse_f64),
low_24h: result.get("stats")
.and_then(|s| s.get("low"))
.and_then(Self::parse_f64),
volume_24h: result.get("stats")
.and_then(|s| s.get("volume"))
.and_then(Self::parse_f64),
quote_volume_24h: result.get("stats")
.and_then(|s| s.get("volume_usd"))
.and_then(Self::parse_f64),
price_change_24h: result.get("stats")
.and_then(|s| s.get("price_change"))
.and_then(Self::parse_f64),
price_change_percent_24h: result.get("stats")
.and_then(|s| s.get("price_change"))
.and_then(Self::parse_f64),
timestamp: Self::get_i64(result, "timestamp").unwrap_or(0),
})
}
pub fn parse_klines(response: &Value, interval_ms: u64) -> ExchangeResult<Vec<Kline>> {
let result = Self::extract_result(response)?;
let ticks = result.get("ticks")
.and_then(|v| v.as_array())
.ok_or_else(|| ExchangeError::Parse("Missing 'ticks' array in chart data".to_string()))?;
let opens = result.get("open")
.and_then(|v| v.as_array())
.ok_or_else(|| ExchangeError::Parse("Missing 'open' array in chart data".to_string()))?;
let highs = result.get("high")
.and_then(|v| v.as_array())
.ok_or_else(|| ExchangeError::Parse("Missing 'high' array in chart data".to_string()))?;
let lows = result.get("low")
.and_then(|v| v.as_array())
.ok_or_else(|| ExchangeError::Parse("Missing 'low' array in chart data".to_string()))?;
let closes = result.get("close")
.and_then(|v| v.as_array())
.ok_or_else(|| ExchangeError::Parse("Missing 'close' array in chart data".to_string()))?;
let volumes = result.get("volume")
.and_then(|v| v.as_array())
.ok_or_else(|| ExchangeError::Parse("Missing 'volume' array in chart data".to_string()))?;
let costs = result.get("cost")
.and_then(|v| v.as_array())
.ok_or_else(|| ExchangeError::Parse("Missing 'cost' array in chart data".to_string()))?;
let len = ticks.len();
let mut klines = Vec::with_capacity(len);
for i in 0..len {
let open_time = ticks[i].as_u64().unwrap_or(0);
let close_time = open_time + interval_ms.saturating_sub(1);
klines.push(Kline {
open_time: open_time as i64,
open: Self::parse_f64(&opens[i]).unwrap_or(0.0),
high: Self::parse_f64(&highs[i]).unwrap_or(0.0),
low: Self::parse_f64(&lows[i]).unwrap_or(0.0),
close: Self::parse_f64(&closes[i]).unwrap_or(0.0),
volume: Self::parse_f64(&volumes[i]).unwrap_or(0.0),
quote_volume: Self::parse_f64(&costs[i]),
close_time: Some(close_time as i64),
trades: None,
});
}
Ok(klines)
}
pub fn parse_funding_rate(response: &Value) -> ExchangeResult<FundingRate> {
let result = Self::extract_result(response)?;
Ok(FundingRate {
symbol: Self::get_str(result, "instrument_name").unwrap_or("").to_string(),
rate: Self::get_f64(result, "current_funding")
.or_else(|| Self::get_f64(result, "funding_8h"))
.unwrap_or(0.0),
next_funding_time: None,
timestamp: Self::get_i64(result, "timestamp").unwrap_or(0),
})
}
pub fn parse_order(response: &Value, symbol: &str) -> ExchangeResult<Order> {
let result = Self::extract_result(response)?;
let order_data = result.get("order").unwrap_or(result);
Self::parse_order_data(order_data, symbol)
}
fn parse_order_data(data: &Value, symbol: &str) -> ExchangeResult<Order> {
let direction = Self::get_str(data, "direction").unwrap_or("buy");
let side = match direction {
"sell" => OrderSide::Sell,
_ => OrderSide::Buy,
};
let order_type_str = Self::get_str(data, "order_type").unwrap_or("limit");
let order_type = match order_type_str {
"market" => OrderType::Market,
_ => OrderType::Limit { price: 0.0 },
};
let status = Self::parse_order_status(data);
Ok(Order {
id: Self::get_str(data, "order_id").unwrap_or("").to_string(),
client_order_id: Self::get_str(data, "label").map(String::from),
symbol: Self::get_str(data, "instrument_name").unwrap_or(symbol).to_string(),
side,
order_type,
status,
price: Self::get_f64(data, "price"),
stop_price: Self::get_f64(data, "trigger_price"),
quantity: Self::get_f64(data, "amount").unwrap_or(0.0),
filled_quantity: Self::get_f64(data, "filled_amount").unwrap_or(0.0),
average_price: Self::get_f64(data, "average_price"),
commission: Self::get_f64(data, "commission"),
commission_asset: None, created_at: Self::get_i64(data, "creation_timestamp").unwrap_or(0),
updated_at: Self::get_i64(data, "last_update_timestamp"),
time_in_force: crate::core::TimeInForce::Gtc,
})
}
fn parse_order_status(data: &Value) -> OrderStatus {
match Self::get_str(data, "order_state").unwrap_or("open") {
"open" => {
let filled = Self::get_f64(data, "filled_amount").unwrap_or(0.0);
if filled > 0.0 {
OrderStatus::PartiallyFilled
} else {
OrderStatus::New
}
}
"filled" => OrderStatus::Filled,
"rejected" => OrderStatus::Rejected,
"cancelled" => OrderStatus::Canceled,
"untriggered" => OrderStatus::New, _ => OrderStatus::New,
}
}
pub fn parse_orders(response: &Value) -> ExchangeResult<Vec<Order>> {
let result = Self::extract_result(response)?;
let arr = result.as_array()
.ok_or_else(|| ExchangeError::Parse("Expected array of orders".to_string()))?;
arr.iter()
.map(|item| Self::parse_order_data(item, ""))
.collect()
}
pub fn parse_order_id(response: &Value) -> ExchangeResult<String> {
let result = Self::extract_result(response)?;
let order_data = result.get("order").unwrap_or(result);
Self::require_str(order_data, "order_id").map(String::from)
}
pub fn parse_user_trades(response: &Value) -> ExchangeResult<Vec<UserTrade>> {
let result = Self::extract_result(response)?;
let trades_arr = result.get("trades")
.and_then(|v| v.as_array())
.ok_or_else(|| ExchangeError::Parse("Missing 'trades' array in user trades response".to_string()))?;
trades_arr.iter().map(|item| {
let id = Self::get_str(item, "trade_id")
.map(|s| s.to_string())
.ok_or_else(|| ExchangeError::Parse("Missing 'trade_id'".to_string()))?;
let order_id = Self::get_str(item, "order_id")
.unwrap_or("")
.to_string();
let symbol = Self::get_str(item, "instrument_name")
.unwrap_or("")
.to_string();
let direction = Self::get_str(item, "direction").unwrap_or("buy");
let side = match direction {
"sell" => OrderSide::Sell,
_ => OrderSide::Buy,
};
let price = Self::get_f64(item, "price").unwrap_or(0.0);
let quantity = Self::get_f64(item, "amount").unwrap_or(0.0);
let commission = Self::get_f64(item, "fee").unwrap_or(0.0);
let commission_asset = Self::get_str(item, "fee_currency")
.unwrap_or("")
.to_string();
let is_maker = Self::get_str(item, "liquidity")
.map(|l| l == "M")
.unwrap_or(false);
let timestamp = Self::get_i64(item, "timestamp").unwrap_or(0);
Ok(UserTrade {
id,
order_id,
symbol,
side,
price,
quantity,
commission,
commission_asset,
is_maker,
timestamp,
})
}).collect()
}
pub fn parse_bracket_order(response: &Value, symbol: &str) -> ExchangeResult<BracketResponse> {
let result = Self::extract_result(response)?;
let entry_data = result.get("order").unwrap_or(result);
let entry_order = Self::parse_order_data(entry_data, symbol)?;
let legs_config = entry_data
.get("otoco_config")
.and_then(|v| v.as_array());
let tp_order = if let Some(legs) = legs_config {
let leg = legs.first().unwrap_or(&serde_json::Value::Null);
let tp_price = Self::get_f64(leg, "limit_price")
.or_else(|| Self::get_f64(leg, "price"))
.unwrap_or(0.0);
Order {
id: Self::get_str(leg, "order_id").unwrap_or("tp_pending").to_string(),
client_order_id: None,
symbol: symbol.to_string(),
side: entry_order.side.opposite(),
order_type: OrderType::Limit { price: tp_price },
status: OrderStatus::New,
price: Some(tp_price),
stop_price: None,
quantity: entry_order.quantity,
filled_quantity: 0.0,
average_price: None,
commission: None,
commission_asset: None,
created_at: entry_order.created_at,
updated_at: None,
time_in_force: crate::core::TimeInForce::Gtc,
}
} else {
Order {
id: "tp_pending".to_string(),
client_order_id: None,
symbol: symbol.to_string(),
side: entry_order.side.opposite(),
order_type: OrderType::Market,
status: OrderStatus::New,
price: None,
stop_price: None,
quantity: entry_order.quantity,
filled_quantity: 0.0,
average_price: None,
commission: None,
commission_asset: None,
created_at: entry_order.created_at,
updated_at: None,
time_in_force: crate::core::TimeInForce::Gtc,
}
};
let sl_order = if let Some(legs) = legs_config {
let leg = legs.get(1).unwrap_or(&serde_json::Value::Null);
let sl_price = Self::get_f64(leg, "trigger_price")
.or_else(|| Self::get_f64(leg, "price"))
.unwrap_or(0.0);
Order {
id: Self::get_str(leg, "order_id").unwrap_or("sl_pending").to_string(),
client_order_id: None,
symbol: symbol.to_string(),
side: entry_order.side.opposite(),
order_type: OrderType::StopMarket { stop_price: sl_price },
status: OrderStatus::New,
price: None,
stop_price: Some(sl_price),
quantity: entry_order.quantity,
filled_quantity: 0.0,
average_price: None,
commission: None,
commission_asset: None,
created_at: entry_order.created_at,
updated_at: None,
time_in_force: crate::core::TimeInForce::Gtc,
}
} else {
Order {
id: "sl_pending".to_string(),
client_order_id: None,
symbol: symbol.to_string(),
side: entry_order.side.opposite(),
order_type: OrderType::Market,
status: OrderStatus::New,
price: None,
stop_price: None,
quantity: entry_order.quantity,
filled_quantity: 0.0,
average_price: None,
commission: None,
commission_asset: None,
created_at: entry_order.created_at,
updated_at: None,
time_in_force: crate::core::TimeInForce::Gtc,
}
};
Ok(BracketResponse {
entry_order,
tp_order,
sl_order,
})
}
pub fn parse_balances(response: &Value) -> ExchangeResult<Vec<Balance>> {
let result = Self::extract_result(response)?;
let currency = Self::get_str(result, "currency").unwrap_or("BTC").to_string();
let equity = Self::get_f64(result, "equity").unwrap_or(0.0);
let available = Self::get_f64(result, "available_funds")
.or_else(|| Self::get_f64(result, "balance"))
.unwrap_or(0.0);
let margin = Self::get_f64(result, "initial_margin").unwrap_or(0.0);
Ok(vec![Balance {
asset: currency,
free: available,
locked: margin,
total: equity,
}])
}
pub fn parse_positions(response: &Value) -> ExchangeResult<Vec<Position>> {
let result = Self::extract_result(response)?;
let arr = result.as_array()
.ok_or_else(|| ExchangeError::Parse("Expected array of positions".to_string()))?;
let mut positions = Vec::new();
for item in arr {
if let Some(pos) = Self::parse_position_data(item) {
positions.push(pos);
}
}
Ok(positions)
}
pub fn parse_position(response: &Value) -> ExchangeResult<Position> {
let result = Self::extract_result(response)?;
Self::parse_position_data(result)
.ok_or_else(|| ExchangeError::Parse("Invalid position data".to_string()))
}
fn parse_position_data(data: &Value) -> Option<Position> {
let symbol = Self::get_str(data, "instrument_name")?.to_string();
let size = Self::get_f64(data, "size").unwrap_or(0.0);
if size.abs() < f64::EPSILON {
return None;
}
let direction = Self::get_str(data, "direction").unwrap_or("buy");
let side = match direction {
"sell" => PositionSide::Short,
"buy" => PositionSide::Long,
_ => PositionSide::Both,
};
Some(Position {
symbol,
side,
quantity: size.abs(),
entry_price: Self::get_f64(data, "average_price").unwrap_or(0.0),
mark_price: Self::get_f64(data, "mark_price"),
unrealized_pnl: Self::get_f64(data, "floating_profit_loss").unwrap_or(0.0),
realized_pnl: Self::get_f64(data, "realized_profit_loss"),
leverage: Self::get_f64(data, "leverage").map(|l| l as u32).unwrap_or(1),
liquidation_price: Self::get_f64(data, "estimated_liquidation_price"),
margin: Self::get_f64(data, "initial_margin"),
margin_type: crate::core::MarginType::Cross,
take_profit: None,
stop_loss: None,
})
}
pub fn parse_ws_ticker(data: &Value) -> ExchangeResult<Ticker> {
let stats = data.get("stats");
Ok(Ticker {
symbol: Self::get_str(data, "instrument_name").unwrap_or("").to_string(),
last_price: Self::get_f64(data, "last_price").unwrap_or(0.0),
bid_price: Self::get_f64(data, "best_bid_price"),
ask_price: Self::get_f64(data, "best_ask_price"),
high_24h: stats.and_then(|s| Self::get_f64(s, "high")),
low_24h: stats.and_then(|s| Self::get_f64(s, "low")),
volume_24h: stats.and_then(|s| Self::get_f64(s, "volume")),
quote_volume_24h: stats.and_then(|s| Self::get_f64(s, "volume_usd")),
price_change_24h: stats.and_then(|s| Self::get_f64(s, "price_change")),
price_change_percent_24h: stats.and_then(|s| Self::get_f64(s, "price_change")),
timestamp: Self::get_i64(data, "timestamp").unwrap_or(0),
})
}
pub fn parse_ws_trade(data: &Value) -> ExchangeResult<PublicTrade> {
let item = if data.is_array() {
data.as_array().and_then(|a| a.last()).unwrap_or(data)
} else {
data
};
let direction = Self::get_str(item, "direction").unwrap_or("buy");
let side = match direction {
"sell" => TradeSide::Sell,
_ => TradeSide::Buy,
};
Ok(PublicTrade {
id: Self::get_str(item, "trade_id").unwrap_or("").to_string(),
symbol: Self::get_str(item, "instrument_name").unwrap_or("").to_string(),
price: Self::require_f64(item, "price")?,
quantity: Self::get_f64(item, "amount").unwrap_or(0.0),
side,
timestamp: Self::get_i64(item, "timestamp").unwrap_or(0),
})
}
pub fn parse_ws_orderbook(data: &Value) -> ExchangeResult<StreamEvent> {
let msg_type = Self::get_str(data, "type").unwrap_or("change");
if msg_type == "snapshot" {
let orderbook = Self::parse_orderbook(&serde_json::json!({
"result": data
}))?;
Ok(StreamEvent::OrderbookSnapshot(orderbook))
} else {
let parse_changes = |key: &str| -> Vec<(f64, f64)> {
data.get(key)
.and_then(|arr| arr.as_array())
.map(|changes| {
changes.iter()
.filter_map(|change| {
let arr = change.as_array()?;
if arr.len() < 3 { return None; }
let price = Self::parse_f64(&arr[1])?;
let size = Self::parse_f64(&arr[2])?;
Some((price, size))
})
.collect()
})
.unwrap_or_default()
};
Ok(StreamEvent::OrderbookDelta {
bids: parse_changes("bids"),
asks: parse_changes("asks"),
timestamp: Self::get_i64(data, "timestamp").unwrap_or(0),
})
}
}
pub fn parse_ws_order_update(data: &Value) -> ExchangeResult<OrderUpdateEvent> {
let direction = Self::get_str(data, "direction").unwrap_or("buy");
let side = match direction {
"sell" => OrderSide::Sell,
_ => OrderSide::Buy,
};
let order_type_str = Self::get_str(data, "order_type").unwrap_or("limit");
let order_type = match order_type_str {
"market" => OrderType::Market,
_ => OrderType::Limit { price: 0.0 },
};
let status = Self::parse_order_status(data);
Ok(OrderUpdateEvent {
order_id: Self::get_str(data, "order_id").unwrap_or("").to_string(),
client_order_id: Self::get_str(data, "label").map(String::from),
symbol: Self::get_str(data, "instrument_name").unwrap_or("").to_string(),
side,
order_type,
status,
price: Self::get_f64(data, "price"),
quantity: Self::get_f64(data, "amount").unwrap_or(0.0),
filled_quantity: Self::get_f64(data, "filled_amount").unwrap_or(0.0),
average_price: Self::get_f64(data, "average_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_update_timestamp").unwrap_or(0),
})
}
pub fn parse_exchange_info(response: &Value, account_type: AccountType) -> ExchangeResult<Vec<SymbolInfo>> {
let result = Self::extract_result(response)?;
let items = result.as_array()
.ok_or_else(|| ExchangeError::Parse("Expected array in result".to_string()))?;
let mut symbols = Vec::with_capacity(items.len());
for item in items {
let is_active = item.get("is_active")
.and_then(|v| v.as_bool())
.unwrap_or(true);
if !is_active {
continue;
}
let instrument_name = match item.get("instrument_name").and_then(|v| v.as_str()) {
Some(n) => n.to_string(),
None => continue,
};
let base_asset = item.get("base_currency")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let quote_asset = item.get("quote_currency")
.and_then(|v| v.as_str())
.unwrap_or("USD")
.to_string();
if base_asset.is_empty() {
continue;
}
let raw_tick_size = item.get("tick_size")
.and_then(|v| v.as_f64());
let price_precision = raw_tick_size
.map(|ts| {
if ts <= 0.0 { 8u8 }
else { (-ts.log10().ceil()).max(0.0) as u8 }
})
.unwrap_or(2);
let min_quantity = item.get("min_trade_amount")
.and_then(|v| v.as_f64());
let step_size = item.get("contract_size")
.and_then(|v| v.as_f64());
symbols.push(SymbolInfo {
symbol: instrument_name,
base_asset,
quote_asset,
status: "TRADING".to_string(),
price_precision,
quantity_precision: 8,
min_quantity,
max_quantity: None,
tick_size: raw_tick_size,
step_size,
min_notional: None,
account_type,
});
}
Ok(symbols)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_parse_auth() {
let response = json!({
"jsonrpc": "2.0",
"id": 1,
"result": {
"access_token": "test_access_token",
"refresh_token": "test_refresh_token",
"expires_in": 900,
"token_type": "bearer"
}
});
let (access, refresh, expires) = DeribitParser::parse_auth(&response).unwrap();
assert_eq!(access, "test_access_token");
assert_eq!(refresh, "test_refresh_token");
assert_eq!(expires, 900);
}
#[test]
fn test_parse_error() {
let response = json!({
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": 10004,
"message": "order_not_found"
}
});
let result = DeribitParser::extract_result(&response);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("order_not_found"));
}
#[test]
fn test_parse_ticker() {
let response = json!({
"jsonrpc": "2.0",
"id": 1,
"result": {
"instrument_name": "BTC-PERPETUAL",
"last_price": 50000.5,
"best_bid_price": 50000.0,
"best_ask_price": 50001.0,
"timestamp": 1234567890000i64,
"stats": {
"volume": 15894.89,
"high": 51000.0,
"low": 49000.0
}
}
});
let ticker = DeribitParser::parse_ticker(&response).unwrap();
assert_eq!(ticker.symbol, "BTC-PERPETUAL");
assert!((ticker.last_price - 50000.5).abs() < f64::EPSILON);
assert_eq!(ticker.bid_price, Some(50000.0));
assert_eq!(ticker.ask_price, Some(50001.0));
}
}