mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-22 23:05:15 +00:00
395 lines
10 KiB
Go
395 lines
10 KiB
Go
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))
|
|
}
|