bbgo_origin/pkg/exchange/kucoin/exchange.go
2022-01-01 01:28:29 +08:00

399 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package kucoin
import (
"context"
"fmt"
"strconv"
"time"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"go.uber.org/multierr"
"golang.org/x/time/rate"
"github.com/c9s/bbgo/pkg/exchange/kucoin/kucoinapi"
"github.com/c9s/bbgo/pkg/types"
)
var marketDataLimiter = rate.NewLimiter(rate.Every(500*time.Millisecond), 1)
var ErrMissingSequence = errors.New("sequence is missing")
// OKB is the platform currency of OKEx, pre-allocate static string here
const KCS = "KCS"
var log = logrus.WithFields(logrus.Fields{
"exchange": "kucoin",
})
type Exchange struct {
key, secret, passphrase string
client *kucoinapi.RestClient
}
func New(key, secret, passphrase string) *Exchange {
client := kucoinapi.NewClient()
// for public access mode
if len(key) > 0 && len(secret) > 0 && len(passphrase) > 0 {
client.Auth(key, secret, passphrase)
}
return &Exchange{
key: key,
secret: secret,
passphrase: passphrase,
client: client,
}
}
func (e *Exchange) Name() types.ExchangeName {
return types.ExchangeKucoin
}
func (e *Exchange) PlatformFeeCurrency() string {
return KCS
}
func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) {
accounts, err := e.client.AccountService.ListAccounts(ctx)
if err != nil {
return nil, err
}
// for now, we only return the trading account
a := types.NewAccount()
balances := toGlobalBalanceMap(accounts)
a.UpdateBalances(balances)
return a, nil
}
func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) {
accounts, err := e.client.AccountService.ListAccounts(ctx)
if err != nil {
return nil, err
}
return toGlobalBalanceMap(accounts), nil
}
func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) {
markets, err := e.client.MarketDataService.ListSymbols()
if err != nil {
return nil, err
}
marketMap := types.MarketMap{}
for _, s := range markets {
market := toGlobalMarket(s)
marketMap.Add(market)
}
return marketMap, nil
}
func (e *Exchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticker, error) {
s, err := e.client.MarketDataService.GetTicker24HStat(symbol)
if err != nil {
return nil, err
}
ticker := toGlobalTicker(*s)
return &ticker, nil
}
func (e *Exchange) QueryTickers(ctx context.Context, symbols ...string) (map[string]types.Ticker, error) {
tickers := map[string]types.Ticker{}
if len(symbols) > 0 {
for _, s := range symbols {
t, err := e.QueryTicker(ctx, s)
if err != nil {
return nil, err
}
tickers[s] = *t
}
return tickers, nil
}
allTickers, err := e.client.MarketDataService.ListTickers()
if err != nil {
return nil, err
}
for _, s := range allTickers.Ticker {
tickers[s.Symbol] = toGlobalTicker(s)
}
return tickers, nil
}
// From the doc
// Type of candlestick patterns: 1min, 3min, 5min, 15min, 30min, 1hour, 2hour, 4hour, 6hour, 8hour, 12hour, 1day, 1week
var supportedIntervals = map[types.Interval]int{
types.Interval1m: 60,
types.Interval5m: 60 * 5,
types.Interval15m: 60 * 15,
types.Interval30m: 60 * 30,
types.Interval1h: 60 * 60,
types.Interval2h: 60 * 60 * 2,
types.Interval4h: 60 * 60 * 4,
types.Interval6h: 60 * 60 * 6,
// types.Interval8h: 60 * 60 * 8,
types.Interval12h: 60 * 60 * 12,
}
func (e *Exchange) SupportedInterval() map[types.Interval]int {
return supportedIntervals
}
func (e *Exchange) IsSupportedInterval(interval types.Interval) bool {
_, ok := supportedIntervals[interval]
return ok
}
func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions) ([]types.KLine, error) {
_ = marketDataLimiter.Wait(ctx)
req := e.client.MarketDataService.NewGetKLinesRequest()
req.Symbol(toLocalSymbol(symbol))
req.Interval(toLocalInterval(interval))
if options.StartTime != nil {
req.StartAt(*options.StartTime)
// For each query, the system would return at most **1500** pieces of data. To obtain more data, please page the data by time.
req.EndAt(options.StartTime.Add(1500 * interval.Duration()))
} else if options.EndTime != nil {
req.EndAt(*options.EndTime)
}
ks, err := req.Do(ctx)
if err != nil {
return nil, err
}
var klines []types.KLine
for _, k := range ks {
gi := toGlobalInterval(k.Interval)
klines = append(klines, types.KLine{
Exchange: types.ExchangeKucoin,
Symbol: toGlobalSymbol(k.Symbol),
StartTime: types.Time(k.StartTime),
EndTime: types.Time(k.StartTime.Add(gi.Duration() - time.Millisecond)),
Interval: gi,
Open: k.Open.Float64(),
Close: k.Close.Float64(),
High: k.High.Float64(),
Low: k.Low.Float64(),
Volume: k.Volume.Float64(),
QuoteVolume: k.QuoteVolume.Float64(),
Closed: true,
})
}
return klines, nil
}
func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (createdOrders types.OrderSlice, err error) {
for _, order := range orders {
req := e.client.TradeService.NewPlaceOrderRequest()
req.Symbol(toLocalSymbol(order.Symbol))
req.Side(toLocalSide(order.Side))
if order.ClientOrderID != "" {
req.ClientOrderID(order.ClientOrderID)
}
if len(order.QuantityString) > 0 {
req.Size(order.QuantityString)
} else if order.Market.Symbol != "" {
req.Size(order.Market.FormatQuantity(order.Quantity))
} else {
req.Size(strconv.FormatFloat(order.Quantity, 'f', 8, 64))
}
// set price field for limit orders
switch order.Type {
case types.OrderTypeStopLimit, types.OrderTypeLimit:
if len(order.PriceString) > 0 {
req.Price(order.PriceString)
} else if order.Market.Symbol != "" {
req.Price(order.Market.FormatPrice(order.Price))
}
}
switch order.TimeInForce {
case "FOK":
req.TimeInForce(kucoinapi.TimeInForceFOK)
case "IOC":
req.TimeInForce(kucoinapi.TimeInForceIOC)
default:
// default to GTC
req.TimeInForce(kucoinapi.TimeInForceGTC)
}
orderResponse, err := req.Do(ctx)
if err != nil {
return createdOrders, err
}
createdOrders = append(createdOrders, types.Order{
SubmitOrder: order,
Exchange: types.ExchangeKucoin,
OrderID: hashStringID(orderResponse.OrderID),
UUID: orderResponse.OrderID,
Status: types.OrderStatusNew,
ExecutedQuantity: 0,
IsWorking: true,
CreationTime: types.Time(time.Now()),
UpdateTime: types.Time(time.Now()),
})
}
return createdOrders, err
}
// QueryOpenOrders
/*
Documentation from the Kucoin API page
Any order on the exchange order book is in active status.
Orders removed from the order book will be marked with done status.
After an order becomes done, there may be a few milliseconds latency before its fully settled.
You can check the orders in any status.
If the status parameter is not specified, orders of done status will be returned by default.
When you query orders in active status, there is no time limit.
However, when you query orders in done status, the start and end time range cannot exceed 7* 24 hours.
An error will occur if the specified time window exceeds the range.
If you specify the end time only, the system will automatically calculate the start time as end time minus 7*24 hours, and vice versa.
The history for cancelled orders is only kept for one month.
You will not be able to query for cancelled orders that have happened more than a month ago.
*/
func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) {
req := e.client.TradeService.NewListOrdersRequest()
req.Symbol(toLocalSymbol(symbol))
req.Status("active")
orderList, err := req.Do(ctx)
if err != nil {
return nil, err
}
// TODO: support pagination (right now we can only get 50 items from the first page)
for _, o := range orderList.Items {
order := toGlobalOrder(o)
orders = append(orders, order)
}
return orders, err
}
func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since, until time.Time, lastOrderID uint64) (orders []types.Order, err error) {
req := e.client.TradeService.NewListOrdersRequest()
req.Symbol(toLocalSymbol(symbol))
req.Status("done")
req.StartAt(since)
// kucoin:
// When you query orders in active status, there is no time limit.
// However, when you query orders in done status, the start and end time range cannot exceed 7* 24 hours.
// An error will occur if the specified time window exceeds the range.
// If you specify the end time only, the system will automatically calculate the start time as end time minus 7*24 hours, and vice versa.
if until.Sub(since) < 7 * 24 * time.Hour {
req.EndAt(until)
} else {
req.EndAt(since.Add(7 * 24 * time.Hour - time.Minute))
}
orderList, err := req.Do(ctx)
if err != nil {
return orders, err
}
for _, o := range orderList.Items {
order := toGlobalOrder(o)
orders = append(orders, order)
}
return orders, err
}
func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) (trades []types.Trade, err error) {
req := e.client.TradeService.NewGetFillsRequest()
req.Symbol(toLocalSymbol(symbol))
if options.StartTime != nil {
req.StartAt(*options.StartTime)
} else if options.EndTime != nil {
req.EndAt(*options.EndTime)
}
response, err := req.Do(ctx)
if err != nil {
return trades, err
}
for _, fill := range response.Items {
trade := toGlobalTrade(fill)
trades = append(trades, trade)
}
return trades, nil
}
func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) (errs error) {
for _, o := range orders {
req := e.client.TradeService.NewCancelOrderRequest()
if o.UUID != "" {
req.OrderID(o.UUID)
} else if o.ClientOrderID != "" {
req.ClientOrderID(o.ClientOrderID)
} else {
errs = multierr.Append(
errs,
fmt.Errorf("the order uuid or client order id is empty, order: %#v", o),
)
continue
}
response, err := req.Do(ctx)
if err != nil {
errs = multierr.Append(errs, err)
continue
}
log.Infof("cancelled orders: %v", response.CancelledOrderIDs)
}
return errors.Wrap(errs, "order cancel error")
}
func (e *Exchange) NewStream() types.Stream {
return NewStream(e.client, e)
}
func (e *Exchange) QueryDepth(ctx context.Context, symbol string) (types.SliceOrderBook, int64, error) {
orderBook, err := e.client.MarketDataService.GetOrderBook(toLocalSymbol(symbol), 100)
if err != nil {
return types.SliceOrderBook{}, 0, err
}
if len(orderBook.Sequence) == 0 {
return types.SliceOrderBook{}, 0, ErrMissingSequence
}
sequence, err := strconv.ParseInt(orderBook.Sequence, 10, 64)
if err != nil {
return types.SliceOrderBook{}, 0, err
}
return types.SliceOrderBook{
Symbol: toGlobalSymbol(symbol),
Bids: orderBook.Bids,
Asks: orderBook.Asks,
}, sequence, nil
}