Merge pull request #349 from c9s/feature/kucoin

feature: integrate kucoin api
This commit is contained in:
Yo-An Lin 2021-12-11 23:17:07 +08:00 committed by GitHub
commit 2e7b69320b
15 changed files with 1825 additions and 0 deletions

View File

@ -0,0 +1,16 @@
# Kucoin command-line tool
```shell
go run ./examples/kucoin accounts
go run ./examples/kucoin subaccounts
go run ./examples/kucoin symbols
go run ./examples/kucoin tickers
go run ./examples/kucoin tickers BTC-USDT
go run ./examples/kucoin orderbook BTC-USDT 20
go run ./examples/kucoin orderbook BTC-USDT 100
go run ./examples/kucoin orders place --symbol LTC-USDT --price 50 --size 1 --order-type limit --side buy
go run ./examples/kucoin orders --symbol LTC-USDT --status active
go run ./examples/kucoin orders --symbol LTC-USDT --status done
go run ./examples/kucoin orders cancel --order-id 61b48b73b4de3e0001251382
```

View File

@ -0,0 +1,70 @@
package main
import (
"context"
"os"
"strings"
"github.com/c9s/bbgo/pkg/exchange/kucoin/kucoinapi"
"github.com/joho/godotenv"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func init() {
rootCmd.PersistentFlags().String("kucoin-api-key", "", "okex api key")
rootCmd.PersistentFlags().String("kucoin-api-secret", "", "okex api secret")
rootCmd.PersistentFlags().String("kucoin-api-passphrase", "", "okex api secret")
}
var rootCmd = &cobra.Command{
Use: "kucoin-accounts",
Short: "kucoin accounts",
// SilenceUsage is an option to silence usage when an error occurs.
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
accounts, err := client.AccountService.ListAccounts()
if err != nil {
return err
}
log.Infof("accounts: %+v", accounts)
return nil
},
}
var client *kucoinapi.RestClient = nil
func main() {
if _, err := os.Stat(".env.local"); err == nil {
if err := godotenv.Load(".env.local"); err != nil {
log.Fatal(err)
}
}
viper.AutomaticEnv()
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
if err := viper.BindPFlags(rootCmd.PersistentFlags()); err != nil {
log.WithError(err).Error("bind pflags error")
}
client = kucoinapi.NewClient()
key, secret, passphrase := viper.GetString("kucoin-api-key"),
viper.GetString("kucoin-api-secret"),
viper.GetString("kucoin-api-passphrase")
if len(key) == 0 || len(secret) == 0 || len(passphrase) == 0 {
log.Fatal("empty key, secret or passphrase")
}
client.Auth(key, secret, passphrase)
if err := rootCmd.ExecuteContext(context.Background()); err != nil {
log.WithError(err).Error("cmd error")
}
}

View File

@ -0,0 +1,70 @@
package main
import (
"context"
"os"
"strings"
"github.com/c9s/bbgo/pkg/exchange/kucoin/kucoinapi"
"github.com/joho/godotenv"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func init() {
rootCmd.PersistentFlags().String("kucoin-api-key", "", "okex api key")
rootCmd.PersistentFlags().String("kucoin-api-secret", "", "okex api secret")
rootCmd.PersistentFlags().String("kucoin-api-passphrase", "", "okex api secret")
}
var rootCmd = &cobra.Command{
Use: "kucoin-subaccount",
Short: "kucoin subaccount",
// SilenceUsage is an option to silence usage when an error occurs.
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
subAccounts, err := client.AccountService.QuerySubAccounts()
if err != nil {
return err
}
log.Infof("subAccounts: %+v", subAccounts)
return nil
},
}
var client *kucoinapi.RestClient = nil
func main() {
if _, err := os.Stat(".env.local"); err == nil {
if err := godotenv.Load(".env.local"); err != nil {
log.Fatal(err)
}
}
viper.AutomaticEnv()
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
if err := viper.BindPFlags(rootCmd.PersistentFlags()); err != nil {
log.WithError(err).Error("bind pflags error")
}
client = kucoinapi.NewClient()
key, secret, passphrase := viper.GetString("kucoin-api-key"),
viper.GetString("kucoin-api-secret"),
viper.GetString("kucoin-api-passphrase")
if len(key) == 0 || len(secret) == 0 || len(passphrase) == 0 {
log.Fatal("empty key, secret or passphrase")
}
client.Auth(key, secret, passphrase)
if err := rootCmd.ExecuteContext(context.Background()); err != nil {
log.WithError(err).Error("cmd error")
}
}

View File

@ -0,0 +1,34 @@
package main
import (
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var accountsCmd = &cobra.Command{
Use: "accounts",
// SilenceUsage is an option to silence usage when an error occurs.
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) > 0 {
account, err := client.AccountService.GetAccount(args[0])
if err != nil {
return err
}
logrus.Infof("account: %+v", account)
return nil
}
accounts, err := client.AccountService.ListAccounts()
if err != nil {
return err
}
logrus.Infof("accounts: %+v", accounts)
return nil
},
}

70
examples/kucoin/main.go Normal file
View File

@ -0,0 +1,70 @@
package main
import (
"context"
"os"
"strings"
"github.com/c9s/bbgo/pkg/exchange/kucoin/kucoinapi"
"github.com/joho/godotenv"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func init() {
rootCmd.PersistentFlags().String("kucoin-api-key", "", "okex api key")
rootCmd.PersistentFlags().String("kucoin-api-secret", "", "okex api secret")
rootCmd.PersistentFlags().String("kucoin-api-passphrase", "", "okex api secret")
rootCmd.AddCommand(accountsCmd)
rootCmd.AddCommand(subAccountsCmd)
rootCmd.AddCommand(symbolsCmd)
rootCmd.AddCommand(tickersCmd)
rootCmd.AddCommand(orderbookCmd)
}
var rootCmd = &cobra.Command{
Use: "kucoin",
Short: "kucoin",
// SilenceUsage is an option to silence usage when an error occurs.
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
return nil
},
}
var client *kucoinapi.RestClient = nil
func main() {
if _, err := os.Stat(".env.local"); err == nil {
if err := godotenv.Load(".env.local"); err != nil {
log.Fatal(err)
}
}
viper.AutomaticEnv()
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
if err := viper.BindPFlags(rootCmd.PersistentFlags()); err != nil {
log.WithError(err).Error("bind pflags error")
}
client = kucoinapi.NewClient()
key, secret, passphrase := viper.GetString("kucoin-api-key"),
viper.GetString("kucoin-api-secret"),
viper.GetString("kucoin-api-passphrase")
if len(key) == 0 || len(secret) == 0 || len(passphrase) == 0 {
log.Fatal("empty key, secret or passphrase")
}
client.Auth(key, secret, passphrase)
if err := rootCmd.ExecuteContext(context.Background()); err != nil {
log.WithError(err).Error("cmd error")
}
}

View File

@ -0,0 +1,40 @@
package main
import (
"strconv"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var orderbookCmd = &cobra.Command{
Use: "orderbook",
// SilenceUsage is an option to silence usage when an error occurs.
SilenceUsage: true,
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return nil
}
var depth = 0
if len(args) > 1 {
v, err := strconv.Atoi(args[1])
if err != nil {
return err
}
depth = v
}
orderBook, err := client.MarketDataService.GetOrderBook(args[0], depth)
if err != nil {
return err
}
logrus.Infof("orderBook: %+v", orderBook)
return nil
},
}

176
examples/kucoin/orders.go Normal file
View File

@ -0,0 +1,176 @@
package main
import (
"context"
"github.com/c9s/bbgo/pkg/exchange/kucoin/kucoinapi"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
func init() {
ordersCmd.Flags().String("symbol", "", "symbol, BTC-USDT, LTC-USDT...etc")
ordersCmd.Flags().String("status", "", "status, active or done")
rootCmd.AddCommand(ordersCmd)
cancelOrderCmd.Flags().String("client-order-id", "", "client order id")
cancelOrderCmd.Flags().String("order-id", "", "order id")
ordersCmd.AddCommand(cancelOrderCmd)
placeOrderCmd.Flags().String("symbol", "", "symbol")
placeOrderCmd.Flags().String("price", "", "price")
placeOrderCmd.Flags().String("size", "", "size")
placeOrderCmd.Flags().String("order-type", string(kucoinapi.OrderTypeLimit), "order type")
placeOrderCmd.Flags().String("side", "", "buy or sell")
ordersCmd.AddCommand(placeOrderCmd)
}
// go run ./examples/kucoin orders
var ordersCmd = &cobra.Command{
Use: "orders",
// SilenceUsage is an option to silence usage when an error occurs.
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
req := client.TradeService.NewListOrdersRequest()
symbol, err := cmd.Flags().GetString("symbol")
if err != nil {
return err
}
if len(symbol) == 0 {
return errors.New("--symbol option is required")
}
req.Symbol(symbol)
status, err := cmd.Flags().GetString("status")
if err != nil {
return err
}
if len(status) > 0 {
req.Status(status)
}
page, err := req.Do(context.Background())
if err != nil {
return err
}
logrus.Infof("page: %+v", page)
return nil
},
}
// usage:
// go run ./examples/kucoin orders place --symbol LTC-USDT --price 50 --size 1 --order-type limit --side buy
var placeOrderCmd = &cobra.Command{
Use: "place",
// SilenceUsage is an option to silence usage when an error occurs.
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
req := client.TradeService.NewPlaceOrderRequest()
orderType, err := cmd.Flags().GetString("order-type")
if err != nil {
return err
}
req.OrderType(kucoinapi.OrderType(orderType))
side, err := cmd.Flags().GetString("side")
if err != nil {
return err
}
req.Side(kucoinapi.SideType(side))
symbol, err := cmd.Flags().GetString("symbol")
if err != nil {
return err
}
if len(symbol) == 0 {
return errors.New("--symbol is required")
}
req.Symbol(symbol)
switch kucoinapi.OrderType(orderType) {
case kucoinapi.OrderTypeLimit:
price, err := cmd.Flags().GetString("price")
if err != nil {
return err
}
req.Price(price)
case kucoinapi.OrderTypeMarket:
}
size, err := cmd.Flags().GetString("size")
if err != nil {
return err
}
req.Size(size)
response, err := req.Do(context.Background())
if err != nil {
return err
}
logrus.Infof("place order response: %+v", response)
return nil
},
}
// usage:
var cancelOrderCmd = &cobra.Command{
Use: "cancel",
// SilenceUsage is an option to silence usage when an error occurs.
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
req := client.TradeService.NewCancelOrderRequest()
orderID, err := cmd.Flags().GetString("order-id")
if err != nil {
return err
}
clientOrderID, err := cmd.Flags().GetString("client-order-id")
if err != nil {
return err
}
if len(orderID) > 0 {
req.OrderID(orderID)
} else if len(clientOrderID) > 0 {
req.ClientOrderID(clientOrderID)
} else {
return errors.New("either order id or client order id is required")
}
response, err := req.Do(context.Background())
if err != nil {
return err
}
logrus.Infof("cancel order response: %+v", response)
return nil
},
}

View File

@ -0,0 +1,24 @@
package main
import (
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var subAccountsCmd = &cobra.Command{
Use: "subaccounts",
Short: "subaccounts",
// SilenceUsage is an option to silence usage when an error occurs.
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
subAccounts, err := client.AccountService.QuerySubAccounts()
if err != nil {
return err
}
logrus.Infof("subAccounts: %+v", subAccounts)
return nil
},
}

View File

@ -0,0 +1,24 @@
package main
import (
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var symbolsCmd = &cobra.Command{
Use: "symbols",
// SilenceUsage is an option to silence usage when an error occurs.
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
symbols, err := client.MarketDataService.ListSymbols(args...)
if err != nil {
return err
}
logrus.Infof("symbols: %+v", symbols)
return nil
},
}

View File

@ -0,0 +1,35 @@
package main
import (
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var tickersCmd = &cobra.Command{
Use: "tickers",
// SilenceUsage is an option to silence usage when an error occurs.
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
allTickers, err := client.MarketDataService.ListTickers()
if err != nil {
return err
}
logrus.Infof("allTickers: %+v", allTickers)
return nil
}
ticker, err := client.MarketDataService.GetTicker(args[0])
if err != nil {
return err
}
logrus.Infof("ticker: %+v", ticker)
return nil
},
}

View File

@ -0,0 +1,47 @@
package kucoin
import (
"context"
"github.com/c9s/bbgo/pkg/types"
"github.com/sirupsen/logrus"
)
// 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
}
func (e *Exchange) NewStream() types.Stream {
panic("implement me")
}
func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) {
panic("implement me")
}
func (e *Exchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticker, error) {
panic("implement me")
}
func (e *Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[string]types.Ticker, error) {
panic("implement me")
}
func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions) ([]types.KLine, error) {
panic("implement me")
}
func New(key, secret, passphrase string) *Exchange {
return &Exchange{
key: key,
secret: secret,
passphrase: passphrase,
}
}

View File

@ -0,0 +1,95 @@
package kucoinapi
import "github.com/c9s/bbgo/pkg/fixedpoint"
type AccountService struct {
client *RestClient
}
type SubAccount struct {
UserID string `json:"userId"`
Name string `json:"subName"`
Type string `json:"type"`
Remark string `json:"remarks"`
}
func (s *AccountService) QuerySubAccounts() ([]SubAccount, error) {
req, err := s.client.newAuthenticatedRequest("GET", "/api/v1/sub/user", nil, nil)
if err != nil {
return nil, err
}
response, err := s.client.sendRequest(req)
if err != nil {
return nil, err
}
var apiResponse struct {
Code string `json:"code"`
Message string `json:"msg"`
Data []SubAccount `json:"data"`
}
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
return apiResponse.Data, nil
}
type Account struct {
ID string `json:"id"`
Currency string `json:"currency"`
Type string `json:"type"`
Balance fixedpoint.Value `json:"balance"`
Available fixedpoint.Value `json:"available"`
Holds fixedpoint.Value `json:"holds"`
}
func (s *AccountService) ListAccounts() ([]Account, error) {
req, err := s.client.newAuthenticatedRequest("GET", "/api/v1/accounts", nil, nil)
if err != nil {
return nil, err
}
response, err := s.client.sendRequest(req)
if err != nil {
return nil, err
}
var apiResponse struct {
Code string `json:"code"`
Message string `json:"msg"`
Data []Account `json:"data"`
}
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
return apiResponse.Data, nil
}
func (s *AccountService) GetAccount(accountID string) (*Account, error) {
req, err := s.client.newAuthenticatedRequest("GET", "/api/v1/accounts/" + accountID, nil, nil)
if err != nil {
return nil, err
}
response, err := s.client.sendRequest(req)
if err != nil {
return nil, err
}
var apiResponse struct {
Code string `json:"code"`
Message string `json:"msg"`
Data *Account `json:"data"`
}
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
return apiResponse.Data, nil
}

View File

@ -0,0 +1,394 @@
package kucoinapi
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/util"
"github.com/pkg/errors"
)
const defaultHTTPTimeout = time.Second * 15
const RestBaseURL = "https://api.kucoin.com/api"
const SandboxRestBaseURL = "https://openapi-sandbox.kucoin.com/api"
type TradeType string
const (
TradeTypeSpot TradeType = "TRADE"
TradeTypeMargin TradeType = "MARGIN"
)
type SideType string
const (
SideTypeBuy SideType = "buy"
SideTypeSell SideType = "sell"
)
type TimeInForceType string
const (
// GTC Good Till Canceled orders remain open on the book until canceled. This is the default behavior if no policy is specified.
TimeInForceGTC TimeInForceType = "GTC"
// GTT Good Till Time orders remain open on the book until canceled or the allotted cancelAfter is depleted on the matching engine. GTT orders are guaranteed to cancel before any other order is processed after the cancelAfter seconds placed in order book.
TimeInForceGTT TimeInForceType = "GTT"
// FOK Fill Or Kill orders are rejected if the entire size cannot be matched.
TimeInForceFOK TimeInForceType = "FOK"
// IOC Immediate Or Cancel orders instantly cancel the remaining size of the limit order instead of opening it on the book.
TimeInForceIOC TimeInForceType = "IOC"
)
type OrderType string
const (
OrderTypeMarket OrderType = "market"
OrderTypeLimit OrderType = "limit"
)
type InstrumentType string
const (
InstrumentTypeSpot InstrumentType = "SPOT"
InstrumentTypeSwap InstrumentType = "SWAP"
InstrumentTypeFutures InstrumentType = "FUTURES"
InstrumentTypeOption InstrumentType = "OPTION"
)
type OrderState string
const (
OrderStateCanceled OrderState = "canceled"
OrderStateLive OrderState = "live"
OrderStatePartiallyFilled OrderState = "partially_filled"
OrderStateFilled OrderState = "filled"
)
type RestClient struct {
BaseURL *url.URL
client *http.Client
Key, Secret, Passphrase string
KeyVersion string
AccountService *AccountService
MarketDataService *MarketDataService
TradeService *TradeService
}
func NewClient() *RestClient {
u, err := url.Parse(RestBaseURL)
if err != nil {
panic(err)
}
client := &RestClient{
BaseURL: u,
KeyVersion: "2",
client: &http.Client{
Timeout: defaultHTTPTimeout,
},
}
client.AccountService = &AccountService{client: client}
client.MarketDataService = &MarketDataService{client: client}
client.TradeService = &TradeService{client: client}
return client
}
func (c *RestClient) Auth(key, secret, passphrase string) {
c.Key = key
c.Secret = secret
c.Passphrase = passphrase
}
// NewRequest create new API request. Relative url can be provided in refURL.
func (c *RestClient) newRequest(method, refURL string, params url.Values, body []byte) (*http.Request, error) {
rel, err := url.Parse(refURL)
if err != nil {
return nil, err
}
if params != nil {
rel.RawQuery = params.Encode()
}
pathURL := c.BaseURL.ResolveReference(rel)
return http.NewRequest(method, pathURL.String(), bytes.NewReader(body))
}
// sendRequest sends the request to the API server and handle the response
func (c *RestClient) sendRequest(req *http.Request) (*util.Response, error) {
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
// newResponse reads the response body and return a new Response object
response, err := util.NewResponse(resp)
if err != nil {
return response, err
}
// Check error, if there is an error, return the ErrorResponse struct type
if response.IsError() {
return response, errors.New(string(response.Body))
}
return response, nil
}
// newAuthenticatedRequest creates new http request for authenticated routes.
func (c *RestClient) newAuthenticatedRequest(method, refURL string, params url.Values, payload interface{}) (*http.Request, error) {
if len(c.Key) == 0 {
return nil, errors.New("empty api key")
}
if len(c.Secret) == 0 {
return nil, errors.New("empty api secret")
}
rel, err := url.Parse(refURL)
if err != nil {
return nil, err
}
if params != nil {
rel.RawQuery = params.Encode()
}
pathURL := c.BaseURL.ResolveReference(rel)
path := pathURL.Path
if rel.RawQuery != "" {
path += "?" + rel.RawQuery
}
// set location to UTC so that it outputs "2020-12-08T09:08:57.715Z"
t := time.Now().In(time.UTC)
// timestamp := t.Format("2006-01-02T15:04:05.999Z07:00")
timestamp := strconv.FormatInt(t.UnixMilli(), 10)
var body []byte
if payload != nil {
switch v := payload.(type) {
case string:
body = []byte(v)
case []byte:
body = v
default:
body, err = json.Marshal(v)
if err != nil {
return nil, err
}
}
}
signKey := timestamp + strings.ToUpper(method) + path + string(body)
signature := sign(c.Secret, signKey)
req, err := http.NewRequest(method, pathURL.String(), bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
req.Header.Add("KC-API-KEY", c.Key)
req.Header.Add("KC-API-SIGN", signature)
req.Header.Add("KC-API-TIMESTAMP", timestamp)
req.Header.Add("KC-API-PASSPHRASE", sign(c.Secret, c.Passphrase))
req.Header.Add("KC-API-KEY-VERSION", c.KeyVersion)
return req, nil
}
type BalanceDetail struct {
Currency string `json:"ccy"`
Available fixedpoint.Value `json:"availEq"`
CashBalance fixedpoint.Value `json:"cashBal"`
OrderFrozen fixedpoint.Value `json:"ordFrozen"`
Frozen fixedpoint.Value `json:"frozenBal"`
Equity fixedpoint.Value `json:"eq"`
EquityInUSD fixedpoint.Value `json:"eqUsd"`
UpdateTime types.MillisecondTimestamp `json:"uTime"`
UnrealizedProfitAndLoss fixedpoint.Value `json:"upl"`
}
type AssetBalance struct {
Currency string `json:"ccy"`
Balance fixedpoint.Value `json:"bal"`
Frozen fixedpoint.Value `json:"frozenBal,omitempty"`
Available fixedpoint.Value `json:"availBal,omitempty"`
}
type AssetBalanceList []AssetBalance
func (c *RestClient) AssetBalances() (AssetBalanceList, error) {
req, err := c.newAuthenticatedRequest("GET", "/api/v5/asset/balances", nil, nil)
if err != nil {
return nil, err
}
response, err := c.sendRequest(req)
if err != nil {
return nil, err
}
var balanceResponse struct {
Code string `json:"code"`
Message string `json:"msg"`
Data AssetBalanceList `json:"data"`
}
if err := response.DecodeJSON(&balanceResponse); err != nil {
return nil, err
}
return balanceResponse.Data, nil
}
type AssetCurrency struct {
Currency string `json:"ccy"`
Name string `json:"name"`
Chain string `json:"chain"`
CanDeposit bool `json:"canDep"`
CanWithdraw bool `json:"canWd"`
CanInternal bool `json:"canInternal"`
MinWithdrawalFee fixedpoint.Value `json:"minFee"`
MaxWithdrawalFee fixedpoint.Value `json:"maxFee"`
MinWithdrawalThreshold fixedpoint.Value `json:"minWd"`
}
func (c *RestClient) AssetCurrencies() ([]AssetCurrency, error) {
req, err := c.newAuthenticatedRequest("GET", "/api/v5/asset/currencies", nil, nil)
if err != nil {
return nil, err
}
response, err := c.sendRequest(req)
if err != nil {
return nil, err
}
var currencyResponse struct {
Code string `json:"code"`
Message string `json:"msg"`
Data []AssetCurrency `json:"data"`
}
if err := response.DecodeJSON(&currencyResponse); err != nil {
return nil, err
}
return currencyResponse.Data, nil
}
type MarketTicker struct {
InstrumentType string `json:"instType"`
InstrumentID string `json:"instId"`
// last traded price
Last fixedpoint.Value `json:"last"`
// last traded size
LastSize fixedpoint.Value `json:"lastSz"`
AskPrice fixedpoint.Value `json:"askPx"`
AskSize fixedpoint.Value `json:"askSz"`
BidPrice fixedpoint.Value `json:"bidPx"`
BidSize fixedpoint.Value `json:"bidSz"`
Open24H fixedpoint.Value `json:"open24h"`
High24H fixedpoint.Value `json:"high24H"`
Low24H fixedpoint.Value `json:"low24H"`
Volume24H fixedpoint.Value `json:"vol24h"`
VolumeCurrency24H fixedpoint.Value `json:"volCcy24h"`
// Millisecond timestamp
Timestamp types.MillisecondTimestamp `json:"ts"`
}
func (c *RestClient) MarketTicker(instId string) (*MarketTicker, error) {
// SPOT, SWAP, FUTURES, OPTION
var params = url.Values{}
params.Add("instId", instId)
req, err := c.newRequest("GET", "/api/v5/market/ticker", params, nil)
if err != nil {
return nil, err
}
response, err := c.sendRequest(req)
if err != nil {
return nil, err
}
var tickerResponse struct {
Code string `json:"code"`
Message string `json:"msg"`
Data []MarketTicker `json:"data"`
}
if err := response.DecodeJSON(&tickerResponse); err != nil {
return nil, err
}
if len(tickerResponse.Data) == 0 {
return nil, fmt.Errorf("ticker of %s not found", instId)
}
return &tickerResponse.Data[0], nil
}
func (c *RestClient) MarketTickers(instType InstrumentType) ([]MarketTicker, error) {
// SPOT, SWAP, FUTURES, OPTION
var params = url.Values{}
params.Add("instType", string(instType))
req, err := c.newRequest("GET", "/api/v5/market/tickers", params, nil)
if err != nil {
return nil, err
}
response, err := c.sendRequest(req)
if err != nil {
return nil, err
}
var tickerResponse struct {
Code string `json:"code"`
Message string `json:"msg"`
Data []MarketTicker `json:"data"`
}
if err := response.DecodeJSON(&tickerResponse); err != nil {
return nil, err
}
return tickerResponse.Data, nil
}
func sign(secret, payload string) string {
var sig = hmac.New(sha256.New, []byte(secret))
_, err := sig.Write([]byte(payload))
if err != nil {
return ""
}
return base64.StdEncoding.EncodeToString(sig.Sum(nil))
}

View File

@ -0,0 +1,253 @@
package kucoinapi
import (
"fmt"
"net/http"
"net/url"
"strconv"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
"github.com/pkg/errors"
)
type MarketDataService struct {
client *RestClient
}
type Symbol struct {
Symbol string `json:"symbol"`
Name string `json:"name"`
BaseCurrency string `json:"baseCurrency"`
QuoteCurrency string `json:"quoteCurrency"`
FeeCurrency string `json:"feeCurrency"`
Market string `json:"market"`
BaseMinSize fixedpoint.Value `json:"baseMinSize"`
QuoteMinSize fixedpoint.Value `json:"quoteMinSize"`
BaseIncrement fixedpoint.Value `json:"baseIncrement"`
QuoteIncrement fixedpoint.Value `json:"quoteIncrement"`
PriceIncrement fixedpoint.Value `json:"priceIncrement"`
PriceLimitRate fixedpoint.Value `json:"priceLimitRate"`
IsMarginEnabled bool `json:"isMarginEnabled"`
EnableTrading bool `json:"enableTrading"`
}
func (s *MarketDataService) ListSymbols(market ...string) ([]Symbol, error) {
var params = url.Values{}
if len(market) == 1 {
params["market"] = []string{market[0]}
} else if len(market) > 1 {
return nil, errors.New("symbols api only supports one market parameter")
}
req, err := s.client.newRequest("GET", "/api/v1/symbols", params, nil)
if err != nil {
return nil, err
}
response, err := s.client.sendRequest(req)
if err != nil {
return nil, err
}
var apiResponse struct {
Code string `json:"code"`
Message string `json:"msg"`
Data []Symbol `json:"data"`
}
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
return apiResponse.Data, nil
}
/*
//Get Ticker
{
"sequence": "1550467636704",
"bestAsk": "0.03715004",
"size": "0.17",
"price": "0.03715005",
"bestBidSize": "3.803",
"bestBid": "0.03710768",
"bestAskSize": "1.788",
"time": 1550653727731
}
*/
type Ticker struct {
Sequence string `json:"sequence"`
Size fixedpoint.Value `json:"size"`
Price fixedpoint.Value `json:"price"`
BestAsk fixedpoint.Value `json:"bestAsk"`
BestBid fixedpoint.Value `json:"bestBid"`
BestBidSize fixedpoint.Value `json:"bestBidSize"`
Time types.MillisecondTimestamp `json:"time"`
}
func (s *MarketDataService) GetTicker(symbol string) (*Ticker, error) {
var params = url.Values{}
params["symbol"] = []string{symbol}
req, err := s.client.newRequest("GET", "/api/v1/market/orderbook/level1", params, nil)
if err != nil {
return nil, err
}
response, err := s.client.sendRequest(req)
if err != nil {
return nil, err
}
var apiResponse struct {
Code string `json:"code"`
Message string `json:"msg"`
Data *Ticker `json:"data"`
}
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
return apiResponse.Data, nil
}
/*
{
"time":1602832092060,
"ticker":[
{
"symbol": "BTC-USDT", // symbol
"symbolName":"BTC-USDT", // Name of trading pairs, it would change after renaming
"buy": "11328.9", // bestAsk
"sell": "11329", // bestBid
"changeRate": "-0.0055", // 24h change rate
"changePrice": "-63.6", // 24h change price
"high": "11610", // 24h highest price
"low": "11200", // 24h lowest price
"vol": "2282.70993217", // 24h volumethe aggregated trading volume in BTC
"volValue": "25984946.157790431", // 24h total, the trading volume in quote currency of last 24 hours
"last": "11328.9", // last price
"averagePrice": "11360.66065903", // 24h average transaction price yesterday
"takerFeeRate": "0.001", // Basic Taker Fee
"makerFeeRate": "0.001", // Basic Maker Fee
"takerCoefficient": "1", // Taker Fee Coefficient
"makerCoefficient": "1" // Maker Fee Coefficient
}
]
}
*/
type Ticker24H struct {
Symbol string `json:"symbol"`
Name string `json:"symbolName"`
Buy fixedpoint.Value `json:"buy"`
Sell fixedpoint.Value `json:"sell"`
ChangeRate fixedpoint.Value `json:"changeRate"`
ChangePrice fixedpoint.Value `json:"changePrice"`
High fixedpoint.Value `json:"high"`
Low fixedpoint.Value `json:"low"`
Last fixedpoint.Value `json:"last"`
AveragePrice fixedpoint.Value `json:"averagePrice"`
Volume fixedpoint.Value `json:"vol"` // base volume
VolumeValue fixedpoint.Value `json:"volValue"` // quote volume
TakerFeeRate fixedpoint.Value `json:"takerFeeRate"`
MakerFeeRate fixedpoint.Value `json:"makerFeeRate"`
TakerCoefficient fixedpoint.Value `json:"takerCoefficient"`
MakerCoefficient fixedpoint.Value `json:"makerCoefficient"`
}
type AllTickers struct {
Time types.MillisecondTimestamp `json:"time"`
Ticker []Ticker24H `json:"ticker"`
}
func (s *MarketDataService) ListTickers() (*AllTickers, error) {
req, err := s.client.newRequest("GET", "/api/v1/market/allTickers", nil, nil)
if err != nil {
return nil, err
}
response, err := s.client.sendRequest(req)
if err != nil {
return nil, err
}
var apiResponse struct {
Code string `json:"code"`
Message string `json:"msg"`
Data *AllTickers `json:"data"`
}
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
return apiResponse.Data, nil
}
/*
{
"sequence": "3262786978",
"time": 1550653727731,
"bids": [["6500.12", "0.45054140"],
["6500.11", "0.45054140"]], //[pricesize]
"asks": [["6500.16", "0.57753524"],
["6500.15", "0.57753524"]]
}
*/
type OrderBook struct {
Sequence string `json:"sequence"`
Time types.MillisecondTimestamp `json:"time"`
Bids [][]fixedpoint.Value `json:"bids,omitempty"`
Asks [][]fixedpoint.Value `json:"asks,omitempty"`
}
func (s *MarketDataService) GetOrderBook(symbol string, depth int) (*OrderBook, error) {
params := url.Values{}
params["symbol"] = []string{symbol}
var req *http.Request
var err error
switch depth {
case 20, 100:
refURL := "/api/v1/market/orderbook/level2_" + strconv.Itoa(depth)
req, err = s.client.newRequest("GET", refURL, params, nil)
if err != nil {
return nil, err
}
case 0:
refURL := "/api/v3/market/orderbook/level2"
req, err = s.client.newAuthenticatedRequest("GET", refURL, params, nil)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("depth %d is not supported, use 20, 100 or 0", depth)
}
response, err := s.client.sendRequest(req)
if err != nil {
return nil, err
}
var apiResponse struct {
Code string `json:"code"`
Message string `json:"msg"`
Data *OrderBook `json:"data"`
}
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
return apiResponse.Data, nil
}

View File

@ -0,0 +1,477 @@
package kucoinapi
import (
"context"
"net/url"
"strconv"
"time"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
"github.com/google/uuid"
"github.com/pkg/errors"
)
type TradeService struct {
client *RestClient
}
type OrderResponse struct {
OrderID string `json:"orderId"`
}
func (c *TradeService) NewPlaceOrderRequest() *PlaceOrderRequest {
return &PlaceOrderRequest{
client: c.client,
}
}
func (c *TradeService) NewBatchPlaceOrderRequest() *BatchPlaceOrderRequest {
return &BatchPlaceOrderRequest{
client: c.client,
}
}
func (c *TradeService) NewCancelOrderRequest() *CancelOrderRequest {
return &CancelOrderRequest{
client: c.client,
}
}
func (c *TradeService) NewCancelAllOrderRequest() *CancelAllOrderRequest {
return &CancelAllOrderRequest{
client: c.client,
}
}
type ListOrdersRequest struct {
client *RestClient
status *string
symbol *string
side *SideType
orderType *OrderType
tradeType *TradeType
startAt *time.Time
endAt *time.Time
}
func (r *ListOrdersRequest) Status(status string) {
r.status = &status
}
func (r *ListOrdersRequest) Symbol(symbol string) {
r.symbol = &symbol
}
func (r *ListOrdersRequest) Side(side SideType) {
r.side = &side
}
func (r *ListOrdersRequest) OrderType(orderType OrderType) {
r.orderType = &orderType
}
func (r *ListOrdersRequest) StartAt(startAt time.Time) {
r.startAt = &startAt
}
func (r *ListOrdersRequest) EndAt(endAt time.Time) {
r.endAt = &endAt
}
type Order struct {
ID string `json:"id"`
Symbol string `json:"symbol"`
OperationType string `json:"opType"`
Type string `json:"type"`
Side string `json:"side"`
Price fixedpoint.Value `json:"price"`
Size fixedpoint.Value `json:"size"`
Funds fixedpoint.Value `json:"funds"`
DealFunds fixedpoint.Value `json:"dealFunds"`
DealSize fixedpoint.Value `json:"dealSize"`
Fee fixedpoint.Value `json:"fee"`
FeeCurrency string `json:"feeCurrency"`
StopType string `json:"stop"`
StopTriggerred bool `json:"stopTriggered"`
StopPrice fixedpoint.Value `json:"stopPrice"`
TimeInForce TimeInForceType `json:"timeInForce"`
PostOnly bool `json:"postOnly"`
Hidden bool `json:"hidden"`
Iceberg bool `json:"iceberg"`
Channel string `json:"channel"`
ClientOrderID string `json:"clientOid"`
Remark string `json:"remark"`
IsActive bool `json:"isActive"`
CancelExist bool `json:"cancelExist"`
CreatedAt types.MillisecondTimestamp `json:"createdAt"`
}
type OrderListPage struct {
CurrentPage int `json:"currentPage"`
PageSize int `json:"pageSize"`
TotalNumber int `json:"totalNum"`
TotalPage int `json:"totalPage"`
Items []Order `json:"items"`
}
func (r *ListOrdersRequest) Do(ctx context.Context) (*OrderListPage, error) {
var params = url.Values{}
if r.status != nil {
params["status"] = []string{*r.status}
}
if r.symbol != nil {
params["symbol"] = []string{*r.symbol}
}
if r.side != nil {
params["side"] = []string{string(*r.side)}
}
if r.orderType != nil {
params["type"] = []string{string(*r.orderType)}
}
if r.tradeType != nil {
params["tradeType"] = []string{string(*r.tradeType)}
} else {
params["tradeType"] = []string{"TRADE"}
}
if r.startAt != nil {
params["startAt"] = []string{strconv.FormatInt(r.startAt.UnixNano()/int64(time.Millisecond), 10)}
}
if r.endAt != nil {
params["endAt"] = []string{strconv.FormatInt(r.endAt.UnixNano()/int64(time.Millisecond), 10)}
}
req, err := r.client.newAuthenticatedRequest("GET", "/api/v1/orders", params, nil)
if err != nil {
return nil, err
}
response, err := r.client.sendRequest(req)
if err != nil {
return nil, err
}
var orderResponse struct {
Code string `json:"code"`
Message string `json:"msg"`
Data *OrderListPage `json:"data"`
}
if err := response.DecodeJSON(&orderResponse); err != nil {
return nil, err
}
if orderResponse.Data == nil {
return nil, errors.New("api error: [" + orderResponse.Code + "] " + orderResponse.Message)
}
return orderResponse.Data, nil
}
func (c *TradeService) NewListOrdersRequest() *ListOrdersRequest {
return &ListOrdersRequest{client: c.client}
}
type PlaceOrderRequest struct {
client *RestClient
// A combination of case-sensitive alphanumerics, all numbers, or all letters of up to 32 characters.
clientOrderID *string
symbol string
// A combination of case-sensitive alphanumerics, all numbers, or all letters of up to 8 characters.
tag *string
// "buy" or "sell"
side SideType
ordType OrderType
// limit order parameters
size string
price *string
timeInForce *TimeInForceType
}
func (r *PlaceOrderRequest) Symbol(symbol string) *PlaceOrderRequest {
r.symbol = symbol
return r
}
func (r *PlaceOrderRequest) ClientOrderID(clientOrderID string) *PlaceOrderRequest {
r.clientOrderID = &clientOrderID
return r
}
func (r *PlaceOrderRequest) Side(side SideType) *PlaceOrderRequest {
r.side = side
return r
}
func (r *PlaceOrderRequest) Size(size string) *PlaceOrderRequest {
r.size = size
return r
}
func (r *PlaceOrderRequest) Price(price string) *PlaceOrderRequest {
r.price = &price
return r
}
func (r *PlaceOrderRequest) TimeInForce(timeInForce TimeInForceType) *PlaceOrderRequest {
r.timeInForce = &timeInForce
return r
}
func (r *PlaceOrderRequest) OrderType(orderType OrderType) *PlaceOrderRequest {
r.ordType = orderType
return r
}
func (r *PlaceOrderRequest) getParameters() (map[string]interface{}, error) {
payload := map[string]interface{}{}
payload["symbol"] = r.symbol
if r.clientOrderID != nil {
payload["clientOid"] = r.clientOrderID
} else {
payload["clientOid"] = uuid.New().String()
}
if len(r.side) == 0 {
return nil, errors.New("order side is required")
}
payload["side"] = r.side
payload["type"] = r.ordType
payload["size"] = r.size
if r.price != nil {
payload["price"] = r.price
}
if r.timeInForce != nil {
payload["timeInForce"] = r.timeInForce
}
return payload, nil
}
func (r *PlaceOrderRequest) Do(ctx context.Context) (*OrderResponse, error) {
payload, err := r.getParameters()
if err != nil {
return nil, err
}
req, err := r.client.newAuthenticatedRequest("POST", "/api/v1/orders", nil, payload)
if err != nil {
return nil, err
}
response, err := r.client.sendRequest(req)
if err != nil {
return nil, err
}
var orderResponse struct {
Code string `json:"code"`
Message string `json:"msg"`
Data *OrderResponse `json:"data"`
}
if err := response.DecodeJSON(&orderResponse); err != nil {
return nil, err
}
if orderResponse.Data == nil {
return nil, errors.New("api error: [" + orderResponse.Code + "] " + orderResponse.Message)
}
return orderResponse.Data, nil
}
type CancelOrderRequest struct {
client *RestClient
orderID *string
clientOrderID *string
}
func (r *CancelOrderRequest) OrderID(orderID string) *CancelOrderRequest {
r.orderID = &orderID
return r
}
func (r *CancelOrderRequest) ClientOrderID(clientOrderID string) *CancelOrderRequest {
r.clientOrderID = &clientOrderID
return r
}
type CancelOrderResponse struct {
CancelledOrderIDs []string `json:"cancelledOrderIds,omitempty"`
// used when using client order id for canceling order
CancelledOrderId string `json:"cancelledOrderId,omitempty"`
ClientOrderID string `json:"clientOid,omitempty"`
}
func (r *CancelOrderRequest) Do(ctx context.Context) (*CancelOrderResponse, error) {
if r.orderID == nil && r.clientOrderID == nil {
return nil, errors.New("either orderID or clientOrderID is required for canceling order")
}
var refURL string
if r.orderID != nil {
refURL = "/api/v1/orders/" + *r.orderID
} else if r.clientOrderID != nil {
refURL = "/api/v1/order/client-order/" + *r.clientOrderID
}
req, err := r.client.newAuthenticatedRequest("DELETE", refURL, nil, nil)
if err != nil {
return nil, err
}
response, err := r.client.sendRequest(req)
if err != nil {
return nil, err
}
var apiResponse struct {
Code string `json:"code"`
Message string `json:"msg"`
Data *CancelOrderResponse `json:"data"`
}
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
if apiResponse.Data == nil {
return nil, errors.New("api error: [" + apiResponse.Code + "] " + apiResponse.Message)
}
return apiResponse.Data, nil
}
type CancelAllOrderRequest struct {
client *RestClient
symbol *string
// tradeType string
}
func (r *CancelAllOrderRequest) Symbol(symbol string) *CancelAllOrderRequest {
r.symbol = &symbol
return r
}
func (r *CancelAllOrderRequest) Do(ctx context.Context) (*CancelOrderResponse, error) {
req, err := r.client.newAuthenticatedRequest("DELETE", "/api/v1/orders", nil, nil)
if err != nil {
return nil, err
}
response, err := r.client.sendRequest(req)
if err != nil {
return nil, err
}
var apiResponse struct {
Code string `json:"code"`
Message string `json:"msg"`
Data *CancelOrderResponse `json:"data"`
}
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
if apiResponse.Data == nil {
return nil, errors.New("api error: [" + apiResponse.Code + "] " + apiResponse.Message)
}
return apiResponse.Data, nil
}
// Request via this endpoint to place 5 orders at the same time.
// The order type must be a limit order of the same symbol.
// The interface currently only supports spot trading
type BatchPlaceOrderRequest struct {
client *RestClient
symbol string
reqs []*PlaceOrderRequest
}
func (r *BatchPlaceOrderRequest) Symbol(symbol string) *BatchPlaceOrderRequest {
r.symbol = symbol
return r
}
func (r *BatchPlaceOrderRequest) Add(reqs ...*PlaceOrderRequest) *BatchPlaceOrderRequest {
r.reqs = append(r.reqs, reqs...)
return r
}
func (r *BatchPlaceOrderRequest) Do(ctx context.Context) ([]OrderResponse, error) {
var orderList []map[string]interface{}
for _, req := range r.reqs {
params, err := req.getParameters()
if err != nil {
return nil, err
}
orderList = append(orderList, params)
}
var payload = map[string]interface{}{
"symbol": r.symbol,
"orderList": orderList,
}
req, err := r.client.newAuthenticatedRequest("POST", "/api/v1/orders/multi", nil, payload)
if err != nil {
return nil, err
}
response, err := r.client.sendRequest(req)
if err != nil {
return nil, err
}
var apiResponse struct {
Code string `json:"code"`
Message string `json:"msg"`
Data []OrderResponse `json:"data"`
}
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
if apiResponse.Data == nil {
return nil, errors.New("api error: [" + apiResponse.Code + "] " + apiResponse.Message)
}
return apiResponse.Data, nil
}