use std::collections::{BTreeMap, VecDeque};
use jiff::Timestamp;
use serde::{Deserialize, Serialize};
use serde_json::json;
use serde_with::{DisplayFromStr, serde_as};
use v_exchanges_adapters::kucoin::{KucoinHttpUrl, KucoinOption};
use v_utils::trades::{Kline, Ohlc, Pair};
use crate::{
ExchangeResult, RequestRange, Symbol,
core::{ExchangeInfo, Klines, PairInfo},
kucoin::KucoinTimeframe,
};
#[derive(Debug, Deserialize, Serialize)]
pub struct AllTickersResponse {
pub code: String,
pub data: AllTickersData,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct AllTickersData {
pub time: i64,
pub ticker: Vec<TickerInfo>,
}
#[serde_as]
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TickerInfo {
pub symbol: String,
pub symbol_name: Option<String>,
#[serde_as(as = "Option<DisplayFromStr>")]
pub buy: Option<f64>,
#[serde_as(as = "Option<DisplayFromStr>")]
pub sell: Option<f64>,
#[serde_as(as = "Option<DisplayFromStr>")]
pub change_rate: Option<f64>,
#[serde_as(as = "Option<DisplayFromStr>")]
pub change_price: Option<f64>,
#[serde_as(as = "Option<DisplayFromStr>")]
pub high: Option<f64>,
#[serde_as(as = "Option<DisplayFromStr>")]
pub low: Option<f64>,
#[serde_as(as = "Option<DisplayFromStr>")]
pub vol: Option<f64>,
#[serde_as(as = "Option<DisplayFromStr>")]
pub vol_value: Option<f64>,
#[serde_as(as = "DisplayFromStr")]
pub last: f64,
#[serde_as(as = "Option<DisplayFromStr>")]
pub average_price: Option<f64>,
#[serde_as(as = "Option<DisplayFromStr>")]
pub taker_fee_rate: Option<f64>,
#[serde_as(as = "Option<DisplayFromStr>")]
pub maker_fee_rate: Option<f64>,
#[serde_as(as = "Option<DisplayFromStr>")]
pub taker_coef_ficient: Option<f64>,
#[serde_as(as = "Option<DisplayFromStr>")]
pub maker_coef_ficient: Option<f64>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct KlineResponse {
pub code: String,
pub data: Vec<Vec<String>>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct SymbolsResponse {
pub code: String,
pub data: Vec<KucoinSymbol>,
}
#[serde_as]
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct KucoinSymbol {
pub symbol: String,
pub name: String,
pub base_currency: String,
pub quote_currency: String,
pub fee_currency: String,
pub market: String,
#[serde_as(as = "DisplayFromStr")]
pub base_min_size: f64,
#[serde_as(as = "DisplayFromStr")]
pub quote_min_size: f64,
#[serde_as(as = "DisplayFromStr")]
pub base_max_size: f64,
#[serde_as(as = "DisplayFromStr")]
pub quote_max_size: f64,
#[serde_as(as = "DisplayFromStr")]
pub base_increment: f64,
#[serde_as(as = "DisplayFromStr")]
pub quote_increment: f64,
#[serde_as(as = "DisplayFromStr")]
pub price_increment: f64,
pub price_limit_rate: Option<String>,
#[serde_as(as = "Option<DisplayFromStr>")]
pub min_funds: Option<f64>,
pub is_margin_enabled: bool,
pub enable_trading: bool,
}
pub mod futures {
use std::collections::{BTreeMap, VecDeque};
use jiff::Timestamp;
use serde::{Deserialize, Serialize};
use serde_json::json;
use v_exchanges_adapters::kucoin::{KucoinHttpUrl, KucoinOption};
use v_utils::trades::{Kline, Ohlc, Pair};
use crate::{
ExchangeResult, RequestRange, Symbol,
core::{ExchangeInfo, Klines, PairInfo},
kucoin::KucoinTimeframe,
};
fn to_kucoin_futures_base(base: &str) -> &str {
match base {
"BTC" => "XBT",
other => other,
}
}
fn from_kucoin_futures_base(base: &str) -> &str {
match base {
"XBT" => "BTC",
other => other,
}
}
pub(in crate::kucoin) async fn prices(client: &v_exchanges_adapters::Client, pairs: Option<Vec<Pair>>, _recv_window: Option<std::time::Duration>) -> ExchangeResult<BTreeMap<Pair, f64>> {
let options = vec![KucoinOption::HttpUrl(KucoinHttpUrl::Futures)];
let response: ContractsActiveResponse = client.get("/api/v1/contracts/active", &json!({}), options).await?;
let mut price_map = BTreeMap::default();
for contract in response.data {
let symbol = &contract.symbol;
if !symbol.ends_with('M') {
continue;
}
let base = from_kucoin_futures_base(&contract.base_currency);
let pair = Pair::new(base, contract.quote_currency.as_str());
if let Some(ref requested_pairs) = pairs
&& !requested_pairs.contains(&pair)
{
continue;
}
price_map.insert(pair, contract.last_trade_price);
}
Ok(price_map)
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ContractsActiveResponse {
pub code: String,
pub data: Vec<ContractInfo>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ContractInfo {
pub symbol: String,
pub base_currency: String,
pub quote_currency: String,
pub settle_currency: String,
#[serde(rename = "type")]
pub contract_type: String,
pub status: String,
pub multiplier: f64,
pub tick_size: f64,
pub lot_size: f64,
pub max_leverage: i32,
pub last_trade_price: f64,
}
pub(in crate::kucoin) async fn klines(
client: &v_exchanges_adapters::Client,
symbol: Symbol,
tf: KucoinTimeframe,
range: RequestRange,
_recv_window: Option<std::time::Duration>,
) -> ExchangeResult<Klines> {
let base = to_kucoin_futures_base(symbol.pair.base().as_ref());
let kucoin_symbol = format!("{base}{}M", symbol.pair.quote());
let granularity = (tf.duration().as_secs() / 60) as u32;
let (from_ts, to_ts) = match range {
RequestRange::Span { since, until } => {
let start = since.as_millisecond();
let end = until.map(|t| t.as_millisecond()).unwrap_or_else(|| Timestamp::now().as_millisecond());
(start, end)
}
RequestRange::Limit(_) => {
let end = Timestamp::now();
let start = end - tf.duration() * 200; (start.as_millisecond(), end.as_millisecond())
}
};
let params = json!({
"symbol": kucoin_symbol,
"granularity": granularity,
"from": from_ts,
"to": to_ts,
});
let options = vec![KucoinOption::HttpUrl(KucoinHttpUrl::Futures)];
let response: FuturesKlineResponse = client.get("/api/v1/kline/query", ¶ms, options).await?;
let mut klines_vec = VecDeque::default();
for kline_data in response.data {
if kline_data.len() >= 7 {
let timestamp_ms = kline_data[0] as i64;
let ohlc = Ohlc {
open: kline_data[1],
high: kline_data[2],
low: kline_data[3],
close: kline_data[4],
};
klines_vec.push_back(Kline {
open_time: Timestamp::from_millisecond(timestamp_ms).map_err(|e| eyre::eyre!("Invalid timestamp: {e}"))?,
ohlc,
volume_quote: kline_data[6],
trades: None,
taker_buy_volume_quote: None,
});
}
}
Ok(Klines::new(klines_vec, *tf))
}
#[derive(Debug, Deserialize, Serialize)]
pub struct FuturesKlineResponse {
pub code: String,
pub data: Vec<Vec<f64>>,
}
pub(in crate::kucoin) async fn exchange_info(client: &v_exchanges_adapters::Client, _recv_window: Option<std::time::Duration>) -> ExchangeResult<ExchangeInfo> {
let options = vec![KucoinOption::HttpUrl(KucoinHttpUrl::Futures)];
let response: ContractsActiveResponse = client.get("/api/v1/contracts/active", &json!({}), options).await?;
let mut pairs = BTreeMap::default();
let step_precision = |step: f64| if step == 0.0 { 0u8 } else { (-step.log10()).max(0.0).round() as u8 };
for contract in response.data {
if contract.status != "Open" {
continue;
}
let base = from_kucoin_futures_base(&contract.base_currency);
let pair = Pair::new(base, contract.quote_currency.as_str());
let price_precision = step_precision(contract.tick_size);
let qty_precision = step_precision(contract.lot_size);
let pair_info = PairInfo {
price_precision,
qty_precision,
delivery_date: None,
};
pairs.insert(pair, pair_info);
}
Ok(ExchangeInfo {
server_time: Timestamp::now(),
pairs,
})
}
}
pub(super) async fn prices(client: &v_exchanges_adapters::Client, pairs: Option<Vec<Pair>>, _recv_window: Option<std::time::Duration>) -> ExchangeResult<BTreeMap<Pair, f64>> {
let options = vec![KucoinOption::HttpUrl(KucoinHttpUrl::Spot)];
let response: AllTickersResponse = client.get("/api/v1/market/allTickers", &json!({}), options).await?;
let mut price_map = BTreeMap::default();
for ticker in response.data.ticker {
if let Some((base, quote)) = ticker.symbol.split_once('-') {
let pair = Pair::new(base, quote);
if let Some(ref requested_pairs) = pairs
&& !requested_pairs.contains(&pair)
{
continue;
}
price_map.insert(pair, ticker.last);
}
}
Ok(price_map)
}
pub(super) async fn klines(
client: &v_exchanges_adapters::Client,
symbol: Symbol,
tf: KucoinTimeframe,
range: RequestRange,
_recv_window: Option<std::time::Duration>,
) -> ExchangeResult<Klines> {
let kucoin_symbol = format!("{}-{}", symbol.pair.base(), symbol.pair.quote());
let tf_str = tf.to_string();
let type_param = tf_str.replace("m", "min").replace("h", "hour").replace("d", "day").replace("w", "week");
let mut params = vec![("symbol", kucoin_symbol.as_str()), ("type", type_param.as_str())];
let (start_at, end_at) = match range {
RequestRange::Span { since, until } => {
let start = since.as_second().to_string();
let end = until.map(|t| t.as_second().to_string()).unwrap_or_else(|| Timestamp::now().as_second().to_string());
(start, end)
}
RequestRange::Limit(_) => {
let end = Timestamp::now();
let start = end - tf.duration() * 1500; (start.as_second().to_string(), end.as_second().to_string())
}
};
params.push(("startAt", &start_at));
params.push(("endAt", &end_at));
let options = vec![KucoinOption::HttpUrl(KucoinHttpUrl::Spot)];
let response: KlineResponse = client.get("/api/v1/market/candles", ¶ms, options).await?;
let mut klines_vec = VecDeque::default();
for kline_data in response.data.iter().rev() {
if kline_data.len() >= 7 {
let timestamp_str = &kline_data[0];
let timestamp_secs: i64 = timestamp_str.parse().map_err(|e| eyre::eyre!("Failed to parse timestamp: {e}"))?;
let ohlc = Ohlc {
open: kline_data[1].parse().map_err(|e| eyre::eyre!("Failed to parse open: {e}"))?,
high: kline_data[3].parse().map_err(|e| eyre::eyre!("Failed to parse high: {e}"))?,
low: kline_data[4].parse().map_err(|e| eyre::eyre!("Failed to parse low: {e}"))?,
close: kline_data[2].parse().map_err(|e| eyre::eyre!("Failed to parse close: {e}"))?,
};
klines_vec.push_back(Kline {
open_time: Timestamp::from_second(timestamp_secs).map_err(|e| eyre::eyre!("Invalid timestamp: {e}"))?,
ohlc,
volume_quote: kline_data[6].parse().map_err(|e| eyre::eyre!("Failed to parse turnover: {e}"))?,
trades: None,
taker_buy_volume_quote: None,
});
}
}
Ok(Klines::new(klines_vec, *tf))
}
pub(super) async fn exchange_info(client: &v_exchanges_adapters::Client, _recv_window: Option<std::time::Duration>) -> ExchangeResult<ExchangeInfo> {
let options = vec![KucoinOption::HttpUrl(KucoinHttpUrl::Spot)];
let response: SymbolsResponse = client.get("/api/v2/symbols", &json!({}), options).await?;
let mut pairs = BTreeMap::default();
let step_precision = |step: f64| if step == 0.0 { 0u8 } else { (-step.log10()).max(0.0).round() as u8 };
for symbol in response.data {
if symbol.enable_trading
&& let Some((base, quote)) = symbol.symbol.split_once('-')
{
let pair = Pair::new(base, quote);
let price_precision = step_precision(symbol.price_increment);
let qty_precision = step_precision(symbol.base_increment);
let pair_info = PairInfo {
price_precision,
qty_precision,
delivery_date: None,
};
pairs.insert(pair, pair_info);
}
}
Ok(ExchangeInfo {
server_time: Timestamp::now(), pairs,
})
}