use std::collections::HashMap;
use chrono::serde as chrono_serde;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use crate::common::*;
use crate::v2::rest::api_impl::*;
#[derive(Serialize, Debug)]
pub struct GetOHLC {
pub market: Symbol,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u64>,
#[serde(rename = "period")]
pub period_minutes: u16,
#[serde(
rename = "timestamp",
skip_serializing_if = "Option::is_none",
with = "chrono_serde::ts_seconds_option"
)]
pub after_timestamp: Option<DateTime>,
}
impl_api!(GetOHLC => Vec<OHLC> : GET, "/api/v2/k");
#[derive(Serialize, Debug)]
pub struct GetDepth {
pub market: Symbol,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u64>,
pub sort_by_price: bool,
}
impl_api!(GetDepth => RespDepth : GET, "/api/v2/depth");
#[derive(Serialize, Debug)]
pub struct GetPublicTrades {
pub market: Symbol,
#[serde(rename = "timestamp", with = "chrono_serde::ts_seconds")]
pub timestamp_before: DateTime,
#[serde(rename = "from", skip_serializing_if = "Option::is_none")]
pub after_order_id: Option<u64>,
#[serde(rename = "to", skip_serializing_if = "Option::is_none")]
pub before_order_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub order_by: Option<OrderBy>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pagination: Option<bool>,
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub page_params: Option<PageParams>,
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<u64>,
}
impl_api!(GetPublicTrades => Vec<TradeRecord> : GET, "/api/v2/trades");
#[derive(Serialize, Debug)]
pub struct GetMarkets {}
impl_api!(GetMarkets => Vec<MarketInfo> : GET, "/api/v2/markets");
#[derive(Serialize, Debug)]
pub struct GetMarketsSummary {}
impl_api!(GetMarketsSummary => RespSummary : GET, "/api/v2/summary");
#[derive(Serialize, Debug)]
pub struct GetTickers {}
impl_api!(GetTickers => HashMap<Symbol, RespTickerInfo> : GET, "/api/v2/tickers");
#[derive(Serialize, Debug)]
pub struct GetTickersOfMarket {
#[serde(skip)]
pub market: Symbol,
}
impl_api!(GetTickersOfMarket => RespTickerInfo : GET, dynamic params {
api_url!(dynamic "/api/v2/tickers/{}", params.market)
});
#[derive(Deserialize, Eq, PartialEq, Debug)]
pub struct RespDepth {
#[serde(rename = "timestamp", with = "chrono_serde::ts_seconds")]
pub time: DateTime,
pub last_update_version: u64,
pub last_update_id: u64,
pub asks: Vec<DepthEntry>,
pub bids: Vec<DepthEntry>,
}
#[derive(Deserialize, Eq, PartialEq, Default, Debug)]
pub struct RespSummary {
pub tickers: HashMap<Symbol, RespTickerInfo>,
pub coins: HashMap<String, CoinInfo>,
}
#[derive(Deserialize, Eq, PartialEq, Debug)]
pub struct RespTickerInfo {
#[serde(with = "chrono_serde::ts_seconds")]
pub at: DateTime,
pub buy: Decimal,
pub sell: Decimal,
pub open: Decimal,
pub low: Decimal,
pub high: Decimal,
#[serde(rename = "last")]
pub last_price: Decimal,
#[serde(alias = "vol")]
pub volume: Decimal,
#[serde(alias = "vol_in_btc")]
pub volume_in_btc: Decimal,
}
#[derive(Deserialize, Eq, PartialEq, Debug)]
pub struct OHLC {
#[serde(with = "chrono_serde::ts_seconds")]
pub time: DateTime,
pub open: Decimal,
pub high: Decimal,
pub low: Decimal,
pub close: Decimal,
pub volume: Decimal,
}
#[derive(Deserialize, Eq, PartialEq, Debug)]
pub struct DepthEntry {
pub price: Decimal,
pub volume: Decimal,
}
#[derive(Deserialize, Eq, PartialEq, Debug)]
pub struct TradeRecord {
pub id: u64,
pub price: Option<Decimal>,
pub volume: Option<Decimal>,
pub funds: Option<Decimal>,
pub market: Symbol,
pub market_name: String,
#[serde(with = "chrono_serde::ts_seconds")]
pub created_at: DateTime,
#[serde(with = "chrono_serde::ts_milliseconds")]
pub created_at_in_ms: DateTime,
pub side: TradeSide,
pub fee: Option<Decimal>,
pub fee_currency: Option<String>,
pub order_id: Option<u64>,
#[serde(default)]
pub info: Option<TradeMakerType>,
}
#[derive(Deserialize, Eq, PartialEq, Debug)]
#[serde(tag = "maker", rename_all = "lowercase")]
pub enum TradeMakerType {
Ask { ask: TradeMakerInfo },
Bid { bid: TradeMakerInfo },
Unknown,
}
impl TradeMakerType {
pub fn is_unknown(&self) -> bool {
self == &Self::Unknown
}
}
impl Default for TradeMakerType {
fn default() -> Self {
Self::Unknown
}
}
#[derive(Deserialize, Default, Eq, PartialEq, Debug)]
pub struct TradeMakerInfo {
pub fee: Decimal,
pub fee_currency: String,
pub order_id: u64,
}
#[derive(Deserialize, Eq, PartialEq, Default, Debug)]
#[serde(default)]
pub struct MarketInfo {
pub id: Symbol,
pub name: String,
pub market_status: String,
pub base_unit: String,
pub base_unit_precision: i8,
pub min_base_amount: Decimal,
pub quote_unit: String,
pub quote_unit_precision: i8,
pub min_quote_amount: Decimal,
pub m_wallet_supported: bool,
}
#[derive(Deserialize, Eq, PartialEq, Debug)]
pub struct CoinInfo {
pub name: String,
#[serde(deserialize_with = "crate::util::serde::bool_from_onoff")]
pub withdraw: bool,
#[serde(deserialize_with = "crate::util::serde::bool_from_onoff")]
pub deposit: bool,
#[serde(deserialize_with = "crate::util::serde::bool_from_onoff")]
pub trade: bool,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::test_util::*;
use chrono::{TimeZone, Utc};
use rust_decimal_macros::dec;
use surf::Client as HTTPClient;
use surf_vcr::VcrMode;
async fn create_client(cassette: &'static str) -> HTTPClient {
let mut path_builder = test_resource_path();
path_builder.push("rest");
path_builder.push("public");
path_builder.push("market");
path_builder.push(cassette);
create_test_recording_client(VcrMode::Replay, path_builder.as_path().to_str().unwrap())
.await
}
#[async_std::test]
async fn get_ohlc() {
let params = GetOHLC {
market: "btctwd".into(),
limit: Some(10),
period_minutes: 1,
after_timestamp: None,
};
let resp = create_client("get_ohlc.yaml")
.await
.send(params.to_request())
.await
.expect("Error while sending request");
let result = GetOHLC::read_response(resp.into()).await;
let ohlcs: Vec<OHLC> = result.expect("failed to parse result");
assert_eq!(ohlcs.len(), 10);
assert_eq!(
ohlcs[1],
OHLC {
time: Utc.timestamp(1636257660, 0),
open: dec!(1735077.9),
high: dec!(1735077.9),
low: dec!(1735077.9),
close: dec!(1735077.9),
volume: dec!(0.0778),
}
);
assert_eq!(
ohlcs[3],
OHLC {
time: Utc.timestamp(1636257780, 0),
open: dec!(1738000),
high: dec!(1738000),
low: dec!(1738000),
close: dec!(1738000),
volume: dec!(0),
}
);
}
#[async_std::test]
async fn get_depth() {
let params = GetDepth {
market: "btctwd".into(),
limit: Some(10),
sort_by_price: true,
};
let resp = create_client("get_depth.yaml")
.await
.send(params.to_request())
.await
.expect("Error while sending request");
let result = GetDepth::read_response(resp.into()).await;
let depth_info: RespDepth = result.expect("failed to parse result");
assert_eq!(depth_info.asks.len(), 10);
assert_eq!(
depth_info.asks[9],
DepthEntry {
price: dec!(1738000.0),
volume: dec!(0.1159757),
}
);
assert_eq!(depth_info.bids.len(), 10);
assert_eq!(
depth_info.bids[8],
DepthEntry {
price: dec!(1732000.0),
volume: dec!(0.05773672),
}
);
}
#[async_std::test]
async fn get_public_trades() {
let params = GetPublicTrades {
market: "btctwd".into(),
timestamp_before: Utc.timestamp(1636212254, 0),
after_order_id: None,
before_order_id: None,
order_by: None,
pagination: None,
page_params: None,
offset: None,
};
let resp = create_client("get_public_trades.yaml")
.await
.send(params.to_request())
.await
.expect("Error while sending request");
let result = GetPublicTrades::read_response(resp.into()).await;
let trade_list: Vec<TradeRecord> = result.expect("failed to parse result");
assert_eq!(trade_list.len(), 50);
assert_eq!(
trade_list[5],
TradeRecord {
id: 29219425,
price: Some(dec!(1699352.1)),
volume: Some(dec!(0.001092)),
funds: Some(dec!(1855.7)),
market: "btctwd".to_string(),
market_name: "BTC/TWD".to_string(),
created_at: Utc.timestamp(1636212047, 0),
created_at_in_ms: Utc.timestamp(1636212047, 217000000),
side: TradeSide::Ask,
fee: None,
fee_currency: None,
order_id: None,
info: None,
}
);
}
#[async_std::test]
async fn get_markets() {
let params = GetMarkets {};
let resp = create_client("get_markets.yaml")
.await
.send(params.to_request())
.await
.expect("Error while sending request");
let result = GetMarkets::read_response(resp.into()).await;
let market_list: Vec<MarketInfo> = result.expect("failed to parse result");
assert_eq!(market_list.len(), 55);
assert_eq!(
market_list[0],
MarketInfo {
id: "maxtwd".into(),
name: "MAX/TWD".into(),
market_status: "active".into(),
base_unit: "max".into(),
base_unit_precision: 2,
min_base_amount: dec!(21),
quote_unit: "twd".into(),
quote_unit_precision: 4,
min_quote_amount: dec!(250),
m_wallet_supported: false,
}
)
}
#[async_std::test]
async fn get_summary() {
let params = GetMarketsSummary {};
let resp = create_client("get_summary.yaml")
.await
.send(params.to_request())
.await
.expect("Error while sending request");
let result = GetMarketsSummary::read_response(resp.into()).await;
let summary: RespSummary = result.expect("failed to parse result");
assert_eq!(summary.coins.len(), 19);
assert_eq!(
summary.coins.get("max"),
Some(&CoinInfo {
name: "max".into(),
withdraw: true,
deposit: true,
trade: true,
})
);
assert_eq!(summary.tickers.len(), 34);
assert_eq!(
summary.tickers.get("btctwd"),
Some(&RespTickerInfo {
at: Utc.timestamp(1636258205, 0),
buy: dec!(1737000.0),
sell: dec!(1738000.0),
open: dec!(1708337.2),
low: dec!(1682500.0),
high: dec!(1739517.2),
last_price: dec!(1738000.0),
volume: dec!(23.70350862),
volume_in_btc: dec!(23.70350862),
})
);
}
#[async_std::test]
async fn get_tickers() {
let params = GetTickers {};
let resp = create_client("get_tickers.yaml")
.await
.send(params.to_request())
.await
.expect("Error while sending request");
let result = GetTickers::read_response(resp.into()).await;
let tickers: HashMap<Symbol, RespTickerInfo> = result.expect("failed to parse result");
assert_eq!(tickers.len(), 34);
assert_eq!(
tickers.get("maxtwd"),
Some(&RespTickerInfo {
at: Utc.timestamp(1636258205, 0),
buy: dec!(11.4951),
sell: dec!(11.5376),
open: dec!(11.5499),
low: dec!(11.4812),
high: dec!(11.5499),
last_price: dec!(11.5377),
volume: dec!(78450.18),
volume_in_btc: dec!(0.51921291849962826),
})
)
}
#[async_std::test]
async fn get_ticker_of_market() {
let params = GetTickersOfMarket {
market: "btctwd".into(),
};
let resp = create_client("get_ticker_of_market.yaml")
.await
.send(params.to_request())
.await
.expect("Error while sending request");
let result = GetTickersOfMarket::read_response(resp.into()).await;
let ticker: RespTickerInfo = result.expect("failed to parse result");
assert_eq!(
ticker,
RespTickerInfo {
at: Utc.timestamp(1636258205, 0),
buy: dec!(1737000.0),
sell: dec!(1738000.0),
open: dec!(1708337.2),
low: dec!(1682500.0),
high: dec!(1739517.2),
last_price: dec!(1738000.0),
volume: dec!(23.70350862),
volume_in_btc: dec!(23.70350862),
}
);
}
}