use ccxt_core::{
Result,
error::{Error, ParseError},
parser_utils::{parse_decimal, parse_timestamp, value_to_hashmap},
types::{
Balance, BalanceEntry, Market, MarketLimits, MarketPrecision, MarketType, MinMax, OHLCV,
Order, OrderBook, OrderBookEntry, OrderSide, OrderStatus, OrderType, Ticker, Trade,
financial::{Amount, Cost, Price},
},
};
use rust_decimal::Decimal;
use rust_decimal::prelude::{FromPrimitive, FromStr};
use serde_json::Value;
use std::collections::HashMap;
pub use ccxt_core::parser_utils::{datetime_to_timestamp, timestamp_to_datetime};
pub fn parse_market(data: &Value) -> Result<Market> {
let id = data["symbol"]
.as_str()
.ok_or_else(|| Error::from(ParseError::missing_field("symbol")))?
.to_string();
let base = data["baseCoin"]
.as_str()
.ok_or_else(|| Error::from(ParseError::missing_field("baseCoin")))?
.to_string();
let quote = data["quoteCoin"]
.as_str()
.ok_or_else(|| Error::from(ParseError::missing_field("quoteCoin")))?
.to_string();
let symbol = format!("{}/{}", base, quote);
let status = data["status"].as_str().unwrap_or("online");
let active = status == "online";
let price_precision = parse_decimal(data, "pricePrecision")
.or_else(|| parse_decimal(data, "pricePlace"))
.map(|p| {
if p.is_integer() {
let places = p.to_string().parse::<i32>().unwrap_or(0);
Decimal::new(1, places as u32)
} else {
p
}
});
let amount_precision = parse_decimal(data, "quantityPrecision")
.or_else(|| parse_decimal(data, "volumePlace"))
.map(|p| {
if p.is_integer() {
let places = p.to_string().parse::<i32>().unwrap_or(0);
Decimal::new(1, places as u32)
} else {
p
}
});
let min_amount =
parse_decimal(data, "minTradeNum").or_else(|| parse_decimal(data, "minTradeAmount"));
let max_amount =
parse_decimal(data, "maxTradeNum").or_else(|| parse_decimal(data, "maxTradeAmount"));
let min_cost = parse_decimal(data, "minTradeUSDT");
let maker_fee = parse_decimal(data, "makerFeeRate");
let taker_fee = parse_decimal(data, "takerFeeRate");
let parsed_symbol = ccxt_core::symbol::SymbolParser::parse(&symbol).ok();
Ok(Market {
id,
symbol,
parsed_symbol,
base: base.clone(),
quote: quote.clone(),
settle: None,
base_id: Some(base),
quote_id: Some(quote),
settle_id: None,
market_type: MarketType::Spot,
active,
margin: false,
contract: Some(false),
linear: None,
inverse: None,
contract_size: None,
expiry: None,
expiry_datetime: None,
strike: None,
option_type: None,
precision: MarketPrecision {
price: price_precision,
amount: amount_precision,
base: None,
quote: None,
},
limits: MarketLimits {
amount: Some(MinMax {
min: min_amount,
max: max_amount,
}),
price: None,
cost: Some(MinMax {
min: min_cost,
max: None,
}),
leverage: None,
},
maker: maker_fee,
taker: taker_fee,
percentage: Some(true),
tier_based: Some(false),
fee_side: Some("quote".to_string()),
info: value_to_hashmap(data),
})
}
pub fn parse_ticker(data: &Value, market: Option<&Market>) -> Result<Ticker> {
let symbol = if let Some(m) = market {
m.symbol.clone()
} else {
data["symbol"]
.as_str()
.or_else(|| data["instId"].as_str())
.map(ToString::to_string)
.ok_or_else(|| {
ccxt_core::Error::from(ccxt_core::ParseError::missing_field("symbol/instId"))
.context("Failed to parse ticker: missing symbol identifier")
})?
};
let timestamp = parse_timestamp(data, "ts")
.or_else(|| parse_timestamp(data, "timestamp"))
.unwrap_or(0);
Ok(Ticker {
symbol,
timestamp,
datetime: timestamp_to_datetime(timestamp),
high: parse_decimal(data, "high24h")
.or_else(|| parse_decimal(data, "high"))
.map(Price::new),
low: parse_decimal(data, "low24h")
.or_else(|| parse_decimal(data, "low"))
.map(Price::new),
bid: parse_decimal(data, "bidPr")
.or_else(|| parse_decimal(data, "bestBid"))
.map(Price::new),
bid_volume: parse_decimal(data, "bidSz")
.or_else(|| parse_decimal(data, "bestBidSize"))
.map(Amount::new),
ask: parse_decimal(data, "askPr")
.or_else(|| parse_decimal(data, "bestAsk"))
.map(Price::new),
ask_volume: parse_decimal(data, "askSz")
.or_else(|| parse_decimal(data, "bestAskSize"))
.map(Amount::new),
vwap: None,
open: parse_decimal(data, "open24h")
.or_else(|| parse_decimal(data, "open"))
.map(Price::new),
close: parse_decimal(data, "lastPr")
.or_else(|| parse_decimal(data, "last"))
.or_else(|| parse_decimal(data, "close"))
.map(Price::new),
last: parse_decimal(data, "lastPr")
.or_else(|| parse_decimal(data, "last"))
.map(Price::new),
previous_close: None,
change: parse_decimal(data, "change24h")
.or_else(|| parse_decimal(data, "change"))
.map(Price::new),
percentage: parse_decimal(data, "changeUtc24h")
.or_else(|| parse_decimal(data, "changePercentage")),
average: None,
base_volume: parse_decimal(data, "baseVolume")
.or_else(|| parse_decimal(data, "vol24h"))
.map(Amount::new),
quote_volume: parse_decimal(data, "quoteVolume")
.or_else(|| parse_decimal(data, "usdtVolume"))
.map(Amount::new),
funding_rate: None,
open_interest: None,
index_price: None,
mark_price: None,
info: value_to_hashmap(data),
})
}
pub fn parse_orderbook(data: &Value, symbol: String) -> Result<OrderBook> {
let timestamp = parse_timestamp(data, "ts")
.or_else(|| parse_timestamp(data, "timestamp"))
.unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
let mut bids = parse_orderbook_side(&data["bids"])?;
let mut asks = parse_orderbook_side(&data["asks"])?;
bids.sort_by(|a, b| b.price.cmp(&a.price));
asks.sort_by(|a, b| a.price.cmp(&b.price));
Ok(OrderBook {
symbol,
timestamp,
datetime: timestamp_to_datetime(timestamp),
nonce: parse_timestamp(data, "seqId"),
bids,
asks,
buffered_deltas: std::collections::VecDeque::new(),
bids_map: std::collections::BTreeMap::new(),
asks_map: std::collections::BTreeMap::new(),
is_synced: false,
needs_resync: false,
last_resync_time: 0,
info: value_to_hashmap(data),
})
}
fn parse_orderbook_side(data: &Value) -> Result<Vec<OrderBookEntry>> {
let Some(array) = data.as_array() else {
return Ok(Vec::new());
};
let mut result = Vec::new();
for item in array {
if let Some(arr) = item.as_array() {
if arr.len() >= 2 {
let price = arr[0]
.as_str()
.and_then(|s| Decimal::from_str(s).ok())
.or_else(|| arr[0].as_f64().and_then(Decimal::from_f64))
.ok_or_else(|| Error::from(ParseError::invalid_value("data", "price")))?;
let amount = arr[1]
.as_str()
.and_then(|s| Decimal::from_str(s).ok())
.or_else(|| arr[1].as_f64().and_then(Decimal::from_f64))
.ok_or_else(|| Error::from(ParseError::invalid_value("data", "amount")))?;
result.push(OrderBookEntry {
price: Price::new(price),
amount: Amount::new(amount),
});
}
}
}
Ok(result)
}
pub fn parse_trade(data: &Value, market: Option<&Market>) -> Result<Trade> {
let symbol = if let Some(m) = market {
m.symbol.clone()
} else {
data["symbol"]
.as_str()
.map(ToString::to_string)
.ok_or_else(|| {
ccxt_core::Error::from(ccxt_core::ParseError::missing_field("symbol"))
.context("Failed to parse trade: missing symbol identifier")
})?
};
let id = data["tradeId"]
.as_str()
.or_else(|| data["id"].as_str())
.map(ToString::to_string);
let timestamp = parse_timestamp(data, "ts")
.or_else(|| parse_timestamp(data, "timestamp"))
.unwrap_or(0);
let side = match data["side"].as_str() {
Some("sell" | "Sell" | "SELL") => OrderSide::Sell,
_ => OrderSide::Buy, };
let price = parse_decimal(data, "price").or_else(|| parse_decimal(data, "fillPrice"));
let amount = parse_decimal(data, "size")
.or_else(|| parse_decimal(data, "amount"))
.or_else(|| parse_decimal(data, "fillSize"));
let cost = match (price, amount) {
(Some(p), Some(a)) => Some(p * a),
_ => None,
};
Ok(Trade {
id,
order: data["orderId"].as_str().map(ToString::to_string),
timestamp,
datetime: timestamp_to_datetime(timestamp),
symbol,
trade_type: None,
side,
taker_or_maker: None,
price: Price::new(price.unwrap_or(Decimal::ZERO)),
amount: Amount::new(amount.unwrap_or(Decimal::ZERO)),
cost: cost.map(Cost::new),
fee: None,
info: value_to_hashmap(data),
})
}
pub fn parse_ohlcv(data: &Value) -> Result<OHLCV> {
let arr = data
.as_array()
.ok_or_else(|| Error::from(ParseError::invalid_format("data", "OHLCV array")))?;
if arr.len() < 6 {
return Err(Error::from(ParseError::invalid_format(
"data",
"OHLCV array with at least 6 elements",
)));
}
let timestamp = arr[0]
.as_str()
.and_then(|s| s.parse::<i64>().ok())
.or_else(|| arr[0].as_i64())
.ok_or_else(|| Error::from(ParseError::invalid_value("data", "timestamp")))?;
let open = arr[1]
.as_str()
.and_then(|s| s.parse::<f64>().ok())
.or_else(|| arr[1].as_f64())
.ok_or_else(|| Error::from(ParseError::invalid_value("data", "open")))?;
let high = arr[2]
.as_str()
.and_then(|s| s.parse::<f64>().ok())
.or_else(|| arr[2].as_f64())
.ok_or_else(|| Error::from(ParseError::invalid_value("data", "high")))?;
let low = arr[3]
.as_str()
.and_then(|s| s.parse::<f64>().ok())
.or_else(|| arr[3].as_f64())
.ok_or_else(|| Error::from(ParseError::invalid_value("data", "low")))?;
let close = arr[4]
.as_str()
.and_then(|s| s.parse::<f64>().ok())
.or_else(|| arr[4].as_f64())
.ok_or_else(|| Error::from(ParseError::invalid_value("data", "close")))?;
let volume = arr[5]
.as_str()
.and_then(|s| s.parse::<f64>().ok())
.or_else(|| arr[5].as_f64())
.ok_or_else(|| Error::from(ParseError::invalid_value("data", "volume")))?;
Ok(OHLCV {
timestamp,
open,
high,
low,
close,
volume,
})
}
pub fn parse_order_status(status: &str) -> OrderStatus {
match status.to_lowercase().as_str() {
"filled" | "full_fill" | "full-fill" => OrderStatus::Closed,
"cancelled" | "canceled" | "cancel" => OrderStatus::Cancelled,
"expired" | "expire" => OrderStatus::Expired,
"rejected" | "reject" => OrderStatus::Rejected,
_ => OrderStatus::Open, }
}
pub fn parse_order(data: &Value, market: Option<&Market>) -> Result<Order> {
let symbol = if let Some(m) = market {
m.symbol.clone()
} else {
data["symbol"]
.as_str()
.or_else(|| data["instId"].as_str())
.map(ToString::to_string)
.ok_or_else(|| {
ccxt_core::Error::from(ccxt_core::ParseError::missing_field("symbol/instId"))
.context("Failed to parse order: missing symbol identifier")
})?
};
let id = data["orderId"]
.as_str()
.ok_or_else(|| Error::from(ParseError::missing_field("orderId")))?
.to_string();
let timestamp = parse_timestamp(data, "cTime")
.or_else(|| parse_timestamp(data, "createTime"))
.or_else(|| parse_timestamp(data, "ts"));
let status_str = data["status"]
.as_str()
.or_else(|| data["state"].as_str())
.unwrap_or("live");
let status = parse_order_status(status_str);
let side = match data["side"].as_str() {
Some("buy" | "Buy" | "BUY") => OrderSide::Buy,
Some("sell" | "Sell" | "SELL") => OrderSide::Sell,
_ => return Err(Error::from(ParseError::invalid_format("data", "side"))),
};
let order_type = match data["orderType"].as_str().or_else(|| data["type"].as_str()) {
Some("market" | "Market" | "MARKET") => OrderType::Market,
Some("limit_maker" | "post_only") => OrderType::LimitMaker,
_ => OrderType::Limit, };
let price = parse_decimal(data, "price").or_else(|| parse_decimal(data, "priceAvg"));
let amount = parse_decimal(data, "size")
.or_else(|| parse_decimal(data, "baseVolume"))
.ok_or_else(|| Error::from(ParseError::missing_field("size")))?;
let filled = parse_decimal(data, "fillSize").or_else(|| parse_decimal(data, "baseVolume"));
let remaining = match filled {
Some(f) => Some(amount - f),
None => Some(amount),
};
let cost =
parse_decimal(data, "fillNotionalUsd").or_else(|| parse_decimal(data, "quoteVolume"));
let average = parse_decimal(data, "priceAvg").or_else(|| parse_decimal(data, "fillPrice"));
Ok(Order {
id,
client_order_id: data["clientOid"]
.as_str()
.or_else(|| data["clientOrderId"].as_str())
.map(ToString::to_string),
timestamp,
datetime: timestamp.and_then(timestamp_to_datetime),
last_trade_timestamp: parse_timestamp(data, "uTime")
.or_else(|| parse_timestamp(data, "updateTime")),
status,
symbol,
order_type,
time_in_force: data["timeInForce"]
.as_str()
.or_else(|| data["force"].as_str())
.map(str::to_uppercase),
side,
price,
average,
amount,
filled,
remaining,
cost,
trades: None,
fee: None,
post_only: None,
reduce_only: data["reduceOnly"].as_bool(),
trigger_price: parse_decimal(data, "triggerPrice"),
stop_price: parse_decimal(data, "stopPrice")
.or_else(|| parse_decimal(data, "presetStopLossPrice")),
take_profit_price: parse_decimal(data, "presetTakeProfitPrice"),
stop_loss_price: parse_decimal(data, "presetStopLossPrice"),
trailing_delta: None,
trailing_percent: None,
activation_price: None,
callback_rate: None,
working_type: None,
fees: Some(Vec::new()),
info: value_to_hashmap(data),
})
}
pub fn parse_balance(data: &Value) -> Result<Balance> {
let mut balances = HashMap::new();
if let Some(balances_array) = data.as_array() {
for balance in balances_array {
parse_balance_entry(balance, &mut balances);
}
} else if let Some(balances_array) = data["data"].as_array() {
for balance in balances_array {
parse_balance_entry(balance, &mut balances);
}
} else {
parse_balance_entry(data, &mut balances);
}
Ok(Balance {
balances,
info: value_to_hashmap(data),
})
}
fn parse_balance_entry(data: &Value, balances: &mut HashMap<String, BalanceEntry>) {
let currency = data["coin"]
.as_str()
.or_else(|| data["coinName"].as_str())
.or_else(|| data["asset"].as_str())
.map(ToString::to_string);
if let Some(currency) = currency {
let available = parse_decimal(data, "available")
.or_else(|| parse_decimal(data, "free"))
.unwrap_or(Decimal::ZERO);
let frozen = parse_decimal(data, "frozen")
.or_else(|| parse_decimal(data, "locked"))
.or_else(|| parse_decimal(data, "lock"))
.unwrap_or(Decimal::ZERO);
let total = available + frozen;
if total > Decimal::ZERO {
balances.insert(
currency,
BalanceEntry {
free: available,
used: frozen,
total,
},
);
}
}
}
pub fn parse_position(data: &Value, symbol: &str) -> Result<ccxt_core::types::Position> {
use ccxt_core::types::position::PositionSide;
let hold_side = data["holdSide"].as_str().unwrap_or("long");
let position_side = match hold_side.to_lowercase().as_str() {
"short" => PositionSide::Short,
"long" => PositionSide::Long,
_ => PositionSide::Both,
};
let side = match position_side {
PositionSide::Long => Some("long".to_string()),
PositionSide::Short => Some("short".to_string()),
PositionSide::Both => None,
};
let total = parse_f64_field(data, "total")
.or_else(|| parse_f64_field(data, "openDelegateSize"))
.unwrap_or(0.0);
let avg_px =
parse_f64_field(data, "averageOpenPrice").or_else(|| parse_f64_field(data, "openPriceAvg"));
let mark_px = parse_f64_field(data, "markPrice");
let upl =
parse_f64_field(data, "unrealizedPL").or_else(|| parse_f64_field(data, "unrealizedPl"));
let lever = parse_f64_field(data, "leverage");
let liq_px = parse_f64_field(data, "liquidationPrice");
let margin = parse_f64_field(data, "margin");
let realized_pnl = parse_f64_field(data, "achievedProfits");
let mgn_mode = data["marginMode"].as_str().unwrap_or("crossed");
let margin_mode = Some(match mgn_mode {
"isolated" => "isolated".to_string(),
_ => "cross".to_string(),
});
let timestamp = parse_timestamp(data, "uTime").or_else(|| parse_timestamp(data, "cTime"));
let datetime = timestamp.and_then(timestamp_to_datetime);
let initial_margin_percentage = lever.map(|l| if l > 0.0 { 1.0 / l } else { 0.0 });
let notional = match (avg_px, Some(total)) {
(Some(price), Some(qty)) if qty > 0.0 => Some(price * qty),
_ => None,
};
let percentage = match (upl, margin) {
(Some(pnl), Some(m)) if m > 0.0 => Some((pnl / m) * 100.0),
_ => None,
};
Ok(ccxt_core::types::Position {
info: data.clone(),
id: data["posId"]
.as_str()
.or_else(|| data["trackingNo"].as_str())
.map(ToString::to_string),
symbol: symbol.to_string(),
side,
position_side: Some(position_side),
dual_side_position: None,
contracts: Some(total),
contract_size: parse_f64_field(data, "contractSize"),
entry_price: avg_px,
mark_price: mark_px,
notional,
leverage: lever,
collateral: margin,
initial_margin: margin,
initial_margin_percentage,
maintenance_margin: None,
maintenance_margin_percentage: None,
unrealized_pnl: upl,
realized_pnl,
liquidation_price: liq_px,
margin_ratio: None,
margin_mode,
hedged: None,
percentage,
timestamp,
datetime,
})
}
pub fn parse_funding_rate(data: &Value, symbol: &str) -> Result<ccxt_core::types::FundingRate> {
let funding_rate = parse_f64_field(data, "fundingRate");
let funding_time =
parse_timestamp(data, "fundingTime").or_else(|| parse_timestamp(data, "nextFundingTime"));
let timestamp = parse_timestamp(data, "ts").or_else(|| parse_timestamp(data, "fundingTime"));
let datetime = timestamp.and_then(timestamp_to_datetime);
let funding_datetime = funding_time.and_then(timestamp_to_datetime);
Ok(ccxt_core::types::FundingRate {
info: data.clone(),
symbol: symbol.to_string(),
mark_price: parse_f64_field(data, "markPrice"),
index_price: parse_f64_field(data, "indexPrice"),
interest_rate: None,
estimated_settle_price: None,
funding_rate,
funding_timestamp: funding_time,
funding_datetime,
previous_funding_rate: None,
previous_funding_timestamp: None,
previous_funding_datetime: None,
timestamp,
datetime,
})
}
pub fn parse_funding_rate_history(
data: &Value,
symbol: &str,
) -> Result<ccxt_core::types::FundingRateHistory> {
let funding_rate = parse_f64_field(data, "fundingRate");
let timestamp =
parse_timestamp(data, "fundingTime").or_else(|| parse_timestamp(data, "settleTime"));
let datetime = timestamp.and_then(timestamp_to_datetime);
Ok(ccxt_core::types::FundingRateHistory {
info: data.clone(),
symbol: symbol.to_string(),
funding_rate,
timestamp,
datetime,
})
}
fn parse_f64_field(data: &Value, field: &str) -> Option<f64> {
data[field]
.as_str()
.and_then(|s| {
if s.is_empty() {
None
} else {
s.parse::<f64>().ok()
}
})
.or_else(|| data[field].as_f64())
}
#[cfg(test)]
mod tests {
#![allow(clippy::disallowed_methods)]
use super::*;
use rust_decimal_macros::dec;
use serde_json::json;
#[test]
fn test_parse_market() {
let data = json!({
"symbol": "BTCUSDT",
"baseCoin": "BTC",
"quoteCoin": "USDT",
"status": "online",
"pricePrecision": "2",
"quantityPrecision": "4",
"minTradeNum": "0.0001",
"makerFeeRate": "0.001",
"takerFeeRate": "0.001"
});
let market = parse_market(&data).unwrap();
assert_eq!(market.id, "BTCUSDT");
assert_eq!(market.symbol, "BTC/USDT");
assert_eq!(market.base, "BTC");
assert_eq!(market.quote, "USDT");
assert!(market.active);
}
#[test]
fn test_parse_ticker() {
let data = json!({
"symbol": "BTCUSDT",
"lastPr": "50000.00",
"high24h": "51000.00",
"low24h": "49000.00",
"bidPr": "49999.00",
"askPr": "50001.00",
"baseVolume": "1000.5",
"ts": "1700000000000"
});
let ticker = parse_ticker(&data, None).unwrap();
assert_eq!(ticker.symbol, "BTCUSDT");
assert_eq!(ticker.last, Some(Price::new(dec!(50000.00))));
assert_eq!(ticker.high, Some(Price::new(dec!(51000.00))));
assert_eq!(ticker.low, Some(Price::new(dec!(49000.00))));
assert_eq!(ticker.timestamp, 1700000000000);
}
#[test]
fn test_parse_orderbook() {
let data = json!({
"bids": [
["50000.00", "1.5"],
["49999.00", "2.0"]
],
"asks": [
["50001.00", "1.0"],
["50002.00", "3.0"]
],
"ts": "1700000000000"
});
let orderbook = parse_orderbook(&data, "BTC/USDT".to_string()).unwrap();
assert_eq!(orderbook.symbol, "BTC/USDT");
assert_eq!(orderbook.bids.len(), 2);
assert_eq!(orderbook.asks.len(), 2);
assert_eq!(orderbook.bids[0].price, Price::new(dec!(50000.00)));
assert_eq!(orderbook.asks[0].price, Price::new(dec!(50001.00)));
}
#[test]
fn test_parse_trade() {
let data = json!({
"tradeId": "123456",
"symbol": "BTCUSDT",
"side": "buy",
"price": "50000.00",
"size": "0.5",
"ts": "1700000000000"
});
let trade = parse_trade(&data, None).unwrap();
assert_eq!(trade.id, Some("123456".to_string()));
assert_eq!(trade.side, OrderSide::Buy);
assert_eq!(trade.price, Price::new(dec!(50000.00)));
assert_eq!(trade.amount, Amount::new(dec!(0.5)));
}
#[test]
fn test_parse_ohlcv() {
let data = json!([
"1700000000000",
"50000.00",
"51000.00",
"49000.00",
"50500.00",
"1000.5"
]);
let ohlcv = parse_ohlcv(&data).unwrap();
assert_eq!(ohlcv.timestamp, 1700000000000);
assert_eq!(ohlcv.open, 50000.00);
assert_eq!(ohlcv.high, 51000.00);
assert_eq!(ohlcv.low, 49000.00);
assert_eq!(ohlcv.close, 50500.00);
assert_eq!(ohlcv.volume, 1000.5);
}
#[test]
fn test_parse_order_status() {
assert_eq!(parse_order_status("live"), OrderStatus::Open);
assert_eq!(parse_order_status("partially_filled"), OrderStatus::Open);
assert_eq!(parse_order_status("filled"), OrderStatus::Closed);
assert_eq!(parse_order_status("cancelled"), OrderStatus::Cancelled);
assert_eq!(parse_order_status("expired"), OrderStatus::Expired);
assert_eq!(parse_order_status("rejected"), OrderStatus::Rejected);
}
#[test]
fn test_parse_order() {
let data = json!({
"orderId": "123456789",
"symbol": "BTCUSDT",
"side": "buy",
"orderType": "limit",
"price": "50000.00",
"size": "0.5",
"status": "live",
"cTime": "1700000000000"
});
let order = parse_order(&data, None).unwrap();
assert_eq!(order.id, "123456789");
assert_eq!(order.side, OrderSide::Buy);
assert_eq!(order.order_type, OrderType::Limit);
assert_eq!(order.price, Some(dec!(50000.00)));
assert_eq!(order.amount, dec!(0.5));
assert_eq!(order.status, OrderStatus::Open);
}
#[test]
fn test_parse_balance() {
let data = json!([
{
"coin": "BTC",
"available": "1.5",
"frozen": "0.5"
},
{
"coin": "USDT",
"available": "10000.00",
"frozen": "0"
}
]);
let balance = parse_balance(&data).unwrap();
let btc = balance.get("BTC").unwrap();
assert_eq!(btc.free, dec!(1.5));
assert_eq!(btc.used, dec!(0.5));
assert_eq!(btc.total, dec!(2.0));
let usdt = balance.get("USDT").unwrap();
assert_eq!(usdt.free, dec!(10000.00));
assert_eq!(usdt.total, dec!(10000.00));
}
#[test]
fn test_timestamp_to_datetime() {
let ts = 1700000000000i64;
let dt = timestamp_to_datetime(ts).unwrap();
assert!(dt.contains("2023-11-14"));
}
}