bbgo_origin/pkg/exchange/kucoin/kucoinapi/client.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(&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))
}