Files
gocryptotrader/exchanges/kucoin/kucoin_wrapper.go
Ryan O'Hara-Reid facf291069 currency/exchanges: Add bespoke exchange translator and pair matching helper (#1556)
* currency: translation and matching pairs

* Update exchanges/exchange_types.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* glorious: nits

* linter: fix?

* translation

* fix cherry pick

* gateio: translation for mbabydoge with 1e6 divisor

* okx: add translation

* cherry-pick: fix

* glorious: todos

* thrasher: nits

---------

Co-authored-by: Ryan O'Hara-Reid <ryan.oharareid@thrasher.io>
Co-authored-by: Scott <gloriousCode@users.noreply.github.com>
2024-08-16 16:47:17 +10:00

2025 lines
66 KiB
Go

package kucoin
import (
"context"
"errors"
"fmt"
"sort"
"strings"
"time"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/key"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/collateral"
"github.com/thrasher-corp/gocryptotrader/exchanges/deposit"
"github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate"
"github.com/thrasher-corp/gocryptotrader/exchanges/futures"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/margin"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
"github.com/thrasher-corp/gocryptotrader/log"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
// SetDefaults sets the basic defaults for Kucoin
func (ku *Kucoin) SetDefaults() {
ku.Name = "Kucoin"
ku.Enabled = true
ku.Verbose = false
ku.API.CredentialsValidator.RequiresKey = true
ku.API.CredentialsValidator.RequiresSecret = true
ku.API.CredentialsValidator.RequiresClientID = true
spot := currency.PairStore{
RequestFormat: &currency.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter},
ConfigFormat: &currency.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter},
}
futures := currency.PairStore{
RequestFormat: &currency.PairFormat{Uppercase: true},
ConfigFormat: &currency.PairFormat{Uppercase: true, Delimiter: currency.UnderscoreDelimiter},
}
err := ku.StoreAssetPairFormat(asset.Spot, spot)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
err = ku.StoreAssetPairFormat(asset.Margin, spot)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
err = ku.StoreAssetPairFormat(asset.Futures, futures)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
ku.Features = exchange.Features{
CurrencyTranslations: currency.NewTranslations(map[currency.Code]currency.Code{
currency.XBT: currency.BTC,
currency.USDTM: currency.USDT,
currency.USDM: currency.USD,
currency.USDCM: currency.USDC,
}),
TradingRequirements: protocol.TradingRequirements{
ClientOrderID: true,
},
Supports: exchange.FeaturesSupported{
REST: true,
Websocket: true,
RESTCapabilities: protocol.Features{
TickerFetching: true,
TickerBatching: true,
OrderbookFetching: true,
AutoPairUpdates: true,
AccountInfo: true,
CryptoWithdrawal: true,
SubmitOrder: true,
GetOrder: true,
GetOrders: true,
CancelOrder: true,
CancelOrders: true,
TradeFetching: true,
UserTradeHistory: true,
KlineFetching: true,
DepositHistory: true,
WithdrawalHistory: true,
},
WebsocketCapabilities: protocol.Features{
TickerFetching: true,
OrderbookFetching: true,
Subscribe: true,
Unsubscribe: true,
AuthenticatedEndpoints: true,
AccountInfo: true,
GetOrders: true,
TradeFetching: true,
KlineFetching: true,
GetOrder: true,
},
FuturesCapabilities: exchange.FuturesCapabilities{
Positions: true,
Leverage: true,
CollateralMode: true,
FundingRates: true,
MaximumFundingRateHistory: kline.ThreeMonth.Duration(),
SupportedFundingRateFrequencies: map[kline.Interval]bool{
kline.EightHour: true,
},
FundingRateBatching: map[asset.Item]bool{
asset.Futures: true,
},
OpenInterest: exchange.OpenInterestSupport{
Supported: true,
SupportedViaTicker: true,
SupportsRestBatch: true,
},
},
MaximumOrderHistory: kline.OneDay.Duration() * 7,
WithdrawPermissions: exchange.AutoWithdrawCrypto,
},
Enabled: exchange.FeaturesEnabled{
AutoPairUpdates: true,
Kline: kline.ExchangeCapabilitiesEnabled{
Intervals: kline.DeployExchangeIntervals(
kline.IntervalCapacity{Interval: kline.OneMin},
kline.IntervalCapacity{Interval: kline.ThreeMin},
kline.IntervalCapacity{Interval: kline.FiveMin},
kline.IntervalCapacity{Interval: kline.FifteenMin},
kline.IntervalCapacity{Interval: kline.ThirtyMin},
kline.IntervalCapacity{Interval: kline.OneHour},
kline.IntervalCapacity{Interval: kline.TwoHour},
kline.IntervalCapacity{Interval: kline.FourHour},
kline.IntervalCapacity{Interval: kline.SixHour},
kline.IntervalCapacity{Interval: kline.EightHour},
kline.IntervalCapacity{Interval: kline.TwelveHour},
kline.IntervalCapacity{Interval: kline.OneDay},
kline.IntervalCapacity{Interval: kline.OneWeek},
),
GlobalResultLimit: 1500,
},
},
Subscriptions: defaultSubscriptions.Clone(),
}
ku.Requester, err = request.New(ku.Name,
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout),
request.WithLimiter(GetRateLimit()))
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
ku.API.Endpoints = ku.NewEndpoints()
err = ku.API.Endpoints.SetDefaultEndpoints(map[exchange.URL]string{
exchange.RestSpot: kucoinAPIURL,
exchange.RestFutures: kucoinFuturesAPIURL,
exchange.WebsocketSpot: kucoinWebsocketURL,
})
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
ku.Websocket = stream.NewWebsocket()
ku.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit
ku.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout
ku.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit
}
// Setup takes in the supplied exchange configuration details and sets params
func (ku *Kucoin) Setup(exch *config.Exchange) error {
err := exch.Validate()
if err != nil {
return err
}
if !exch.Enabled {
ku.SetEnabled(false)
return nil
}
err = ku.SetupDefaults(exch)
if err != nil {
return err
}
ku.checkSubscriptions()
wsRunningEndpoint, err := ku.API.Endpoints.GetURL(exchange.WebsocketSpot)
if err != nil {
return err
}
err = ku.Websocket.Setup(
&stream.WebsocketSetup{
ExchangeConfig: exch,
DefaultURL: kucoinWebsocketURL,
RunningURL: wsRunningEndpoint,
Connector: ku.WsConnect,
Subscriber: ku.Subscribe,
Unsubscriber: ku.Unsubscribe,
GenerateSubscriptions: ku.generateSubscriptions,
Features: &ku.Features.Supports.WebsocketCapabilities,
OrderbookBufferConfig: buffer.Config{
SortBuffer: true,
SortBufferByUpdateIDs: true,
UpdateIDProgression: true,
},
TradeFeed: ku.Features.Enabled.TradeFeed,
})
if err != nil {
return err
}
return ku.Websocket.SetupNewConnection(stream.ConnectionSetup{
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
RateLimit: 500,
})
}
// FetchTradablePairs returns a list of the exchanges tradable pairs
func (ku *Kucoin) FetchTradablePairs(ctx context.Context, assetType asset.Item) (currency.Pairs, error) {
var cp currency.Pair
switch assetType {
case asset.Futures:
myPairs, err := ku.GetFuturesOpenContracts(ctx)
if err != nil {
return nil, err
}
pairs := make(currency.Pairs, 0, len(myPairs))
for x := range myPairs {
if strings.ToLower(myPairs[x].Status) != "open" { //nolint:gocritic // strings.ToLower is faster
continue
}
cp, err = currency.NewPairFromStrings(myPairs[x].BaseCurrency, myPairs[x].Symbol[len(myPairs[x].BaseCurrency):])
if err != nil {
return nil, err
}
pairs = pairs.Add(cp)
}
configFormat, err := ku.GetPairFormat(asset.Futures, false)
if err != nil {
return nil, err
}
return pairs.Format(configFormat), nil
case asset.Spot, asset.Margin:
myPairs, err := ku.GetSymbols(ctx, "")
if err != nil {
return nil, err
}
pairs := make(currency.Pairs, 0, len(myPairs))
for x := range myPairs {
if !myPairs[x].EnableTrading || (assetType == asset.Margin && !myPairs[x].IsMarginEnabled) {
continue
}
// Symbol field must be used to generate pair as this is the symbol
// to fetch data from the API. e.g. BSV-USDT name is BCHSV-USDT as symbol.
cp, err = currency.NewPairFromString(strings.ToUpper(myPairs[x].Symbol))
if err != nil {
return nil, err
}
pairs = pairs.Add(cp)
}
return pairs, nil
default:
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType)
}
}
// UpdateTradablePairs updates the exchanges available pairs and stores
// them in the exchanges config
func (ku *Kucoin) UpdateTradablePairs(ctx context.Context, forceUpdate bool) error {
assets := ku.GetAssetTypes(true)
for a := range assets {
pairs, err := ku.FetchTradablePairs(ctx, assets[a])
if err != nil {
return err
}
if len(pairs) == 0 {
return fmt.Errorf("%v; no tradable pairs", currency.ErrCurrencyPairsEmpty)
}
err = ku.UpdatePairs(pairs, assets[a], false, forceUpdate)
if err != nil {
return err
}
}
return nil
}
// UpdateTicker updates and returns the ticker for a currency pair
func (ku *Kucoin) UpdateTicker(ctx context.Context, p currency.Pair, assetType asset.Item) (*ticker.Price, error) {
p, err := ku.FormatExchangeCurrency(p, assetType)
if err != nil {
return nil, err
}
if err := ku.UpdateTickers(ctx, assetType); err != nil {
return nil, err
}
return ticker.GetTicker(ku.Name, p, assetType)
}
// UpdateTickers updates all currency pairs of a given asset type
func (ku *Kucoin) UpdateTickers(ctx context.Context, assetType asset.Item) error {
var errs error
switch assetType {
case asset.Futures:
ticks, err := ku.GetFuturesOpenContracts(ctx)
if err != nil {
return err
}
pairs, err := ku.GetEnabledPairs(asset.Futures)
if err != nil {
return err
}
for x := range ticks {
var pair currency.Pair
pair, err = currency.NewPairFromStrings(ticks[x].BaseCurrency, ticks[x].Symbol[len(ticks[x].BaseCurrency):])
if err != nil {
return err
}
if !pairs.Contains(pair, true) {
continue
}
err = ticker.ProcessTicker(&ticker.Price{
Last: ticks[x].LastTradePrice,
High: ticks[x].HighPrice,
Low: ticks[x].LowPrice,
Volume: ticks[x].VolumeOf24h,
OpenInterest: ticks[x].OpenInterest.Float64(),
Pair: pair,
ExchangeName: ku.Name,
AssetType: assetType,
})
if err != nil {
errs = common.AppendError(errs, err)
}
}
case asset.Spot, asset.Margin:
ticks, err := ku.GetTickers(ctx)
if err != nil {
return err
}
for t := range ticks.Tickers {
pair, enabled, err := ku.MatchSymbolCheckEnabled(ticks.Tickers[t].Symbol, assetType, true)
if err != nil && !errors.Is(err, currency.ErrPairNotFound) {
return err
}
if !enabled {
continue
}
err = ticker.ProcessTicker(&ticker.Price{
Last: ticks.Tickers[t].Last,
High: ticks.Tickers[t].High,
Low: ticks.Tickers[t].Low,
Volume: ticks.Tickers[t].Volume,
Ask: ticks.Tickers[t].Sell,
Bid: ticks.Tickers[t].Buy,
Pair: pair,
ExchangeName: ku.Name,
AssetType: assetType,
LastUpdated: ticks.Time.Time(),
})
if err != nil {
errs = common.AppendError(errs, err)
}
}
default:
return fmt.Errorf("%w %v", asset.ErrNotSupported, assetType)
}
return errs
}
// FetchTicker returns the ticker for a currency pair
func (ku *Kucoin) FetchTicker(ctx context.Context, p currency.Pair, assetType asset.Item) (*ticker.Price, error) {
p, err := ku.FormatExchangeCurrency(p, assetType)
if err != nil {
return nil, err
}
tickerNew, err := ticker.GetTicker(ku.Name, p, assetType)
if err != nil {
return ku.UpdateTicker(ctx, p, assetType)
}
return tickerNew, nil
}
// FetchOrderbook returns orderbook base on the currency pair
func (ku *Kucoin) FetchOrderbook(ctx context.Context, pair currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
pair, err := ku.FormatExchangeCurrency(pair, assetType)
if err != nil {
return nil, err
}
ob, err := orderbook.Get(ku.Name, pair, assetType)
if err != nil {
return ku.UpdateOrderbook(ctx, pair, assetType)
}
return ob, nil
}
// UpdateOrderbook updates and returns the orderbook for a currency pair
func (ku *Kucoin) UpdateOrderbook(ctx context.Context, pair currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
err := ku.CurrencyPairs.IsAssetEnabled(assetType)
if err != nil {
return nil, err
}
pair, err = ku.FormatExchangeCurrency(pair, assetType)
if err != nil {
return nil, err
}
var ordBook *Orderbook
switch assetType {
case asset.Futures:
ordBook, err = ku.GetFuturesOrderbook(ctx, pair.String())
case asset.Spot, asset.Margin:
if ku.IsRESTAuthenticationSupported() && ku.AreCredentialsValid(ctx) {
ordBook, err = ku.GetOrderbook(ctx, pair.String())
if err != nil {
return nil, err
}
} else {
ordBook, err = ku.GetPartOrderbook100(ctx, pair.String())
}
default:
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType)
}
if err != nil {
return nil, err
}
book := &orderbook.Base{
Exchange: ku.Name,
Pair: pair,
Asset: assetType,
VerifyOrderbook: ku.CanVerifyOrderbook,
Asks: ordBook.Asks,
Bids: ordBook.Bids,
}
err = book.Process()
if err != nil {
return book, err
}
return orderbook.Get(ku.Name, pair, assetType)
}
// UpdateAccountInfo retrieves balances for all enabled currencies
func (ku *Kucoin) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
holding := account.Holdings{Exchange: ku.Name}
err := ku.CurrencyPairs.IsAssetEnabled(assetType)
if err != nil {
return holding, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType)
}
switch assetType {
case asset.Futures:
balances := make([]account.Balance, 2)
for i, settlement := range []string{"XBT", "USDT"} {
accountH, err := ku.GetFuturesAccountOverview(ctx, settlement)
if err != nil {
return account.Holdings{}, err
}
balances[i] = account.Balance{
Currency: currency.NewCode(accountH.Currency),
Total: accountH.AvailableBalance + accountH.FrozenFunds,
Hold: accountH.FrozenFunds,
Free: accountH.AvailableBalance,
}
}
holding.Accounts = append(holding.Accounts, account.SubAccount{
AssetType: assetType,
Currencies: balances,
})
case asset.Spot, asset.Margin:
accountH, err := ku.GetAllAccounts(ctx, "", ku.accountTypeToString(assetType))
if err != nil {
return account.Holdings{}, err
}
for x := range accountH {
holding.Accounts = append(holding.Accounts, account.SubAccount{
AssetType: assetType,
Currencies: []account.Balance{
{
Currency: currency.NewCode(accountH[x].Currency),
Total: accountH[x].Balance,
Hold: accountH[x].Holds,
Free: accountH[x].Available,
}},
})
}
default:
return holding, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType)
}
return holding, nil
}
// FetchAccountInfo retrieves balances for all enabled currencies
func (ku *Kucoin) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
creds, err := ku.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
acc, err := account.GetHoldings(ku.Name, creds, assetType)
if err != nil {
return ku.UpdateAccountInfo(ctx, assetType)
}
return acc, nil
}
// GetAccountFundingHistory returns funding history, deposits and
// withdrawals
func (ku *Kucoin) GetAccountFundingHistory(ctx context.Context) ([]exchange.FundingHistory, error) {
withdrawalsData, err := ku.GetWithdrawalList(ctx, "", "", time.Time{}, time.Time{})
if err != nil {
return nil, err
}
depositsData, err := ku.GetHistoricalDepositList(ctx, "", "", time.Time{}, time.Time{})
if err != nil {
return nil, err
}
fundingData := make([]exchange.FundingHistory, len(withdrawalsData.Items)+len(depositsData.Items))
for x := range depositsData.Items {
fundingData[x] = exchange.FundingHistory{
Timestamp: depositsData.Items[x].CreatedAt.Time(),
ExchangeName: ku.Name,
TransferType: "deposit",
CryptoTxID: depositsData.Items[x].WalletTxID,
Status: depositsData.Items[x].Status,
Amount: depositsData.Items[x].Amount,
Currency: depositsData.Items[x].Currency,
}
}
length := len(depositsData.Items)
for x := range withdrawalsData.Items {
fundingData[length+x] = exchange.FundingHistory{
Fee: withdrawalsData.Items[x].Fee,
Timestamp: withdrawalsData.Items[x].UpdatedAt.Time(),
ExchangeName: ku.Name,
TransferType: "withdrawal",
CryptoToAddress: withdrawalsData.Items[x].Address,
CryptoTxID: withdrawalsData.Items[x].WalletTxID,
Status: withdrawalsData.Items[x].Status,
Amount: withdrawalsData.Items[x].Amount,
Currency: withdrawalsData.Items[x].Currency,
TransferID: withdrawalsData.Items[x].ID,
}
}
return fundingData, nil
}
// GetWithdrawalsHistory returns previous withdrawals data
func (ku *Kucoin) GetWithdrawalsHistory(ctx context.Context, c currency.Code, a asset.Item) ([]exchange.WithdrawalHistory, error) {
err := ku.CurrencyPairs.IsAssetEnabled(a)
if err != nil {
return nil, err
}
switch a {
case asset.Spot:
var withdrawals *HistoricalDepositWithdrawalResponse
withdrawals, err = ku.GetHistoricalWithdrawalList(ctx, c.String(), "", time.Time{}, time.Time{}, 0, 0)
if err != nil {
return nil, err
}
resp := make([]exchange.WithdrawalHistory, len(withdrawals.Items))
for x := range withdrawals.Items {
resp[x] = exchange.WithdrawalHistory{
Status: withdrawals.Items[x].Status,
CryptoTxID: withdrawals.Items[x].WalletTxID,
Timestamp: withdrawals.Items[x].CreatedAt.Time(),
Amount: withdrawals.Items[x].Amount,
TransferType: "withdrawal",
Currency: c.String(),
}
}
return resp, nil
case asset.Futures:
var futuresWithdrawals *FuturesWithdrawalsListResponse
futuresWithdrawals, err = ku.GetFuturesWithdrawalList(ctx, c.String(), "", time.Time{}, time.Time{})
if err != nil {
return nil, err
}
resp := make([]exchange.WithdrawalHistory, len(futuresWithdrawals.Items))
for y := range futuresWithdrawals.Items {
resp[y] = exchange.WithdrawalHistory{
Status: futuresWithdrawals.Items[y].Status,
CryptoTxID: futuresWithdrawals.Items[y].WalletTxID,
Timestamp: futuresWithdrawals.Items[y].CreatedAt.Time(),
Amount: futuresWithdrawals.Items[y].Amount,
Currency: c.String(),
TransferType: "withdrawal",
}
}
return resp, nil
default:
return nil, fmt.Errorf("withdrawal %w for asset type %v", asset.ErrNotSupported, a)
}
}
// GetRecentTrades returns the most recent trades for a currency and asset
func (ku *Kucoin) GetRecentTrades(ctx context.Context, p currency.Pair, assetType asset.Item) ([]trade.Data, error) {
p, err := ku.FormatExchangeCurrency(p, assetType)
if err != nil {
return nil, err
}
var resp []trade.Data
switch assetType {
case asset.Futures:
tradeData, err := ku.GetFuturesTradeHistory(ctx, p.String())
if err != nil {
return nil, err
}
var side order.Side
for i := range tradeData {
side, err = order.StringToOrderSide(tradeData[0].Side)
if err != nil {
return nil, err
}
resp = append(resp, trade.Data{
TID: tradeData[i].TradeID,
Exchange: ku.Name,
CurrencyPair: p,
AssetType: assetType,
Price: tradeData[i].Price,
Amount: tradeData[i].Size,
Timestamp: tradeData[i].FilledTime.Time(),
Side: side,
})
}
case asset.Spot, asset.Margin:
tradeData, err := ku.GetTradeHistory(ctx, p.String())
if err != nil {
return nil, err
}
var side order.Side
for i := range tradeData {
side, err = order.StringToOrderSide(tradeData[0].Side)
if err != nil {
return nil, err
}
resp = append(resp, trade.Data{
TID: tradeData[i].Sequence,
Exchange: ku.Name,
CurrencyPair: p,
Side: side,
AssetType: assetType,
Price: tradeData[i].Price,
Amount: tradeData[i].Size,
Timestamp: tradeData[i].Time.Time(),
})
}
default:
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType)
}
if ku.IsSaveTradeDataEnabled() {
err := trade.AddTradesToBuffer(ku.Name, resp...)
if err != nil {
return nil, err
}
}
sort.Sort(trade.ByDate(resp))
return resp, nil
}
// GetHistoricTrades returns historic trade data within the timeframe provided
func (ku *Kucoin) GetHistoricTrades(_ context.Context, _ currency.Pair, _ asset.Item, _, _ time.Time) ([]trade.Data, error) {
return nil, common.ErrFunctionNotSupported
}
// SubmitOrder submits a new order
func (ku *Kucoin) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) {
err := s.Validate(ku.GetTradingRequirements())
if err != nil {
return nil, err
}
sideString, err := ku.orderSideString(s.Side)
if err != nil {
return nil, err
}
if s.Type != order.UnknownType && s.Type != order.Limit && s.Type != order.Market {
return nil, fmt.Errorf("%w only limit and market are supported", order.ErrTypeIsInvalid)
}
s.Pair, err = ku.FormatExchangeCurrency(s.Pair, s.AssetType)
if err != nil {
return nil, err
}
switch s.AssetType {
case asset.Futures:
o, err := ku.PostFuturesOrder(ctx, &FuturesOrderParam{
ClientOrderID: s.ClientOrderID,
Side: sideString,
Symbol: s.Pair,
OrderType: s.Type.Lower(),
Size: s.Amount,
Price: s.Price,
StopPrice: s.TriggerPrice,
Leverage: s.Leverage,
ReduceOnly: s.ReduceOnly,
PostOnly: s.PostOnly,
Hidden: s.Hidden})
if err != nil {
return nil, err
}
return s.DeriveSubmitResponse(o)
case asset.Spot:
timeInForce := ""
if s.Type == order.Limit {
switch {
case s.FillOrKill:
timeInForce = "FOK"
case s.ImmediateOrCancel:
timeInForce = "IOC"
case s.PostOnly:
default:
timeInForce = "GTC"
}
}
o, err := ku.PostOrder(ctx, &SpotOrderParam{
ClientOrderID: s.ClientOrderID,
Side: sideString,
Symbol: s.Pair,
OrderType: s.Type.Lower(),
Size: s.Amount,
Price: s.Price,
PostOnly: s.PostOnly,
Hidden: s.Hidden,
TimeInForce: timeInForce,
})
if err != nil {
return nil, err
}
return s.DeriveSubmitResponse(o)
case asset.Margin:
o, err := ku.PostMarginOrder(ctx,
&MarginOrderParam{ClientOrderID: s.ClientOrderID,
Side: sideString, Symbol: s.Pair,
OrderType: s.Type.Lower(), MarginMode: marginModeToString(s.MarginType),
Price: s.Price, Size: s.Amount,
VisibleSize: s.Amount, PostOnly: s.PostOnly,
Hidden: s.Hidden, AutoBorrow: s.AutoBorrow})
if err != nil {
return nil, err
}
ret, err := s.DeriveSubmitResponse(o.OrderID)
if err != nil {
return nil, err
}
ret.BorrowSize = o.BorrowSize
ret.LoanApplyID = o.LoanApplyID
return ret, nil
default:
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, s.AssetType)
}
}
func marginModeToString(mType margin.Type) string {
switch mType {
case margin.Isolated:
return mType.String()
case margin.Multi:
return "cross"
default:
return ""
}
}
// ModifyOrder will allow of changing orderbook placement and limit to
// market conversion
func (ku *Kucoin) ModifyOrder(_ context.Context, _ *order.Modify) (*order.ModifyResponse, error) {
return nil, common.ErrFunctionNotSupported
}
// CancelOrder cancels an order by its corresponding ID number
func (ku *Kucoin) CancelOrder(ctx context.Context, ord *order.Cancel) error {
if ord == nil {
return common.ErrNilPointer
}
err := ku.CurrencyPairs.IsAssetEnabled(ord.AssetType)
if err != nil {
return err
}
err = ord.Validate(ord.StandardCancel())
if err != nil {
return err
}
switch ord.AssetType {
case asset.Spot, asset.Margin:
if ord.OrderID == "" && ord.ClientOrderID == "" {
return errors.New("either OrderID or ClientSuppliedOrderID must be specified")
}
if ord.OrderID != "" {
_, err = ku.CancelSingleOrder(ctx, ord.OrderID)
} else if ord.ClientOrderID != "" || ord.ClientID != "" {
if ord.ClientID != "" && ord.ClientOrderID == "" {
ord.ClientOrderID = ord.ClientID
}
_, err = ku.CancelOrderByClientOID(ctx, ord.ClientOrderID)
}
return err
case asset.Futures:
_, err := ku.CancelFuturesOrder(ctx, ord.OrderID)
if err != nil {
return err
}
}
return nil
}
// CancelBatchOrders cancels orders by their corresponding ID numbers
func (ku *Kucoin) CancelBatchOrders(_ context.Context, _ []order.Cancel) (*order.CancelBatchResponse, error) {
return nil, common.ErrFunctionNotSupported
}
// CancelAllOrders cancels all orders associated with a currency pair
func (ku *Kucoin) CancelAllOrders(ctx context.Context, orderCancellation *order.Cancel) (order.CancelAllResponse, error) {
if orderCancellation == nil {
return order.CancelAllResponse{}, common.ErrNilPointer
}
if err := ku.CurrencyPairs.IsAssetEnabled(orderCancellation.AssetType); err != nil {
return order.CancelAllResponse{}, err
}
result := order.CancelAllResponse{}
err := orderCancellation.Validate()
if err != nil {
return result, err
}
var pairString string
if !orderCancellation.Pair.IsEmpty() {
orderCancellation.Pair, err = ku.FormatExchangeCurrency(orderCancellation.Pair, orderCancellation.AssetType)
if err != nil {
return result, err
}
pairString = orderCancellation.Pair.String()
}
var values []string
switch orderCancellation.AssetType {
case asset.Margin, asset.Spot:
tradeType := ku.accountToTradeTypeString(orderCancellation.AssetType, marginModeToString(orderCancellation.MarginType))
values, err = ku.CancelAllOpenOrders(ctx, pairString, tradeType)
if err != nil {
return order.CancelAllResponse{}, err
}
case asset.Futures:
values, err = ku.CancelAllFuturesOpenOrders(ctx, orderCancellation.Pair.String())
if err != nil {
return result, err
}
stopOrders, err := ku.CancelAllFuturesStopOrders(ctx, orderCancellation.Pair.String())
if err != nil {
return result, err
}
values = append(values, stopOrders...)
default:
return order.CancelAllResponse{}, fmt.Errorf("%w %v", asset.ErrNotSupported, orderCancellation.AssetType)
}
result.Status = map[string]string{}
for x := range values {
result.Status[values[x]] = order.Cancelled.String()
}
return result, nil
}
// GetOrderInfo returns order information based on order ID
func (ku *Kucoin) GetOrderInfo(ctx context.Context, orderID string, pair currency.Pair, assetType asset.Item) (*order.Detail, error) {
if err := ku.CurrencyPairs.IsAssetEnabled(assetType); err != nil {
return nil, err
}
pair, err := ku.FormatExchangeCurrency(pair, assetType)
if err != nil {
return nil, err
}
switch assetType {
case asset.Futures:
orderDetail, err := ku.GetFuturesOrderDetails(ctx, orderID)
if err != nil {
return nil, err
}
var nPair currency.Pair
nPair, err = ku.MatchSymbolWithAvailablePairs(orderDetail.Symbol, assetType, true)
if err != nil {
return nil, err
}
oType, err := order.StringToOrderType(orderDetail.OrderType)
if err != nil {
return nil, err
}
side, err := order.StringToOrderSide(orderDetail.Side)
if err != nil {
return nil, err
}
if side == order.Sell {
side = order.Short
} else if side == order.Buy {
side = order.Long
}
if !pair.IsEmpty() && !nPair.Equal(pair) {
return nil, fmt.Errorf("order with id %s and currency Pair %v does not exist", orderID, pair)
}
return &order.Detail{
Exchange: ku.Name,
OrderID: orderDetail.ID,
Pair: pair,
Type: oType,
Side: side,
AssetType: assetType,
ExecutedAmount: orderDetail.DealSize,
RemainingAmount: orderDetail.Size - orderDetail.DealSize,
Amount: orderDetail.Size,
Price: orderDetail.Price,
Date: orderDetail.CreatedAt.Time()}, nil
case asset.Spot, asset.Margin:
orderDetail, err := ku.GetOrderByID(ctx, orderID)
if err != nil {
return nil, err
}
nPair, err := currency.NewPairFromString(orderDetail.Symbol)
if err != nil {
return nil, err
}
oType, err := order.StringToOrderType(orderDetail.Type)
if err != nil {
return nil, err
}
side, err := order.StringToOrderSide(orderDetail.Side)
if err != nil {
return nil, err
}
if !pair.IsEmpty() && !nPair.Equal(pair) {
return nil, fmt.Errorf("order with id %s and currency Pair %v does not exist", orderID, pair)
}
return &order.Detail{
Exchange: ku.Name,
OrderID: orderDetail.ID,
Pair: pair,
Type: oType,
Side: side,
Fee: orderDetail.Fee,
AssetType: assetType,
ExecutedAmount: orderDetail.DealSize,
RemainingAmount: orderDetail.Size - orderDetail.DealSize,
Amount: orderDetail.Size,
Price: orderDetail.Price,
Date: orderDetail.CreatedAt.Time(),
}, nil
default:
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType)
}
}
// GetDepositAddress returns a deposit address for a specified currency
func (ku *Kucoin) GetDepositAddress(ctx context.Context, c currency.Code, _, _ string) (*deposit.Address, error) {
ad, err := ku.GetDepositAddressesV2(ctx, c.Upper().String())
if err != nil {
fad, err := ku.GetFuturesDepositAddress(ctx, c.String())
if err != nil {
return nil, err
}
return &deposit.Address{
Address: fad.Address,
Chain: fad.Chain,
Tag: fad.Memo,
}, nil
}
if len(ad) > 1 {
return nil, errMultipleDepositAddress
} else if len(ad) == 0 {
return nil, errNoDepositAddress
}
return &deposit.Address{
Address: ad[0].Address,
Chain: ad[0].Chain,
Tag: ad[0].Memo,
}, nil
}
// WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is
// submitted
// The endpoint was deprecated for futures, please transfer assets from the FUTURES account to the MAIN account first,
// and then withdraw from the MAIN account
func (ku *Kucoin) WithdrawCryptocurrencyFunds(ctx context.Context, withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) {
if err := withdrawRequest.Validate(); err != nil {
return nil, err
}
withdrawalID, err := ku.ApplyWithdrawal(ctx, withdrawRequest.Currency.String(), withdrawRequest.Crypto.Address, withdrawRequest.Crypto.AddressTag, withdrawRequest.Description, withdrawRequest.Crypto.Chain, "INTERNAL", false, withdrawRequest.Amount)
if err != nil {
return nil, err
}
return &withdraw.ExchangeResponse{
ID: withdrawalID,
}, nil
}
// WithdrawFiatFunds returns a withdrawal ID when a withdrawal is submitted
func (ku *Kucoin) WithdrawFiatFunds(_ context.Context, _ *withdraw.Request) (*withdraw.ExchangeResponse, error) {
return nil, common.ErrFunctionNotSupported
}
// WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a withdrawal is
// submitted
func (ku *Kucoin) WithdrawFiatFundsToInternationalBank(_ context.Context, _ *withdraw.Request) (*withdraw.ExchangeResponse, error) {
return nil, common.ErrFunctionNotSupported
}
func orderTypeToString(oType order.Type) (string, error) {
switch oType {
case order.Limit:
return "limit", nil
case order.Market:
return "market", nil
case order.StopLimit:
return "limit_stop", nil
case order.StopMarket:
return "market_stop", nil
case order.AnyType, order.UnknownType:
return "", nil
default:
return "", order.ErrUnsupportedOrderType
}
}
// GetActiveOrders retrieves any orders that are active/open
func (ku *Kucoin) GetActiveOrders(ctx context.Context, getOrdersRequest *order.MultiOrderRequest) (order.FilteredOrders, error) {
if getOrdersRequest == nil {
return nil, common.ErrNilPointer
}
err := ku.CurrencyPairs.IsAssetEnabled(getOrdersRequest.AssetType)
if err != nil {
return nil, err
}
if getOrdersRequest.Validate() != nil {
return nil, err
}
format, err := ku.GetPairFormat(getOrdersRequest.AssetType, true)
if err != nil {
return nil, err
}
var pair string
var orders []order.Detail
switch getOrdersRequest.AssetType {
case asset.Futures:
if len(getOrdersRequest.Pairs) == 1 {
pair = format.Format(getOrdersRequest.Pairs[0])
}
sideString, err := ku.orderSideString(getOrdersRequest.Side)
if err != nil {
return nil, err
}
oType, err := orderTypeToString(getOrdersRequest.Type)
if err != nil {
return nil, err
}
futuresOrders, err := ku.GetFuturesOrders(ctx, "active", pair, sideString, oType, getOrdersRequest.StartTime, getOrdersRequest.EndTime)
if err != nil {
return nil, err
}
for x := range futuresOrders.Items {
if !futuresOrders.Items[x].IsActive {
continue
}
var dPair currency.Pair
var enabled bool
dPair, enabled, err = ku.MatchSymbolCheckEnabled(futuresOrders.Items[x].Symbol, getOrdersRequest.AssetType, false)
if err != nil {
return nil, err
}
if !enabled {
continue
}
side, err := order.StringToOrderSide(futuresOrders.Items[x].Side)
if err != nil {
return nil, err
}
if side == order.Sell {
side = order.Short
} else if side == order.Buy {
side = order.Long
}
oType, err := order.StringToOrderType(futuresOrders.Items[x].OrderType)
if err != nil {
return nil, fmt.Errorf("asset type: %v order type: %v err: %w", getOrdersRequest.AssetType, getOrdersRequest.Type, err)
}
status, err := order.StringToOrderStatus(futuresOrders.Items[x].Status)
if err != nil {
return nil, err
}
orders = append(orders, order.Detail{
OrderID: futuresOrders.Items[x].ID,
ClientOrderID: futuresOrders.Items[x].ClientOid,
Amount: futuresOrders.Items[x].Size,
ContractAmount: futuresOrders.Items[x].Size,
RemainingAmount: futuresOrders.Items[x].Size - futuresOrders.Items[x].FilledSize,
ExecutedAmount: futuresOrders.Items[x].FilledSize,
Exchange: ku.Name,
Date: futuresOrders.Items[x].CreatedAt.Time(),
LastUpdated: futuresOrders.Items[x].UpdatedAt.Time(),
Price: futuresOrders.Items[x].Price,
Side: side,
Type: oType,
Pair: dPair,
PostOnly: futuresOrders.Items[x].PostOnly,
ReduceOnly: futuresOrders.Items[x].ReduceOnly,
Status: status,
SettlementCurrency: currency.NewCode(futuresOrders.Items[x].SettleCurrency),
Leverage: futuresOrders.Items[x].Leverage,
AssetType: getOrdersRequest.AssetType,
HiddenOrder: futuresOrders.Items[x].Hidden,
})
}
case asset.Spot, asset.Margin:
if len(getOrdersRequest.Pairs) == 1 {
pair = format.Format(getOrdersRequest.Pairs[0])
}
sideString, err := ku.orderSideString(getOrdersRequest.Side)
if err != nil {
return nil, err
}
oType, err := ku.orderTypeToString(getOrdersRequest.Type)
if err != nil {
return nil, fmt.Errorf("asset type: %v order type: %v err: %w", getOrdersRequest.AssetType, getOrdersRequest.Type, err)
}
spotOrders, err := ku.ListOrders(ctx, "active", pair, sideString, oType, "", getOrdersRequest.StartTime, getOrdersRequest.EndTime)
if err != nil {
return nil, err
}
for x := range spotOrders.Items {
if !spotOrders.Items[x].IsActive {
continue
}
var dPair currency.Pair
var isEnabled bool
dPair, isEnabled, err = ku.MatchSymbolCheckEnabled(spotOrders.Items[x].Symbol, getOrdersRequest.AssetType, true)
if err != nil {
return nil, err
}
if !isEnabled {
continue
}
if len(getOrdersRequest.Pairs) > 0 && !getOrdersRequest.Pairs.Contains(dPair, true) {
continue
}
side, err := order.StringToOrderSide(spotOrders.Items[x].Side)
if err != nil {
return nil, err
}
oType, err := order.StringToOrderType(spotOrders.Items[x].TradeType)
if err != nil {
return nil, err
}
orders = append(orders, order.Detail{
OrderID: spotOrders.Items[x].ID,
Amount: spotOrders.Items[x].Size,
RemainingAmount: spotOrders.Items[x].Size - spotOrders.Items[x].DealSize,
ExecutedAmount: spotOrders.Items[x].DealSize,
Exchange: ku.Name,
Date: spotOrders.Items[x].CreatedAt.Time(),
Price: spotOrders.Items[x].Price,
Side: side,
Type: oType,
Pair: dPair,
})
}
default:
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, getOrdersRequest.AssetType)
}
return orders, nil
}
// GetOrderHistory retrieves account order information
// Can Limit response to specific order status
func (ku *Kucoin) GetOrderHistory(ctx context.Context, getOrdersRequest *order.MultiOrderRequest) (order.FilteredOrders, error) {
if getOrdersRequest == nil {
return nil, common.ErrNilPointer
}
err := ku.CurrencyPairs.IsAssetEnabled(getOrdersRequest.AssetType)
if err != nil {
return nil, err
}
if getOrdersRequest.Validate() != nil {
return nil, err
}
var sideString string
sideString, err = ku.orderSideString(getOrdersRequest.Side)
if err != nil {
return nil, err
}
var orders []order.Detail
var orderSide order.Side
var orderStatus order.Status
var oType order.Type
var pair currency.Pair
switch getOrdersRequest.AssetType {
case asset.Futures:
var futuresOrders *FutureOrdersResponse
var newOrders *FutureOrdersResponse
if len(getOrdersRequest.Pairs) == 0 {
futuresOrders, err = ku.GetFuturesOrders(ctx, "", "", sideString, getOrdersRequest.Type.Lower(), getOrdersRequest.StartTime, getOrdersRequest.EndTime)
if err != nil {
return nil, err
}
} else {
for x := range getOrdersRequest.Pairs {
getOrdersRequest.Pairs[x], err = ku.FormatExchangeCurrency(getOrdersRequest.Pairs[x], getOrdersRequest.AssetType)
if err != nil {
return nil, err
}
newOrders, err = ku.GetFuturesOrders(ctx, "", getOrdersRequest.Pairs[x].String(), sideString, getOrdersRequest.Type.Lower(), getOrdersRequest.StartTime, getOrdersRequest.EndTime)
if err != nil {
return nil, fmt.Errorf("%w while fetching for symbol %s", err, getOrdersRequest.Pairs[x].String())
}
if futuresOrders == nil {
futuresOrders = newOrders
} else {
futuresOrders.Items = append(futuresOrders.Items, newOrders.Items...)
}
}
}
orders = make(order.FilteredOrders, 0, len(futuresOrders.Items))
for i := range orders {
orderSide, err = order.StringToOrderSide(futuresOrders.Items[i].Side)
if err != nil {
return nil, err
}
var isEnabled bool
pair, isEnabled, err = ku.MatchSymbolCheckEnabled(futuresOrders.Items[i].Symbol, getOrdersRequest.AssetType, true)
if err != nil {
return nil, err
}
if !isEnabled {
continue
}
oType, err = order.StringToOrderType(futuresOrders.Items[i].OrderType)
if err != nil {
log.Errorf(log.ExchangeSys, "%s %v", ku.Name, err)
}
orders = append(orders, order.Detail{
Price: futuresOrders.Items[i].Price,
Amount: futuresOrders.Items[i].Size,
ExecutedAmount: futuresOrders.Items[i].DealSize,
RemainingAmount: futuresOrders.Items[i].Size - futuresOrders.Items[i].DealSize,
Date: futuresOrders.Items[i].CreatedAt.Time(),
Exchange: ku.Name,
OrderID: futuresOrders.Items[i].ID,
Side: orderSide,
Status: orderStatus,
Type: oType,
Pair: pair,
})
orders[i].InferCostsAndTimes()
}
case asset.Spot, asset.Margin:
var responseOrders *OrdersListResponse
var newOrders *OrdersListResponse
if len(getOrdersRequest.Pairs) == 0 {
responseOrders, err = ku.ListOrders(ctx, "", "", sideString, getOrdersRequest.Type.Lower(), "", getOrdersRequest.StartTime, getOrdersRequest.EndTime)
if err != nil {
return nil, err
}
} else {
for x := range getOrdersRequest.Pairs {
newOrders, err = ku.ListOrders(ctx, "", getOrdersRequest.Pairs[x].String(), sideString, getOrdersRequest.Type.Lower(), "", getOrdersRequest.StartTime, getOrdersRequest.EndTime)
if err != nil {
return nil, fmt.Errorf("%w while fetching for symbol %s", err, getOrdersRequest.Pairs[x].String())
}
if responseOrders == nil {
responseOrders = newOrders
} else {
responseOrders.Items = append(responseOrders.Items, newOrders.Items...)
}
}
}
orders = make([]order.Detail, len(responseOrders.Items))
for i := range orders {
orderSide, err = order.StringToOrderSide(responseOrders.Items[i].Side)
if err != nil {
return nil, err
}
var orderStatus order.Status
pair, err = currency.NewPairFromString(responseOrders.Items[i].Symbol)
if err != nil {
return nil, err
}
var oType order.Type
oType, err = order.StringToOrderType(responseOrders.Items[i].Type)
if err != nil {
log.Errorf(log.ExchangeSys, "%s %v", ku.Name, err)
}
orders[i] = order.Detail{
Price: responseOrders.Items[i].Price,
Amount: responseOrders.Items[i].Size,
ExecutedAmount: responseOrders.Items[i].DealSize,
RemainingAmount: responseOrders.Items[i].Size - responseOrders.Items[i].DealSize,
Date: responseOrders.Items[i].CreatedAt.Time(),
Exchange: ku.Name,
OrderID: responseOrders.Items[i].ID,
Side: orderSide,
Status: orderStatus,
Type: oType,
Pair: pair,
}
orders[i].InferCostsAndTimes()
}
}
return getOrdersRequest.Filter(ku.Name, orders), nil
}
// GetFeeByType returns an estimate of fee based on the type of transaction
func (ku *Kucoin) GetFeeByType(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) {
if feeBuilder == nil {
return 0, fmt.Errorf("%T %w", feeBuilder, common.ErrNilPointer)
}
if !ku.AreCredentialsValid(ctx) &&
feeBuilder.FeeType == exchange.CryptocurrencyTradeFee {
feeBuilder.FeeType = exchange.OfflineTradeFee
}
if feeBuilder.Pair.IsEmpty() {
return 0, currency.ErrCurrencyPairEmpty
}
switch feeBuilder.FeeType {
case exchange.CryptocurrencyWithdrawalFee,
exchange.CryptocurrencyTradeFee:
fee, err := ku.GetTradingFee(ctx, currency.Pairs{feeBuilder.Pair})
if err != nil {
return 0, err
}
if feeBuilder.IsMaker {
return feeBuilder.Amount * fee[0].MakerFeeRate, nil
}
return feeBuilder.Amount * fee[0].TakerFeeRate, nil
case exchange.OfflineTradeFee:
return feeBuilder.Amount * 0.001, nil
case exchange.CryptocurrencyDepositFee:
return 0, nil
default:
if !feeBuilder.FiatCurrency.IsEmpty() {
fee, err := ku.GetBasicFee(ctx, "1")
if err != nil {
return 0, err
}
if feeBuilder.IsMaker {
return feeBuilder.Amount * fee.MakerFeeRate, nil
}
return feeBuilder.Amount * fee.TakerFeeRate, nil
}
return 0, errors.New("can't construct fee")
}
}
// ValidateCredentials validates current credentials used for wrapper
func (ku *Kucoin) ValidateCredentials(ctx context.Context, assetType asset.Item) error {
err := ku.CurrencyPairs.IsAssetEnabled(assetType)
if err != nil {
return err
}
_, err = ku.UpdateAccountInfo(ctx, assetType)
return ku.CheckTransientError(err)
}
// GetHistoricCandles returns candles between a time period for a set time interval
func (ku *Kucoin) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) {
req, err := ku.GetKlineRequest(pair, a, interval, start, end, false)
if err != nil {
return nil, err
}
var timeseries []kline.Candle
switch a {
case asset.Futures:
var candles []FuturesKline
candles, err := ku.GetFuturesKline(ctx, int64(interval.Duration().Minutes()), req.RequestFormatted.String(), req.Start, req.End)
if err != nil {
return nil, err
}
for x := range candles {
timeseries = append(
timeseries, kline.Candle{
Time: candles[x].StartTime,
Open: candles[x].Open,
High: candles[x].High,
Low: candles[x].Low,
Close: candles[x].Close,
Volume: candles[x].Volume,
})
}
case asset.Spot, asset.Margin:
intervalString, err := intervalToString(interval)
if err != nil {
return nil, err
}
var candles []Kline
candles, err = ku.GetKlines(ctx, req.RequestFormatted.String(), intervalString, req.Start, req.End)
if err != nil {
return nil, err
}
for x := range candles {
timeseries = append(
timeseries, kline.Candle{
Time: candles[x].StartTime,
Open: candles[x].Open,
High: candles[x].High,
Low: candles[x].Low,
Close: candles[x].Close,
Volume: candles[x].Volume,
})
}
}
return req.ProcessResponse(timeseries)
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (ku *Kucoin) GetHistoricCandlesExtended(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) {
req, err := ku.GetKlineExtendedRequest(pair, a, interval, start, end)
if err != nil {
return nil, err
}
var timeSeries []kline.Candle
for x := range req.RangeHolder.Ranges {
switch a {
case asset.Futures:
var candles []FuturesKline
candles, err = ku.GetFuturesKline(ctx, int64(interval.Duration().Minutes()), req.RequestFormatted.String(), req.RangeHolder.Ranges[x].Start.Time, req.RangeHolder.Ranges[x].End.Time)
if err != nil {
return nil, err
}
for x := range candles {
timeSeries = append(
timeSeries, kline.Candle{
Time: candles[x].StartTime,
Open: candles[x].Open,
High: candles[x].High,
Low: candles[x].Low,
Close: candles[x].Close,
Volume: candles[x].Volume,
})
}
case asset.Spot, asset.Margin:
var intervalString string
intervalString, err = intervalToString(interval)
if err != nil {
return nil, err
}
var candles []Kline
candles, err = ku.GetKlines(ctx, req.RequestFormatted.String(), intervalString, req.RangeHolder.Ranges[x].Start.Time, req.RangeHolder.Ranges[x].End.Time)
if err != nil {
return nil, err
}
for x := range candles {
timeSeries = append(
timeSeries, kline.Candle{
Time: candles[x].StartTime,
Open: candles[x].Open,
High: candles[x].High,
Low: candles[x].Low,
Close: candles[x].Close,
Volume: candles[x].Volume,
})
}
}
}
return req.ProcessResponse(timeSeries)
}
// GetServerTime returns the current exchange server time.
func (ku *Kucoin) GetServerTime(ctx context.Context, a asset.Item) (time.Time, error) {
switch a {
case asset.Spot, asset.Margin:
return ku.GetCurrentServerTime(ctx)
case asset.Futures:
return ku.GetFuturesServerTime(ctx)
default:
return time.Time{}, fmt.Errorf("%w %v", asset.ErrNotSupported, a)
}
}
// GetAvailableTransferChains returns the available transfer blockchains for the specific
// cryptocurrency
func (ku *Kucoin) GetAvailableTransferChains(ctx context.Context, cryptocurrency currency.Code) ([]string, error) {
if cryptocurrency.IsEmpty() {
return nil, currency.ErrCurrencyCodeEmpty
}
currencyDetail, err := ku.GetCurrencyDetail(ctx, cryptocurrency.String(), "")
if err != nil {
return nil, err
}
chains := make([]string, 0, len(currencyDetail.Chains))
for x := range currencyDetail.Chains {
chains = append(chains, currencyDetail.Chains[x].Name)
}
return chains, nil
}
// ValidateAPICredentials validates current credentials used for wrapper
// functionality
func (ku *Kucoin) ValidateAPICredentials(ctx context.Context, assetType asset.Item) error {
_, err := ku.UpdateAccountInfo(ctx, assetType)
return ku.CheckTransientError(err)
}
// GetFuturesContractDetails returns details about futures contracts
func (ku *Kucoin) GetFuturesContractDetails(ctx context.Context, item asset.Item) ([]futures.Contract, error) {
if !item.IsFutures() {
return nil, futures.ErrNotFuturesAsset
}
if !ku.SupportsAsset(item) || item != asset.Futures {
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, item)
}
contracts, err := ku.GetFuturesOpenContracts(ctx)
if err != nil {
return nil, err
}
resp := make([]futures.Contract, len(contracts))
for i := range contracts {
var cp, underlying currency.Pair
underlying, err = currency.NewPairFromStrings(contracts[i].BaseCurrency, contracts[i].QuoteCurrency)
if err != nil {
return nil, err
}
cp, err = currency.NewPairFromStrings(contracts[i].BaseCurrency, contracts[i].Symbol[len(contracts[i].BaseCurrency):])
if err != nil {
return nil, err
}
settleCurr := currency.NewCode(contracts[i].SettleCurrency)
var ct futures.ContractType
if contracts[i].ContractType == "FFWCSX" {
ct = futures.Perpetual
} else {
ct = futures.Quarterly
}
contractSettlementType := futures.Linear
if contracts[i].IsInverse {
contractSettlementType = futures.Inverse
}
var fri time.Duration
if len(ku.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies) == 1 {
// can infer funding rate interval from the only funding rate frequency defined
for k := range ku.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies {
fri = k.Duration()
}
}
timeOfCurrentFundingRate := time.Now().Add((time.Duration(contracts[i].NextFundingRateTime) * time.Millisecond) - fri).Truncate(time.Hour).UTC()
resp[i] = futures.Contract{
Exchange: ku.Name,
Name: cp,
Underlying: underlying,
SettlementCurrencies: currency.Currencies{settleCurr},
MarginCurrency: settleCurr,
Asset: item,
StartDate: contracts[i].FirstOpenDate.Time(),
EndDate: contracts[i].ExpireDate.Time(),
IsActive: !strings.EqualFold(contracts[i].Status, "closed"),
Status: contracts[i].Status,
Multiplier: contracts[i].Multiplier,
MaxLeverage: contracts[i].MaxLeverage,
SettlementType: contractSettlementType,
LatestRate: fundingrate.Rate{
Rate: decimal.NewFromFloat(contracts[i].FundingFeeRate),
Time: timeOfCurrentFundingRate, // kucoin pays every 8 hours
},
Type: ct,
}
}
return resp, nil
}
// GetLatestFundingRates returns the latest funding rates data
func (ku *Kucoin) GetLatestFundingRates(ctx context.Context, r *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) {
if r == nil {
return nil, fmt.Errorf("%w LatestRateRequest", common.ErrNilPointer)
}
var fri time.Duration
if len(ku.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies) == 1 {
// can infer funding rate interval from the only funding rate frequency defined
for k := range ku.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies {
fri = k.Duration()
}
}
if r.Pair.IsEmpty() {
contracts, err := ku.GetFuturesOpenContracts(ctx)
if err != nil {
return nil, err
}
if r.IncludePredictedRate {
log.Warnf(log.ExchangeSys, "%s predicted rate for all currencies requires an additional %v requests", ku.Name, len(contracts))
}
timeChecked := time.Now()
resp := make([]fundingrate.LatestRateResponse, 0, len(contracts))
for i := range contracts {
timeOfNextFundingRate := time.Now().Add(time.Duration(contracts[i].NextFundingRateTime) * time.Millisecond).Truncate(time.Hour).UTC()
var cp currency.Pair
cp, err = currency.NewPairFromStrings(contracts[i].BaseCurrency, contracts[i].Symbol[len(contracts[i].BaseCurrency):])
if err != nil {
return nil, err
}
var isPerp bool
isPerp, err = ku.IsPerpetualFutureCurrency(r.Asset, cp)
if err != nil {
return nil, err
}
if !isPerp {
continue
}
rate := fundingrate.LatestRateResponse{
Exchange: ku.Name,
Asset: r.Asset,
Pair: cp,
LatestRate: fundingrate.Rate{
Time: timeOfNextFundingRate.Add(-fri),
Rate: decimal.NewFromFloat(contracts[i].FundingFeeRate),
},
TimeOfNextRate: timeOfNextFundingRate,
TimeChecked: timeChecked,
}
if r.IncludePredictedRate {
var fr *FuturesFundingRate
fr, err = ku.GetFuturesCurrentFundingRate(ctx, contracts[i].Symbol)
if err != nil {
return nil, err
}
rate.PredictedUpcomingRate = fundingrate.Rate{
Time: timeOfNextFundingRate,
Rate: decimal.NewFromFloat(fr.PredictedValue),
}
}
resp = append(resp, rate)
}
return resp, nil
}
resp := make([]fundingrate.LatestRateResponse, 1)
is, err := ku.IsPerpetualFutureCurrency(r.Asset, r.Pair)
if err != nil {
return nil, err
}
if !is {
return nil, fmt.Errorf("%w %s %v", futures.ErrNotPerpetualFuture, r.Asset, r.Pair)
}
fPair, err := ku.FormatExchangeCurrency(r.Pair, r.Asset)
if err != nil {
return nil, err
}
var fr *FuturesFundingRate
fr, err = ku.GetFuturesCurrentFundingRate(ctx, fPair.String())
if err != nil {
return nil, err
}
rate := fundingrate.LatestRateResponse{
Exchange: ku.Name,
Asset: r.Asset,
Pair: r.Pair,
LatestRate: fundingrate.Rate{
Time: fr.TimePoint.Time(),
Rate: decimal.NewFromFloat(fr.Value),
},
TimeOfNextRate: fr.TimePoint.Time().Add(fri).Truncate(time.Hour).UTC(),
TimeChecked: time.Now(),
}
if r.IncludePredictedRate {
rate.PredictedUpcomingRate = fundingrate.Rate{
Time: rate.TimeOfNextRate,
Rate: decimal.NewFromFloat(fr.PredictedValue),
}
}
resp[0] = rate
return resp, nil
}
// IsPerpetualFutureCurrency ensures a given asset and currency is a perpetual future
func (ku *Kucoin) IsPerpetualFutureCurrency(a asset.Item, cp currency.Pair) (bool, error) {
return a == asset.Futures && (cp.Quote.Equal(currency.USDTM) || cp.Quote.Equal(currency.USDM)), nil
}
// GetHistoricalFundingRates returns funding rates for a given asset and currency for a time period
func (ku *Kucoin) GetHistoricalFundingRates(_ context.Context, _ *fundingrate.HistoricalRatesRequest) (*fundingrate.HistoricalRates, error) {
return nil, common.ErrFunctionNotSupported
}
// GetLeverage gets the account's initial leverage for the asset type and pair
func (ku *Kucoin) GetLeverage(_ context.Context, _ asset.Item, _ currency.Pair, _ margin.Type, _ order.Side) (float64, error) {
return -1, fmt.Errorf("%w leverage is set during order placement, view orders to view leverage", common.ErrFunctionNotSupported)
}
// SetLeverage sets the account's initial leverage for the asset type and pair
func (ku *Kucoin) SetLeverage(_ context.Context, _ asset.Item, _ currency.Pair, _ margin.Type, _ float64, _ order.Side) error {
return fmt.Errorf("%w leverage is set during order placement", common.ErrFunctionNotSupported)
}
// SetMarginType sets the default margin type for when opening a new position
func (ku *Kucoin) SetMarginType(_ context.Context, _ asset.Item, _ currency.Pair, _ margin.Type) error {
return fmt.Errorf("%w must be set via website", common.ErrFunctionNotSupported)
}
// SetCollateralMode sets the collateral type for your account
func (ku *Kucoin) SetCollateralMode(_ context.Context, _ asset.Item, _ collateral.Mode) error {
return fmt.Errorf("%w must be set via website", common.ErrFunctionNotSupported)
}
// GetCollateralMode returns the collateral type for your account
func (ku *Kucoin) GetCollateralMode(_ context.Context, _ asset.Item) (collateral.Mode, error) {
return collateral.UnknownMode, fmt.Errorf("%w only via website", common.ErrFunctionNotSupported)
}
// ChangePositionMargin will modify a position/currencies margin parameters
func (ku *Kucoin) ChangePositionMargin(ctx context.Context, r *margin.PositionChangeRequest) (*margin.PositionChangeResponse, error) {
if r == nil {
return nil, fmt.Errorf("%w HistoricalRatesRequest", common.ErrNilPointer)
}
if r.Asset != asset.Futures {
return nil, fmt.Errorf("%w %v", futures.ErrNotFuturesAsset, r.Asset)
}
if r.Pair.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
if r.MarginType != margin.Isolated {
return nil, fmt.Errorf("%w %v", margin.ErrMarginTypeUnsupported, r.MarginType)
}
fPair, err := ku.FormatExchangeCurrency(r.Pair, r.Asset)
if err != nil {
return nil, err
}
resp, err := ku.AddMargin(ctx, fPair.String(), fmt.Sprintf("%s%v%v", r.Pair, r.NewAllocatedMargin, time.Now().Unix()), r.NewAllocatedMargin)
if err != nil {
return nil, err
}
if resp == nil {
return nil, fmt.Errorf("%s - %s", ku.Name, "no response received")
}
return &margin.PositionChangeResponse{
Exchange: ku.Name,
Pair: r.Pair,
Asset: r.Asset,
AllocatedMargin: resp.PosMargin,
MarginType: r.MarginType,
}, nil
}
// GetFuturesPositionSummary returns position summary details for an active position
func (ku *Kucoin) GetFuturesPositionSummary(ctx context.Context, r *futures.PositionSummaryRequest) (*futures.PositionSummary, error) {
if r == nil {
return nil, fmt.Errorf("%w HistoricalRatesRequest", common.ErrNilPointer)
}
if r.Asset != asset.Futures {
return nil, fmt.Errorf("%w %v", futures.ErrNotPerpetualFuture, r.Asset)
}
if r.Pair.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
fPair, err := ku.FormatExchangeCurrency(r.Pair, r.Asset)
if err != nil {
return nil, err
}
pos, err := ku.GetFuturesPosition(ctx, fPair.String())
if err != nil {
return nil, err
}
marginType := margin.Isolated
if pos.CrossMode {
marginType = margin.Multi
}
contracts, err := ku.GetFuturesContractDetails(ctx, r.Asset)
if err != nil {
return nil, err
}
var multiplier, contractSize float64
var settlementType futures.ContractSettlementType
for i := range contracts {
if !contracts[i].Name.Equal(fPair) {
continue
}
multiplier = contracts[i].Multiplier
contractSize = multiplier * pos.CurrentQty
settlementType = contracts[i].SettlementType
}
ao, err := ku.GetFuturesAccountOverview(ctx, fPair.String())
if err != nil {
return nil, err
}
return &futures.PositionSummary{
Pair: r.Pair,
Asset: r.Asset,
MarginType: marginType,
CollateralMode: collateral.MultiMode,
Currency: currency.NewCode(pos.SettleCurrency),
StartDate: pos.OpeningTimestamp.Time(),
AvailableEquity: decimal.NewFromFloat(ao.AccountEquity),
MarginBalance: decimal.NewFromFloat(ao.MarginBalance),
NotionalSize: decimal.NewFromFloat(pos.MarkValue),
Leverage: decimal.NewFromFloat(pos.RealLeverage),
MaintenanceMarginRequirement: decimal.NewFromFloat(pos.MaintMarginReq),
InitialMarginRequirement: decimal.NewFromFloat(pos.PosInit),
EstimatedLiquidationPrice: decimal.NewFromFloat(pos.LiquidationPrice),
CollateralUsed: decimal.NewFromFloat(pos.PosCost),
MarkPrice: decimal.NewFromFloat(pos.MarkPrice),
CurrentSize: decimal.NewFromFloat(pos.CurrentQty),
ContractSize: decimal.NewFromFloat(contractSize),
ContractMultiplier: decimal.NewFromFloat(multiplier),
ContractSettlementType: settlementType,
AverageOpenPrice: decimal.NewFromFloat(pos.AvgEntryPrice),
UnrealisedPNL: decimal.NewFromFloat(pos.UnrealisedPnl),
RealisedPNL: decimal.NewFromFloat(pos.RealisedPnl),
MaintenanceMarginFraction: decimal.NewFromFloat(pos.MaintMarginReq),
FreeCollateral: decimal.NewFromFloat(ao.AvailableBalance),
TotalCollateral: decimal.NewFromFloat(ao.AccountEquity),
FrozenBalance: decimal.NewFromFloat(ao.FrozenFunds),
}, nil
}
// GetFuturesPositionOrders returns the orders for futures positions
func (ku *Kucoin) GetFuturesPositionOrders(ctx context.Context, r *futures.PositionsRequest) ([]futures.PositionResponse, error) {
if r == nil {
return nil, fmt.Errorf("%w HistoricalRatesRequest", common.ErrNilPointer)
}
if r.Asset != asset.Futures {
return nil, fmt.Errorf("%w %v", futures.ErrNotPerpetualFuture, r.Asset)
}
if len(r.Pairs) == 0 {
return nil, currency.ErrCurrencyPairEmpty
}
err := common.StartEndTimeCheck(r.StartDate, r.EndDate)
if err != nil {
return nil, err
}
if !r.EndDate.IsZero() && r.EndDate.Sub(r.StartDate) > ku.Features.Supports.MaximumOrderHistory {
if r.RespectOrderHistoryLimits {
r.StartDate = time.Now().Add(-ku.Features.Supports.MaximumOrderHistory)
} else {
return nil, fmt.Errorf("%w max lookup %v", futures.ErrOrderHistoryTooLarge, time.Now().Add(-ku.Features.Supports.MaximumOrderHistory))
}
}
contracts, err := ku.GetFuturesContractDetails(ctx, r.Asset)
if err != nil {
return nil, err
}
resp := make([]futures.PositionResponse, len(r.Pairs))
for x := range r.Pairs {
var multiplier float64
fPair, err := ku.FormatExchangeCurrency(r.Pairs[x], r.Asset)
if err != nil {
return nil, err
}
for i := range contracts {
if !contracts[i].Name.Equal(fPair) {
continue
}
multiplier = contracts[i].Multiplier
}
positionOrders, err := ku.GetFuturesOrders(ctx, "", fPair.String(), "", "", r.StartDate, r.EndDate)
if err != nil {
return nil, err
}
resp[x].Orders = make([]order.Detail, len(positionOrders.Items))
for y := range positionOrders.Items {
side, err := order.StringToOrderSide(positionOrders.Items[y].Side)
if err != nil {
return nil, err
}
oType, err := order.StringToOrderType(positionOrders.Items[y].OrderType)
if err != nil {
return nil, fmt.Errorf("asset type: %v err: %w", r.Asset, err)
}
oStatus, err := order.StringToOrderStatus(positionOrders.Items[y].Status)
if err != nil {
return nil, fmt.Errorf("asset type: %v err: %w", r.Asset, err)
}
resp[x].Orders[y] = order.Detail{
Leverage: positionOrders.Items[y].Leverage,
Price: positionOrders.Items[y].Price,
Amount: positionOrders.Items[y].Size * multiplier,
ContractAmount: positionOrders.Items[y].Size,
ExecutedAmount: positionOrders.Items[y].FilledSize,
RemainingAmount: positionOrders.Items[y].Size - positionOrders.Items[y].FilledSize,
CostAsset: currency.NewCode(positionOrders.Items[y].SettleCurrency),
Exchange: ku.Name,
OrderID: positionOrders.Items[y].ID,
ClientOrderID: positionOrders.Items[y].ClientOid,
Type: oType,
Side: side,
Status: oStatus,
AssetType: asset.Futures,
Date: positionOrders.Items[y].CreatedAt.Time(),
CloseTime: positionOrders.Items[y].EndAt.Time(),
LastUpdated: positionOrders.Items[y].UpdatedAt.Time(),
Pair: fPair,
}
}
}
return resp, nil
}
// UpdateOrderExecutionLimits updates order execution limits
func (ku *Kucoin) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error {
if !ku.SupportsAsset(a) {
return fmt.Errorf("%w %v", asset.ErrNotSupported, a)
}
var limits []order.MinMaxLevel
switch a {
case asset.Spot, asset.Margin:
symbols, err := ku.GetSymbols(ctx, "")
if err != nil {
return err
}
limits = make([]order.MinMaxLevel, 0, len(symbols))
for x := range symbols {
if a == asset.Margin && !symbols[x].IsMarginEnabled {
continue
}
pair, enabled, err := ku.MatchSymbolCheckEnabled(symbols[x].Symbol, a, true)
if err != nil && !errors.Is(err, currency.ErrPairNotFound) {
return err
}
if !enabled {
continue
}
limits = append(limits, order.MinMaxLevel{
Pair: pair,
Asset: a,
AmountStepIncrementSize: symbols[x].BaseIncrement,
QuoteStepIncrementSize: symbols[x].QuoteIncrement,
PriceStepIncrementSize: symbols[x].PriceIncrement,
MinimumBaseAmount: symbols[x].BaseMinSize,
MaximumBaseAmount: symbols[x].BaseMaxSize,
MinimumQuoteAmount: symbols[x].QuoteMinSize,
MaximumQuoteAmount: symbols[x].QuoteMaxSize,
})
}
case asset.Futures:
contract, err := ku.GetFuturesOpenContracts(ctx)
if err != nil {
return err
}
limits = make([]order.MinMaxLevel, 0, len(contract))
for x := range contract {
pair, enabled, err := ku.MatchSymbolCheckEnabled(contract[x].Symbol, a, false)
if err != nil && !errors.Is(err, currency.ErrPairNotFound) {
return err
}
if !enabled {
continue
}
limits = append(limits, order.MinMaxLevel{
Pair: pair,
Asset: a,
AmountStepIncrementSize: contract[x].LotSize,
QuoteStepIncrementSize: contract[x].TickSize,
MaximumBaseAmount: contract[x].MaxOrderQty,
MaximumQuoteAmount: contract[x].MaxPrice,
})
}
}
return ku.LoadLimits(limits)
}
// GetOpenInterest returns the open interest rate for a given asset pair
func (ku *Kucoin) GetOpenInterest(ctx context.Context, k ...key.PairAsset) ([]futures.OpenInterest, error) {
for i := range k {
if k[i].Asset != asset.Futures {
// avoid API calls or returning errors after a successful retrieval
return nil, fmt.Errorf("%w %v %v", asset.ErrNotSupported, k[i].Asset, k[i].Pair())
}
}
contracts, err := ku.GetFuturesOpenContracts(ctx)
if err != nil {
return nil, err
}
resp := make([]futures.OpenInterest, 0, len(contracts))
for i := range contracts {
var symbol currency.Pair
var enabled bool
symbol, enabled, err = ku.MatchSymbolCheckEnabled(contracts[i].Symbol, asset.Futures, true)
if err != nil && !errors.Is(err, currency.ErrPairNotFound) {
return nil, err
}
if !enabled {
continue
}
var appendData bool
for j := range k {
if k[j].Pair().Equal(symbol) {
appendData = true
break
}
}
if len(k) > 0 && !appendData {
continue
}
resp = append(resp, futures.OpenInterest{
Key: key.ExchangePairAsset{
Exchange: ku.Name,
Base: symbol.Base.Item,
Quote: symbol.Quote.Item,
Asset: asset.Futures,
},
OpenInterest: contracts[i].OpenInterest.Float64(),
})
}
return resp, nil
}
// GetCurrencyTradeURL returns the URL to the exchange's trade page for the given asset and currency pair
func (ku *Kucoin) GetCurrencyTradeURL(_ context.Context, a asset.Item, cp currency.Pair) (string, error) {
_, err := ku.CurrencyPairs.IsPairEnabled(cp, a)
if err != nil {
return "", err
}
cp.Delimiter = currency.DashDelimiter
switch a {
case asset.Spot:
return tradeBaseURL + tradeSpot + cp.Upper().String(), nil
case asset.Margin:
return tradeBaseURL + tradeSpot + tradeMargin + cp.Upper().String(), nil
case asset.Futures, asset.CoinMarginedFutures:
cp.Delimiter = ""
return tradeBaseURL + tradeFutures + tradeSpot + cp.Upper().String(), nil
default:
return "", fmt.Errorf("%w %v", asset.ErrNotSupported, a)
}
}