org.knowm.xchange.kucoin.KucoinAdapters Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of xchange-kucoin Show documentation
Show all versions of xchange-kucoin Show documentation
XChange implementation for the Kucoin Exchange
The newest version!
package org.knowm.xchange.kucoin;
import static java.util.stream.Collectors.toCollection;
import static org.knowm.xchange.dto.Order.OrderStatus.CANCELED;
import static org.knowm.xchange.dto.Order.OrderStatus.NEW;
import static org.knowm.xchange.dto.Order.OrderStatus.PARTIALLY_FILLED;
import static org.knowm.xchange.dto.Order.OrderStatus.UNKNOWN;
import static org.knowm.xchange.dto.Order.OrderType.ASK;
import static org.knowm.xchange.dto.Order.OrderType.BID;
import static org.knowm.xchange.kucoin.dto.KucoinOrderFlags.HIDDEN;
import static org.knowm.xchange.kucoin.dto.KucoinOrderFlags.ICEBERG;
import static org.knowm.xchange.kucoin.dto.KucoinOrderFlags.POST_ONLY;
import com.google.common.base.MoreObjects;
import com.google.common.collect.Ordering;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.knowm.xchange.currency.Currency;
import org.knowm.xchange.currency.CurrencyPair;
import org.knowm.xchange.dto.Order;
import org.knowm.xchange.dto.Order.IOrderFlags;
import org.knowm.xchange.dto.Order.OrderStatus;
import org.knowm.xchange.dto.Order.OrderType;
import org.knowm.xchange.dto.account.Balance;
import org.knowm.xchange.dto.account.FundingRecord;
import org.knowm.xchange.dto.account.FundingRecord.Status;
import org.knowm.xchange.dto.account.FundingRecord.Type;
import org.knowm.xchange.dto.marketdata.OrderBook;
import org.knowm.xchange.dto.marketdata.Ticker;
import org.knowm.xchange.dto.marketdata.Trade;
import org.knowm.xchange.dto.marketdata.Trades;
import org.knowm.xchange.dto.marketdata.Trades.TradeSortType;
import org.knowm.xchange.dto.meta.*;
import org.knowm.xchange.dto.trade.LimitOrder;
import org.knowm.xchange.dto.trade.MarketOrder;
import org.knowm.xchange.dto.trade.StopOrder;
import org.knowm.xchange.dto.trade.UserTrade;
import org.knowm.xchange.exceptions.ExchangeException;
import org.knowm.xchange.instrument.Instrument;
import org.knowm.xchange.kucoin.KucoinTradeService.KucoinOrderFlags;
import org.knowm.xchange.kucoin.dto.request.OrderCreateApiRequest;
import org.knowm.xchange.kucoin.dto.response.AccountBalancesResponse;
import org.knowm.xchange.kucoin.dto.response.AllTickersResponse;
import org.knowm.xchange.kucoin.dto.response.CurrenciesResponse;
import org.knowm.xchange.kucoin.dto.response.DepositResponse;
import org.knowm.xchange.kucoin.dto.response.HistOrdersResponse;
import org.knowm.xchange.kucoin.dto.response.OrderBookResponse;
import org.knowm.xchange.kucoin.dto.response.OrderResponse;
import org.knowm.xchange.kucoin.dto.response.SymbolResponse;
import org.knowm.xchange.kucoin.dto.response.SymbolTickResponse;
import org.knowm.xchange.kucoin.dto.response.TradeFeeResponse;
import org.knowm.xchange.kucoin.dto.response.TradeHistoryResponse;
import org.knowm.xchange.kucoin.dto.response.TradeResponse;
import org.knowm.xchange.kucoin.dto.response.WithdrawalResponse;
public class KucoinAdapters {
private static final String TAKER_FEE_RATE = "takerFeeRate";
public static String adaptCurrencyPair(CurrencyPair pair) {
return pair == null ? null : pair.base.getCurrencyCode() + "-" + pair.counter.getCurrencyCode();
}
public static CurrencyPair adaptCurrencyPair(String symbol) {
String[] split = symbol.split("-");
if (split.length != 2) {
throw new ExchangeException("Invalid kucoin symbol: " + symbol);
}
return new CurrencyPair(split[0], split[1]);
}
public static Ticker.Builder adaptTickerFull(CurrencyPair pair, SymbolTickResponse stats) {
return new Ticker.Builder()
.instrument(pair)
.bid(stats.getBuy())
.ask(stats.getSell())
.last(stats.getLast())
.high(stats.getHigh())
.low(stats.getLow())
.volume(stats.getVol())
.quoteVolume(stats.getVolValue())
.open(stats.getOpen())
.timestamp(new Date(stats.getTime()));
}
public static List adaptAllTickers(AllTickersResponse allTickersResponse) {
return Arrays.stream(allTickersResponse.getTicker())
.map(
ticker ->
new Ticker.Builder()
.instrument(adaptCurrencyPair(ticker.getSymbol()))
.bid(ticker.getBuy())
.ask(ticker.getSell())
.last(ticker.getLast())
.high(ticker.getHigh())
.low(ticker.getLow())
.volume(ticker.getVol())
.quoteVolume(ticker.getVolValue())
.percentageChange(
ticker.getChangeRate().multiply(new BigDecimal("100"), new MathContext(8)))
.build())
.collect(Collectors.toList());
}
/**
* Imperfect implementation. Kucoin appears to enforce a base and quote min
* and max amount that the XChange API current doesn't take account of.
*
* @param exchangeMetaData The static exchange metadata.
* @param currenciesResponse Kucoin currencies
* @param symbolsResponse Kucoin symbols
* @param tradeFee Kucoin trade fee (optional)
* @return Exchange metadata.
*/
public static ExchangeMetaData adaptMetadata(
ExchangeMetaData exchangeMetaData,
List currenciesResponse,
List symbolsResponse,
TradeFeeResponse tradeFee)
throws IOException {
Map currencyPairs = exchangeMetaData.getInstruments();
Map currencies = exchangeMetaData.getCurrencies();
Map stringCurrencyMetaDataMap =
adaptCurrencyMetaData(currenciesResponse);
BigDecimal takerTradingFee = tradeFee != null ? tradeFee.getTakerFeeRate() : null;
for (SymbolResponse symbol : symbolsResponse) {
CurrencyPair pair = adaptCurrencyPair(symbol.getSymbol());
InstrumentMetaData staticMetaData = exchangeMetaData.getInstruments().get(pair);
BigDecimal minSize = symbol.getBaseMinSize();
BigDecimal maxSize = symbol.getBaseMaxSize();
BigDecimal minQuoteSize = symbol.getQuoteMinSize();
BigDecimal maxQuoteSize = symbol.getQuoteMaxSize();
int baseScale = symbol.getBaseIncrement().stripTrailingZeros().scale();
int priceScale = symbol.getPriceIncrement().stripTrailingZeros().scale();
FeeTier[] feeTiers = staticMetaData != null ? staticMetaData.getFeeTiers() : null;
Currency feeCurrency = new Currency(symbol.getFeeCurrency());
currencyPairs.put(pair, new InstrumentMetaData.Builder()
.tradingFee(takerTradingFee)
.minimumAmount(minSize)
.maximumAmount(maxSize)
.counterMinimumAmount(minQuoteSize)
.counterMaximumAmount(maxQuoteSize)
.volumeScale(baseScale)
.priceScale(priceScale)
.feeTiers(feeTiers)
.tradingFeeCurrency(feeCurrency)
.marketOrderEnabled(true)
.build());
if (!currencies.containsKey(pair.base))
currencies.put(pair.base, stringCurrencyMetaDataMap.get(pair.base.getCurrencyCode()));
if (!currencies.containsKey(pair.counter))
currencies.put(pair.counter, stringCurrencyMetaDataMap.get(pair.counter.getCurrencyCode()));
}
return new ExchangeMetaData(
currencyPairs,
currencies,
exchangeMetaData.getPublicRateLimits(),
exchangeMetaData.getPrivateRateLimits(),
true);
}
static HashMap adaptCurrencyMetaData(List list) {
HashMap stringCurrencyMetaDataMap = new HashMap<>();
for (CurrenciesResponse currenciesResponse : list) {
BigDecimal precision = currenciesResponse.getPrecision();
BigDecimal withdrawalMinFee = null;
BigDecimal withdrawalMinSize = null;
if (currenciesResponse.getWithdrawalMinFee() != null) {
withdrawalMinFee = new BigDecimal(currenciesResponse.getWithdrawalMinFee());
}
if (currenciesResponse.getWithdrawalMinSize() != null) {
withdrawalMinSize = new BigDecimal(currenciesResponse.getWithdrawalMinSize());
}
WalletHealth walletHealth = getWalletHealth(currenciesResponse);
CurrencyMetaData currencyMetaData =
new CurrencyMetaData(
precision.intValue(), withdrawalMinFee, withdrawalMinSize, walletHealth);
stringCurrencyMetaDataMap.put(currenciesResponse.getCurrency(), currencyMetaData);
}
return stringCurrencyMetaDataMap;
}
/**
* @param currenciesResponse currency response which holds wallet status information
* @return WalletHealth
*/
private static WalletHealth getWalletHealth(CurrenciesResponse currenciesResponse) {
WalletHealth walletHealth = WalletHealth.ONLINE;
if (!currenciesResponse.isWithdrawEnabled() && !currenciesResponse.isDepositEnabled()) {
walletHealth = WalletHealth.OFFLINE;
} else if (!currenciesResponse.isDepositEnabled()) {
walletHealth = WalletHealth.DEPOSITS_DISABLED;
} else if (!currenciesResponse.isWithdrawEnabled()) {
walletHealth = WalletHealth.WITHDRAWALS_DISABLED;
}
return walletHealth;
}
public static OrderBook adaptOrderBook(CurrencyPair currencyPair, OrderBookResponse kc) {
Date timestamp = new Date(kc.getTime());
List asks =
kc.getAsks().stream()
.map(PriceAndSize::new)
.sorted(Ordering.natural().onResultOf(s -> s.price))
.map(s -> adaptLimitOrder(currencyPair, ASK, s, timestamp))
.collect(toCollection(LinkedList::new));
List bids =
kc.getBids().stream()
.map(PriceAndSize::new)
.sorted(Ordering.natural().onResultOf((PriceAndSize s) -> s.price).reversed())
.map(s -> adaptLimitOrder(currencyPair, BID, s, timestamp))
.collect(toCollection(LinkedList::new));
return new OrderBook(timestamp, asks, bids, true);
}
private static LimitOrder adaptLimitOrder(
CurrencyPair currencyPair, OrderType orderType, PriceAndSize priceAndSize, Date timestamp) {
return new LimitOrder.Builder(orderType, currencyPair)
.limitPrice(priceAndSize.price)
.originalAmount(priceAndSize.size)
.orderStatus(NEW)
.build();
}
public static Trades adaptTrades(
CurrencyPair currencyPair, List kucoinTrades) {
return new Trades(
kucoinTrades.stream().map(o -> adaptTrade(currencyPair, o)).collect(Collectors.toList()),
TradeSortType.SortByTimestamp);
}
public static Balance adaptBalance(AccountBalancesResponse a) {
return new Balance(Currency.getInstance(a.getCurrency()), a.getBalance(), a.getAvailable());
}
private static Trade adaptTrade(CurrencyPair currencyPair, TradeHistoryResponse trade) {
return new Trade.Builder()
.instrument(currencyPair)
.originalAmount(trade.getSize())
.price(trade.getPrice())
.timestamp(new Date(Long.parseLong(trade.getSequence())))
.type(adaptSide(trade.getSide()))
.build();
}
private static OrderType adaptSide(String side) {
return "sell".equals(side) ? ASK : BID;
}
private static String adaptSide(OrderType type) {
return type.equals(ASK) ? "sell" : "buy";
}
public static Order adaptOrder(OrderResponse order) {
OrderType orderType = adaptSide(order.getSide());
CurrencyPair currencyPair = adaptCurrencyPair(order.getSymbol());
OrderStatus status;
if (order.isCancelExist()) {
status = CANCELED;
} else if (order.isActive()) {
if (order.getDealSize().signum() == 0) {
status = NEW;
} else {
status = PARTIALLY_FILLED;
}
} else {
status = UNKNOWN;
}
Order.Builder builder;
if (StringUtils.isNotEmpty(order.getStop())) {
BigDecimal limitPrice = order.getPrice();
if (limitPrice != null && limitPrice.compareTo(BigDecimal.ZERO) == 0) {
limitPrice = null;
}
builder =
new StopOrder.Builder(orderType, currencyPair)
.limitPrice(limitPrice)
.stopPrice(order.getStopPrice());
} else {
builder = new LimitOrder.Builder(orderType, currencyPair).limitPrice(order.getPrice());
}
builder =
builder
.averagePrice(
order.getDealSize().compareTo(BigDecimal.ZERO) == 0
? MoreObjects.firstNonNull(order.getPrice(), order.getStopPrice())
: order.getDealFunds().divide(order.getDealSize(), RoundingMode.HALF_UP))
.cumulativeAmount(order.getDealSize())
.fee(order.getFee())
.id(order.getId())
.orderStatus(status)
.originalAmount(order.getSize())
.remainingAmount(order.getSize().subtract(order.getDealSize()))
.timestamp(order.getCreatedAt());
if (StringUtils.isNotEmpty(order.getTimeInForce())) {
builder.flag(TimeInForce.getTimeInForce(order.getTimeInForce()));
}
return builder instanceof StopOrder.Builder
? ((StopOrder.Builder) builder).build()
: ((LimitOrder.Builder) builder).build();
}
public static UserTrade adaptUserTrade(TradeResponse trade) {
return new UserTrade.Builder()
.currencyPair(adaptCurrencyPair(trade.getSymbol()))
.feeAmount(trade.getFee())
.feeCurrency(Currency.getInstance(trade.getFeeCurrency()))
.id(trade.getTradeId())
.orderId(trade.getOrderId())
.originalAmount(trade.getSize())
.price(trade.getPrice())
.timestamp(trade.getTradeCreatedAt())
.type(adaptSide(trade.getSide()))
.build();
}
public static UserTrade adaptHistOrder(HistOrdersResponse histOrder) {
CurrencyPair currencyPair = adaptCurrencyPair(histOrder.getSymbol());
return new UserTrade.Builder()
.currencyPair(currencyPair)
.feeAmount(histOrder.getFee())
.feeCurrency(currencyPair.base)
.id(histOrder.getId())
.originalAmount(histOrder.getAmount())
.price(histOrder.getPrice())
.timestamp(histOrder.getTradeCreatedAt())
.type(adaptSide(histOrder.getSide()))
.build();
}
public static OrderCreateApiRequest adaptLimitOrder(LimitOrder limitOrder) {
return ((OrderCreateApiRequest.OrderCreateApiRequestBuilder) adaptOrder(limitOrder))
.type("limit")
.price(limitOrder.getLimitPrice())
.postOnly(limitOrder.hasFlag(POST_ONLY))
.hidden(limitOrder.hasFlag(HIDDEN))
.iceberg(limitOrder.hasFlag(ICEBERG))
.build();
}
public static OrderCreateApiRequest adaptStopOrder(StopOrder stopOrder) {
return ((OrderCreateApiRequest.OrderCreateApiRequestBuilder) adaptOrder(stopOrder))
.type(stopOrder.getLimitPrice() == null ? "market" : "limit")
.price(stopOrder.getLimitPrice())
.stop(stopOrder.getType().equals(ASK) ? "loss" : "entry")
.stopPrice(stopOrder.getStopPrice())
.build();
}
public static OrderCreateApiRequest adaptMarketOrder(MarketOrder marketOrder) {
return ((OrderCreateApiRequest.OrderCreateApiRequestBuilder) adaptOrder(marketOrder))
.type("market")
.build();
}
/**
* Returns {@code Object} instead of the Lombok builder in order to avoid a Lombok limitation with
* Javadoc.
*/
private static Object adaptOrder(Order order) {
OrderCreateApiRequest.OrderCreateApiRequestBuilder request = OrderCreateApiRequest.builder();
boolean hasClientId = false;
for (IOrderFlags flag : order.getOrderFlags()) {
if (flag instanceof KucoinOrderFlags) {
request.clientOid(((KucoinOrderFlags) flag).getClientId());
hasClientId = true;
} else if (flag instanceof TimeInForce) {
request.timeInForce(((TimeInForce) flag).name());
}
}
if (!hasClientId) {
request.clientOid(UUID.randomUUID().toString());
}
return request
.symbol(adaptCurrencyPair((CurrencyPair) order.getInstrument()))
.size(order.getOriginalAmount())
.side(adaptSide(order.getType()));
}
private static final class PriceAndSize {
final BigDecimal price;
final BigDecimal size;
PriceAndSize(List data) {
this.price = new BigDecimal(data.get(0));
this.size = new BigDecimal(data.get(1));
}
}
public static FundingRecord adaptFundingRecord(WithdrawalResponse wr) {
FundingRecord.Builder b = new FundingRecord.Builder();
return b.setAddress(wr.getAddress())
.setAmount(wr.getAmount())
.setCurrency(Currency.getInstance(wr.getCurrency()))
.setFee(wr.getFee())
.setType(Type.WITHDRAWAL)
.setStatus(convertStatus(wr.getStatus()))
.setInternalId(wr.getId())
.setBlockchainTransactionHash(wr.getWalletTxId())
.setDescription(wr.getMemo())
.setDate(wr.getCreatedAt())
.build();
}
private static Status convertStatus(String status) {
if (status == null) {
return null;
}
switch (status) {
case "WALLET_PROCESSING":
case "PROCESSING":
return Status.PROCESSING;
case "SUCCESS":
return Status.COMPLETE;
case "FAILURE":
return Status.FAILED;
default:
throw new ExchangeException("Not supported status: " + status);
}
}
public static FundingRecord adaptFundingRecord(DepositResponse dr) {
FundingRecord.Builder b = new FundingRecord.Builder();
return b.setAddress(dr.getAddress())
.setAmount(dr.getAmount())
.setCurrency(Currency.getInstance(dr.getCurrency()))
.setFee(dr.getFee())
.setType(Type.DEPOSIT)
.setStatus(convertStatus(dr.getStatus()))
.setBlockchainTransactionHash(dr.getWalletTxId())
.setDescription(dr.getMemo())
.setDate(dr.getCreatedAt())
.build();
}
}