mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-26 08:45:16 +00:00
Merge pull request #349 from c9s/feature/kucoin
feature: integrate kucoin api
This commit is contained in:
commit
2e7b69320b
16
doc/development/kucoin-cli.md
Normal file
16
doc/development/kucoin-cli.md
Normal 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
|
||||
```
|
70
examples/kucoin-accounts/main.go
Normal file
70
examples/kucoin-accounts/main.go
Normal 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")
|
||||
}
|
||||
}
|
70
examples/kucoin-subaccount/main.go
Normal file
70
examples/kucoin-subaccount/main.go
Normal 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")
|
||||
}
|
||||
}
|
34
examples/kucoin/accounts.go
Normal file
34
examples/kucoin/accounts.go
Normal 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
70
examples/kucoin/main.go
Normal 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")
|
||||
}
|
||||
}
|
40
examples/kucoin/orderbook.go
Normal file
40
examples/kucoin/orderbook.go
Normal 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
176
examples/kucoin/orders.go
Normal 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
|
||||
},
|
||||
}
|
24
examples/kucoin/subaccounts.go
Normal file
24
examples/kucoin/subaccounts.go
Normal 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
|
||||
},
|
||||
}
|
24
examples/kucoin/symbols.go
Normal file
24
examples/kucoin/symbols.go
Normal 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
|
||||
},
|
||||
}
|
||||
|
35
examples/kucoin/tickers.go
Normal file
35
examples/kucoin/tickers.go
Normal 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
|
||||
},
|
||||
}
|
||||
|
47
pkg/exchange/kucoin/exchange.go
Normal file
47
pkg/exchange/kucoin/exchange.go
Normal 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,
|
||||
}
|
||||
}
|
95
pkg/exchange/kucoin/kucoinapi/account.go
Normal file
95
pkg/exchange/kucoin/kucoinapi/account.go
Normal 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
|
||||
}
|
394
pkg/exchange/kucoin/kucoinapi/client.go
Normal file
394
pkg/exchange/kucoin/kucoinapi/client.go
Normal 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(¤cyResponse); 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))
|
||||
}
|
253
pkg/exchange/kucoin/kucoinapi/marketdata.go
Normal file
253
pkg/exchange/kucoin/kucoinapi/marketdata.go
Normal 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 volume,the 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"]], //[price,size]
|
||||
"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
|
||||
}
|
477
pkg/exchange/kucoin/kucoinapi/trade.go
Normal file
477
pkg/exchange/kucoin/kucoinapi/trade.go
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user