mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 15:09:42 +00:00
* 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>
2025 lines
66 KiB
Go
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: ¤cy.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter},
|
|
ConfigFormat: ¤cy.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter},
|
|
}
|
|
futures := currency.PairStore{
|
|
RequestFormat: ¤cy.PairFormat{Uppercase: true},
|
|
ConfigFormat: ¤cy.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)
|
|
}
|
|
}
|