Files
gocryptotrader/exchanges/coinbene/coinbene.go
Ryan O'Hara-Reid d636049fb2 exchanges: Initial context propagation (#744)
* gct: phase one context awareness pass

* exchanges: context propagation pass

* common/requester: force context requirement

* gctcli/exchanges: linter fix

* rpcserver: fix test using dummy rpc server

* backtester: fix comments

* grpc: add correct cancel and timeout for commands

* rpcserver_test: add comment on dummy server

* common: deprecated SendHTTPGetRequest

* linter: fix

* linter: turn on no context check

* apichecker: fix context linter issue

* binance: use param context

* common: remove checks as this gets executed before main

* common: change mutex to RW as clients can be used by multiple go routines.

* common: remove init and JIT default client. Unexport global variables and add protection.

* common: Add comments

* bithumb: after dinner mints fix
2021-09-11 13:52:07 +10:00

1245 lines
31 KiB
Go

package coinbene
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
)
// Coinbene is the overarching type across this package
type Coinbene struct {
exchange.Base
}
const (
coinbeneAPIURL = "https://openapi-exchange.coinbene.com/api/exchange/"
coinbeneSwapAPIURL = "https://openapi-contract.coinbene.com/api/usdt/"
coinbeneAuthPath = "/api/exchange/v2"
coinbeneSwapAuthPath = "/api/usdt/v2"
coinbeneAPIVersion = "v2"
// Public endpoints
coinbeneGetTicker = "/market/ticker/one"
coinbeneGetTickersSpot = "/market/ticker/list"
coinbeneGetTickers = "/market/tickers"
coinbeneGetOrderBook = "/market/orderBook"
coinbeneGetKlines = "/market/klines"
coinbeneGetInstruments = "/market/instruments"
// TODO: Implement function ---
coinbeneSpotKlines = "/market/instruments/candles"
coinbeneSpotExchangeRate = "/market/rate/list"
// ---
coinbeneGetTrades = "/market/trades"
coinbeneGetAllPairs = "/market/tradePair/list"
coinbenePairInfo = "/market/tradePair/one"
// Authenticated endpoints
coinbeneAccountInfo = "/account/info"
coinbeneGetUserBalance = "/account/list"
coinbeneAccountBalanceOne = "/account/one"
coinbenePlaceOrder = "/order/place"
coinbeneBatchPlaceOrder = "/order/batchPlaceOrder"
coinbeneTradeFills = "/order/trade/fills"
coinbeneOrderFills = "/order/fills"
coinbeneOrderInfo = "/order/info"
coinbeneCancelOrder = "/order/cancel"
coinbeneBatchCancel = "/order/batchCancel"
coinbeneOpenOrders = "/order/openOrders"
coinbeneOpenOrdersByPage = "/order/openOrdersByPage"
coinbeneClosedOrders = "/order/closedOrders"
coinbeneClosedOrdersByPage = "/order/closedOrdersByPage"
coinbeneListSwapPositions = "/position/list"
coinbenePositionFeeRate = "/position/feeRate"
limitOrder = "1"
marketOrder = "2"
postOnlyOrder = "8"
fillOrKillOrder = "9"
iosOrder = "10"
buyDirection = "1"
openLong = "openLong"
openShort = "openShort"
sellDirection = "2"
)
// GetAllPairs gets all pairs on the exchange
func (c *Coinbene) GetAllPairs(ctx context.Context) ([]PairData, error) {
resp := struct {
Data []PairData `json:"data"`
}{}
path := coinbeneAPIVersion + coinbeneGetAllPairs
return resp.Data, c.SendHTTPRequest(ctx, exchange.RestSpot, path, spotPairs, &resp)
}
// GetPairInfo gets info about a single pair
func (c *Coinbene) GetPairInfo(ctx context.Context, symbol string) (PairData, error) {
resp := struct {
Data PairData `json:"data"`
}{}
params := url.Values{}
params.Set("symbol", symbol)
path := common.EncodeURLValues(coinbeneAPIVersion+coinbenePairInfo, params)
return resp.Data, c.SendHTTPRequest(ctx, exchange.RestSpot, path, spotPairInfo, &resp)
}
// GetOrderbook gets and stores orderbook data for given pair
func (c *Coinbene) GetOrderbook(ctx context.Context, symbol string, size int64) (Orderbook, error) {
resp := struct {
Data struct {
Asks [][]string `json:"asks"`
Bids [][]string `json:"bids"`
Time time.Time `json:"timestamp"`
} `json:"data"`
}{}
params := url.Values{}
params.Set("symbol", symbol)
params.Set("depth", strconv.FormatInt(size, 10))
path := common.EncodeURLValues(coinbeneAPIVersion+coinbeneGetOrderBook, params)
err := c.SendHTTPRequest(ctx, exchange.RestSpot, path, spotOrderbook, &resp)
if err != nil {
return Orderbook{}, err
}
processOB := func(ob [][]string) ([]OrderbookItem, error) {
var o []OrderbookItem
for x := range ob {
var price, amount float64
amount, err = strconv.ParseFloat(ob[x][1], 64)
if err != nil {
return nil, err
}
price, err = strconv.ParseFloat(ob[x][0], 64)
if err != nil {
return nil, err
}
o = append(o, OrderbookItem{
Price: price,
Amount: amount,
})
}
return o, nil
}
var s Orderbook
s.Bids, err = processOB(resp.Data.Bids)
if err != nil {
return s, err
}
s.Asks, err = processOB(resp.Data.Asks)
if err != nil {
return s, err
}
s.Time = resp.Data.Time
return s, nil
}
// GetTicker gets and stores ticker data for a currency pair
func (c *Coinbene) GetTicker(ctx context.Context, symbol string) (TickerData, error) {
resp := struct {
TickerData TickerData `json:"data"`
}{}
params := url.Values{}
params.Set("symbol", symbol)
path := common.EncodeURLValues(coinbeneAPIVersion+coinbeneGetTicker, params)
return resp.TickerData, c.SendHTTPRequest(ctx, exchange.RestSpot, path, spotSpecificTicker, &resp)
}
// GetTickers gets and all spot tickers supported by the exchange
func (c *Coinbene) GetTickers(ctx context.Context) ([]TickerData, error) {
resp := struct {
TickerData []TickerData `json:"data"`
}{}
path := coinbeneAPIVersion + coinbeneGetTickersSpot
return resp.TickerData, c.SendHTTPRequest(ctx, exchange.RestSpot, path, spotTickerList, &resp)
}
// GetTrades gets recent trades from the exchange
func (c *Coinbene) GetTrades(ctx context.Context, symbol string, limit int64) (Trades, error) {
resp := struct {
Data [][]string `json:"data"`
}{}
params := url.Values{}
params.Set("symbol", symbol)
params.Set("limit", strconv.FormatInt(limit, 10))
path := common.EncodeURLValues(coinbeneAPIVersion+coinbeneGetTrades, params)
err := c.SendHTTPRequest(ctx, exchange.RestSpot, path, spotMarketTrades, &resp)
if err != nil {
return nil, err
}
var trades Trades
for x := range resp.Data {
tm, err := time.Parse(time.RFC3339, resp.Data[x][4])
if err != nil {
return nil, err
}
price, err := strconv.ParseFloat(resp.Data[x][1], 64)
if err != nil {
return nil, err
}
volume, err := strconv.ParseFloat(resp.Data[x][2], 64)
if err != nil {
return nil, err
}
trades = append(trades, TradeItem{
CurrencyPair: resp.Data[x][0],
Price: price,
Volume: volume,
Direction: resp.Data[x][3],
TradeTime: tm,
})
}
return trades, nil
}
// GetAccountBalances gets user balanace info
func (c *Coinbene) GetAccountBalances(ctx context.Context) ([]UserBalanceData, error) {
resp := struct {
Data []UserBalanceData `json:"data"`
}{}
path := coinbeneAPIVersion + coinbeneGetUserBalance
err := c.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet,
path,
coinbeneGetUserBalance,
false,
nil,
&resp,
spotAccountInfo)
if err != nil {
return nil, err
}
return resp.Data, nil
}
// GetAccountAssetBalance gets user balanace info
func (c *Coinbene) GetAccountAssetBalance(ctx context.Context, symbol string) (UserBalanceData, error) {
v := url.Values{}
v.Set("asset", symbol)
resp := struct {
Data UserBalanceData `json:"data"`
}{}
path := coinbeneAPIVersion + coinbeneAccountBalanceOne
err := c.SendAuthHTTPRequest(ctx,
exchange.RestSpot,
http.MethodGet,
path,
coinbeneAccountBalanceOne,
false,
v,
&resp,
spotAccountAssetInfo)
if err != nil {
return UserBalanceData{}, err
}
return resp.Data, nil
}
// PlaceSpotOrder creates an order
func (c *Coinbene) PlaceSpotOrder(ctx context.Context, price, quantity float64, symbol, direction,
orderType, clientID string, notional int) (OrderPlacementResponse, error) {
var resp OrderPlacementResponse
params := url.Values{}
switch direction {
case order.Buy.Lower():
params.Set("direction", buyDirection)
case order.Sell.Lower():
params.Set("direction", sellDirection)
default:
return resp,
fmt.Errorf("invalid direction '%v', must be either 'buy' or 'sell'",
direction)
}
switch orderType {
case order.Limit.Lower():
params.Set("orderType", limitOrder)
case order.Market.Lower():
params.Set("orderType", marketOrder)
case order.PostOnly.Lower():
params.Set("orderType", postOnlyOrder)
case order.FillOrKill.Lower():
params.Set("orderType", fillOrKillOrder)
case order.IOS.Lower():
params.Set("orderType", iosOrder)
default:
return resp,
errors.New("invalid order type, must be either 'limit', 'market', 'postOnly', 'fillOrKill', 'ios'")
}
params.Set("symbol", symbol)
params.Set("price", strconv.FormatFloat(price, 'f', -1, 64))
params.Set("quantity", strconv.FormatFloat(quantity, 'f', -1, 64))
if clientID != "" {
params.Set("clientId", clientID)
}
if notional != 0 {
params.Set("notional", strconv.Itoa(notional))
}
path := coinbeneAPIVersion + coinbenePlaceOrder
err := c.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodPost,
path,
coinbenePlaceOrder,
false,
params,
&resp,
spotPlaceOrder)
if err != nil {
return resp, err
}
return resp, nil
}
// PlaceSpotOrders sets a batchful order request
func (c *Coinbene) PlaceSpotOrders(ctx context.Context, orders []PlaceOrderRequest) ([]OrderPlacementResponse, error) {
if len(orders) == 0 {
return nil, errors.New("orders is nil")
}
type ord struct {
Symbol string `json:"symbol"`
Direction string `json:"direction"`
Price string `json:"price"`
Quantity string `json:"quantity"`
OrderType string `json:"orderType"`
Notional string `json:"notional,omitempty"`
ClientID string `json:"clientId,omitempty"`
}
var reqOrders []ord
for x := range orders {
o := ord{
Symbol: orders[x].Symbol,
Price: strconv.FormatFloat(orders[x].Price, 'f', -1, 64),
Quantity: strconv.FormatFloat(orders[x].Quantity, 'f', -1, 64),
}
switch orders[x].Direction {
case order.Buy.Lower():
o.Direction = buyDirection
case order.Sell.Lower():
o.Direction = sellDirection
default:
return nil,
fmt.Errorf("invalid direction '%v', must be either 'buy' or 'sell'",
orders[x].Direction)
}
switch orders[x].OrderType {
case order.Limit.Lower():
o.OrderType = limitOrder
case order.Market.Lower():
o.OrderType = marketOrder
default:
return nil,
errors.New("invalid order type, must be either 'limit' or 'market'")
}
if orders[x].ClientID != "" {
o.ClientID = orders[x].ClientID
}
if orders[x].Notional != 0 {
o.Notional = strconv.Itoa(orders[x].Notional)
}
reqOrders = append(reqOrders, o)
}
resp := struct {
Data []OrderPlacementResponse `json:"data"`
}{}
path := coinbeneAPIVersion + coinbeneBatchPlaceOrder
err := c.SendAuthHTTPRequest(ctx,
exchange.RestSpot,
http.MethodPost,
path,
coinbeneBatchPlaceOrder,
false,
reqOrders,
&resp,
spotBatchOrder)
if err != nil {
return nil, err
}
return resp.Data, nil
}
// FetchOpenSpotOrders finds open orders
func (c *Coinbene) FetchOpenSpotOrders(ctx context.Context, symbol string) (OrdersInfo, error) {
params := url.Values{}
params.Set("symbol", symbol)
path := coinbeneAPIVersion + coinbeneOpenOrders
var orders OrdersInfo
for i := int64(1); ; i++ {
temp := struct {
Data OrdersInfo `json:"data"`
}{}
params.Set("pageNum", strconv.FormatInt(i, 10))
err := c.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet,
path,
coinbeneOpenOrders,
false,
params,
&temp,
spotQueryOpenOrders)
if err != nil {
return nil, err
}
for j := range temp.Data {
orders = append(orders, temp.Data[j])
}
if len(temp.Data) != 20 {
break
}
}
return orders, nil
}
// FetchClosedOrders finds open orders
func (c *Coinbene) FetchClosedOrders(ctx context.Context, symbol, latestID string) (OrdersInfo, error) {
params := url.Values{}
params.Set("symbol", symbol)
params.Set("latestOrderId", latestID)
path := coinbeneAPIVersion + coinbeneClosedOrders
var orders OrdersInfo
for i := int64(1); ; i++ {
temp := struct {
Data OrdersInfo `json:"data"`
}{}
params.Set("pageNum", strconv.FormatInt(i, 10))
err := c.SendAuthHTTPRequest(ctx,
exchange.RestSpot,
http.MethodGet,
path,
coinbeneClosedOrders,
false,
params,
&temp,
spotQueryClosedOrders)
if err != nil {
return nil, err
}
for j := range temp.Data {
orders = append(orders, temp.Data[j])
}
if len(temp.Data) != 20 {
break
}
}
return orders, nil
}
// FetchSpotOrderInfo gets order info
func (c *Coinbene) FetchSpotOrderInfo(ctx context.Context, orderID string) (OrderInfo, error) {
resp := struct {
Data OrderInfo `json:"data"`
}{}
params := url.Values{}
params.Set("orderId", orderID)
path := coinbeneAPIVersion + coinbeneOrderInfo
err := c.SendAuthHTTPRequest(ctx,
exchange.RestSpot,
http.MethodGet,
path,
coinbeneOrderInfo,
false,
params,
&resp,
spotQuerySpecficOrder)
if err != nil {
return resp.Data, err
}
if resp.Data.OrderID != orderID {
return resp.Data, fmt.Errorf("%s orderID doesn't match the returned orderID %s",
orderID, resp.Data.OrderID)
}
return resp.Data, nil
}
// GetSpotOrderFills returns a list of fills related to an order ID
func (c *Coinbene) GetSpotOrderFills(ctx context.Context, orderID string) ([]OrderFills, error) {
resp := struct {
Data []OrderFills `json:"data"`
}{}
params := url.Values{}
params.Set("orderId", orderID)
path := coinbeneAPIVersion + coinbeneTradeFills
err := c.SendAuthHTTPRequest(ctx,
exchange.RestSpot,
http.MethodGet,
path,
coinbeneTradeFills,
false,
params,
&resp,
spotQueryTradeFills)
if err != nil {
return nil, err
}
return resp.Data, nil
}
// CancelSpotOrder removes a given order
func (c *Coinbene) CancelSpotOrder(ctx context.Context, orderID string) (string, error) {
resp := struct {
Data string `json:"data"`
}{}
req := make(map[string]interface{})
req["orderId"] = orderID
path := coinbeneAPIVersion + coinbeneCancelOrder
err := c.SendAuthHTTPRequest(ctx,
exchange.RestSpot,
http.MethodPost,
path,
coinbeneCancelOrder,
false,
req,
&resp,
spotCancelOrder)
if err != nil {
return "", err
}
return resp.Data, nil
}
// CancelSpotOrders cancels a batch of orders
func (c *Coinbene) CancelSpotOrders(ctx context.Context, orderIDs []string) ([]OrderCancellationResponse, error) {
req := make(map[string]interface{})
req["orderIds"] = orderIDs
type resp struct {
Data []OrderCancellationResponse `json:"data"`
}
var r resp
path := coinbeneAPIVersion + coinbeneBatchCancel
err := c.SendAuthHTTPRequest(ctx,
exchange.RestSpot,
http.MethodPost,
path,
coinbeneBatchCancel,
false,
req,
&r,
spotCancelOrdersBatch)
if err != nil {
return nil, err
}
return r.Data, nil
}
// GetSwapTickers returns a map of swap tickers
func (c *Coinbene) GetSwapTickers(ctx context.Context) (SwapTickers, error) {
type resp struct {
Data SwapTickers `json:"data"`
}
var r resp
path := coinbeneAPIVersion + coinbeneGetTickers
err := c.SendHTTPRequest(ctx, exchange.RestSwap, path, contractTickers, &r)
if err != nil {
return nil, err
}
return r.Data, nil
}
// GetSwapTicker returns a specific swap ticker
func (c *Coinbene) GetSwapTicker(ctx context.Context, symbol string) (SwapTicker, error) {
tickers, err := c.GetSwapTickers(ctx)
if err != nil {
return SwapTicker{}, err
}
t, ok := tickers[strings.ToUpper(symbol)]
if !ok {
return SwapTicker{},
fmt.Errorf("symbol %s not found in tickers map", symbol)
}
return t, nil
}
// GetSwapInstruments returns a list of tradable instruments
func (c *Coinbene) GetSwapInstruments(ctx context.Context) ([]Instrument, error) {
resp := struct {
Data []Instrument `json:"data"`
}{}
return resp.Data,
c.SendHTTPRequest(ctx,
exchange.RestSwap,
coinbeneAPIVersion+coinbeneGetInstruments,
contractInstruments,
&resp)
}
// GetSwapOrderbook returns an orderbook for the specified currency
func (c *Coinbene) GetSwapOrderbook(ctx context.Context, symbol string, size int64) (Orderbook, error) {
var s Orderbook
if symbol == "" {
return s, fmt.Errorf("a symbol must be specified")
}
v := url.Values{}
v.Set("symbol", symbol)
if size != 0 {
v.Set("size", strconv.FormatInt(size, 10))
}
type resp struct {
Data struct {
Asks [][]string `json:"asks"`
Bids [][]string `json:"bids"`
Time time.Time `json:"timestamp"`
Symbol string `json:"symbol"`
} `json:"data"`
}
var r resp
path := common.EncodeURLValues(coinbeneAPIVersion+coinbeneGetOrderBook, v)
err := c.SendHTTPRequest(ctx, exchange.RestSwap, path, contractOrderbook, &r)
if err != nil {
return s, err
}
processOB := func(ob [][]string) ([]OrderbookItem, error) {
var o []OrderbookItem
for x := range ob {
var price, amount float64
var count int64
count, err = strconv.ParseInt(ob[x][2], 10, 64)
if err != nil {
return nil, err
}
price, err = strconv.ParseFloat(ob[x][0], 64)
if err != nil {
return nil, err
}
amount, err = strconv.ParseFloat(ob[x][1], 64)
if err != nil {
return nil, err
}
o = append(o, OrderbookItem{Price: price,
Amount: amount,
Count: count,
})
}
return o, nil
}
s.Bids, err = processOB(r.Data.Bids)
if err != nil {
return s, err
}
s.Asks, err = processOB(r.Data.Asks)
if err != nil {
return s, err
}
s.Time = r.Data.Time
s.Symbol = r.Data.Symbol
return s, nil
}
// GetKlines data returns the kline data for a specific symbol
func (c *Coinbene) GetKlines(ctx context.Context, pair string, start, end time.Time, period string) (resp CandleResponse, err error) {
v := url.Values{}
v.Add("symbol", pair)
if !start.IsZero() {
v.Add("start", strconv.FormatInt(start.Unix(), 10))
}
if !end.IsZero() {
v.Add("end", strconv.FormatInt(end.Unix(), 10))
}
v.Add("period", period)
path := common.EncodeURLValues(coinbeneAPIVersion+coinbeneSpotKlines, v)
if err = c.SendHTTPRequest(ctx, exchange.RestSpot, path, contractKline, &resp); err != nil {
return
}
if resp.Code != 200 {
return resp, errors.New(resp.Message)
}
return
}
// GetSwapKlines data returns the kline data for a specific symbol
func (c *Coinbene) GetSwapKlines(ctx context.Context, symbol string, start, end time.Time, resolution string) (resp CandleResponse, err error) {
v := url.Values{}
v.Set("symbol", symbol)
if !start.IsZero() {
v.Add("startTime", strconv.FormatInt(start.Unix(), 10))
}
if !end.IsZero() {
v.Add("endTime", strconv.FormatInt(end.Unix(), 10))
}
v.Set("resolution", resolution)
path := common.EncodeURLValues(coinbeneAPIVersion+coinbeneGetKlines, v)
if err = c.SendHTTPRequest(ctx, exchange.RestSwap, path, contractKline, &resp); err != nil {
return
}
return
}
// GetSwapTrades returns a list of trades for a swap symbol
func (c *Coinbene) GetSwapTrades(ctx context.Context, symbol string, limit int) (SwapTrades, error) {
v := url.Values{}
v.Set("symbol", symbol)
if limit != 0 {
v.Set("limit", strconv.Itoa(limit))
}
type resp struct {
Data [][]string `json:"data"`
}
var r resp
path := common.EncodeURLValues(coinbeneAPIVersion+coinbeneGetTrades, v)
if err := c.SendHTTPRequest(ctx, exchange.RestSwap, path, contractTrades, &r); err != nil {
return nil, err
}
var s SwapTrades
for x := range r.Data {
tm, err := time.Parse(time.RFC3339, r.Data[x][3])
if err != nil {
return nil, err
}
price, err := strconv.ParseFloat(r.Data[x][0], 64)
if err != nil {
return nil, err
}
orderSide := order.Buy
if r.Data[x][1] == "s" {
orderSide = order.Sell
}
volume, err := strconv.ParseFloat(r.Data[x][2], 64)
if err != nil {
return nil, err
}
s = append(s, SwapTrade{
Price: price,
Side: orderSide,
Volume: volume,
Time: tm,
})
}
return s, nil
}
// GetSwapAccountInfo returns a users swap account balance info
func (c *Coinbene) GetSwapAccountInfo(ctx context.Context) (SwapAccountInfo, error) {
type resp struct {
Data SwapAccountInfo `json:"data"`
}
var r resp
path := coinbeneAPIVersion + coinbeneAccountInfo
err := c.SendAuthHTTPRequest(ctx, exchange.RestSwap, http.MethodGet,
path,
coinbeneAccountInfo,
true,
nil,
&r,
contractAccountInfo)
if err != nil {
return SwapAccountInfo{}, err
}
return r.Data, nil
}
// GetSwapPositions returns a list of open swap positions
func (c *Coinbene) GetSwapPositions(ctx context.Context, symbol string) (SwapPositions, error) {
v := url.Values{}
v.Set("symbol", symbol)
type resp struct {
Data SwapPositions `json:"data"`
}
var r resp
path := coinbeneAPIVersion + coinbeneListSwapPositions
err := c.SendAuthHTTPRequest(ctx, exchange.RestSwap, http.MethodGet,
path,
coinbeneListSwapPositions,
true,
v,
&r,
contractPositionInfo)
if err != nil {
return nil, err
}
return r.Data, nil
}
// PlaceSwapOrder places a swap order
func (c *Coinbene) PlaceSwapOrder(ctx context.Context, symbol, direction, orderType, marginMode,
clientID string, price, quantity float64, leverage int) (SwapPlaceOrderResponse, error) {
v := url.Values{}
v.Set("symbol", symbol)
switch direction {
case order.Buy.Lower():
v.Set("direction", openLong)
case order.Sell.Lower():
v.Set("direction", openShort)
default:
return SwapPlaceOrderResponse{},
fmt.Errorf("invalid direction '%v', must be either 'buy' or 'sell'",
direction)
}
switch orderType {
case order.Limit.Lower():
v.Set("orderType", limitOrder)
case order.Market.Lower():
v.Set("orderType", marketOrder)
default:
return SwapPlaceOrderResponse{},
errors.New("invalid order type, must be either 'limit' or 'market'")
}
v.Set("leverage", strconv.Itoa(leverage))
v.Set("orderPrice", strconv.FormatFloat(price, 'f', -1, 64))
v.Set("quantity", strconv.FormatFloat(quantity, 'f', -1, 64))
if marginMode != "" {
v.Set("marginMode", marginMode)
}
if clientID != "" {
v.Set("clientId", clientID)
}
type resp struct {
Data SwapPlaceOrderResponse `json:"data"`
}
var r resp
path := coinbeneAPIVersion + coinbenePlaceOrder
err := c.SendAuthHTTPRequest(ctx,
exchange.RestSwap,
http.MethodPost,
path,
coinbenePlaceOrder,
true,
v,
&r,
contractPlaceOrder)
if err != nil {
return SwapPlaceOrderResponse{}, err
}
return r.Data, nil
}
// CancelSwapOrder cancels a swap order
func (c *Coinbene) CancelSwapOrder(ctx context.Context, orderID string) (string, error) {
params := make(map[string]interface{})
params["orderId"] = orderID
type resp struct {
Data string `json:"data"`
}
var r resp
path := coinbeneAPIVersion + coinbeneCancelOrder
err := c.SendAuthHTTPRequest(ctx, exchange.RestSwap, http.MethodPost,
path,
coinbeneCancelOrder,
true,
params,
&r,
contractCancelOrder)
if err != nil {
return "", err
}
return r.Data, nil
}
// GetSwapOpenOrders gets a list of open swap orders
func (c *Coinbene) GetSwapOpenOrders(ctx context.Context, symbol string, pageNum, pageSize int) (SwapOrders, error) {
v := url.Values{}
v.Set("symbol", symbol)
if pageNum != 0 {
v.Set("pageNum", strconv.Itoa(pageNum))
}
if pageSize != 0 {
v.Set("pageSize", strconv.Itoa(pageSize))
}
type resp struct {
Data SwapOrders `json:"data"`
}
var r resp
path := coinbeneAPIVersion + coinbeneOpenOrders
err := c.SendAuthHTTPRequest(ctx,
exchange.RestSwap,
http.MethodGet,
path,
coinbeneOpenOrders,
true,
v,
&r,
contractGetOpenOrders)
if err != nil {
return nil, err
}
return r.Data, nil
}
// GetSwapOpenOrdersByPage gets a list of open orders by page
func (c *Coinbene) GetSwapOpenOrdersByPage(ctx context.Context, symbol string, latestOrderID int64) (SwapOrders, error) {
v := url.Values{}
if symbol != "" {
v.Set("symbol", symbol)
}
if latestOrderID != 0 {
v.Set("latestOrderId", strconv.FormatInt(latestOrderID, 10))
}
type resp struct {
Data SwapOrders `json:"data"`
}
var r resp
path := coinbeneAPIVersion + coinbeneOpenOrdersByPage
err := c.SendAuthHTTPRequest(ctx,
exchange.RestSwap,
http.MethodGet,
path,
coinbeneOpenOrdersByPage,
true,
v,
&r,
contractOpenOrdersByPage)
if err != nil {
return nil, err
}
return r.Data, nil
}
// GetSwapOrderInfo gets order info for a specific order
func (c *Coinbene) GetSwapOrderInfo(ctx context.Context, orderID string) (SwapOrder, error) {
v := url.Values{}
v.Set("orderId", orderID)
type resp struct {
Data SwapOrder `json:"data"`
}
var r resp
path := coinbeneAPIVersion + coinbeneOrderInfo
err := c.SendAuthHTTPRequest(ctx,
exchange.RestSwap,
http.MethodGet,
path,
coinbeneOrderInfo,
true,
v,
&r,
contractGetOrderInfo)
if err != nil {
return SwapOrder{}, err
}
return r.Data, nil
}
// GetSwapOrderHistory returns the swap order history for a given symbol
func (c *Coinbene) GetSwapOrderHistory(ctx context.Context, beginTime, endTime, symbol string, pageNum,
pageSize int, direction, orderType string) (SwapOrders, error) {
v := url.Values{}
if beginTime != "" {
v.Set("beginTime", beginTime)
}
if endTime != "" {
v.Set("endTime", endTime)
}
v.Set("symbol", symbol)
if pageNum != 0 {
v.Set("pageNum", strconv.Itoa(pageNum))
}
if pageSize != 0 {
v.Set("pageSize", strconv.Itoa(pageSize))
}
if direction != "" {
v.Set("direction", direction)
}
if orderType != "" {
v.Set("orderType", orderType)
}
type resp struct {
Data SwapOrders `json:"data"`
}
var r resp
path := coinbeneAPIVersion + coinbeneClosedOrders
err := c.SendAuthHTTPRequest(ctx, exchange.RestSwap, http.MethodGet,
path,
coinbeneClosedOrders,
true,
v,
&r,
contractGetClosedOrders)
if err != nil {
return nil, err
}
return r.Data, nil
}
// GetSwapOrderHistoryByOrderID returns a list of historic orders based on user params
func (c *Coinbene) GetSwapOrderHistoryByOrderID(ctx context.Context, beginTime, endTime, symbol, status string,
latestOrderID int64) (SwapOrders, error) {
v := url.Values{}
if beginTime != "" {
v.Set("beginTime", beginTime)
}
if endTime != "" {
v.Set("endTime", endTime)
}
if symbol != "" {
v.Set("symbol", symbol)
}
if status != "" {
v.Set("status", status)
}
if latestOrderID != 0 {
v.Set("latestOrderId", strconv.FormatInt(latestOrderID, 10))
}
type resp struct {
Data SwapOrders `json:"data"`
}
var r resp
path := coinbeneAPIVersion + coinbeneClosedOrdersByPage
err := c.SendAuthHTTPRequest(ctx,
exchange.RestSwap,
http.MethodGet,
path,
coinbeneClosedOrdersByPage,
true,
v,
&r,
contractGetClosedOrdersbyPage)
if err != nil {
return nil, err
}
return r.Data, nil
}
// CancelSwapOrders cancels multiple swap order IDs
func (c *Coinbene) CancelSwapOrders(ctx context.Context, orderIDs []string) ([]OrderCancellationResponse, error) {
if len(orderIDs) > 10 {
return nil, errors.New("only 10 orderIDs are allowed at a time")
}
req := make(map[string]interface{})
req["orderIds"] = orderIDs
type resp struct {
Data []OrderCancellationResponse `json:"data"`
}
var r resp
path := coinbeneAPIVersion + coinbeneBatchCancel
err := c.SendAuthHTTPRequest(ctx,
exchange.RestSwap,
http.MethodPost,
path,
coinbeneBatchCancel,
true,
req,
&r,
contractCancelMultipleOrders)
if err != nil {
return nil, err
}
return r.Data, nil
}
// GetSwapOrderFills returns a list of swap order fills
func (c *Coinbene) GetSwapOrderFills(ctx context.Context, symbol, orderID string, lastTradeID int64) (SwapOrderFills, error) {
v := url.Values{}
if symbol != "" {
v.Set("symbol", symbol)
}
if orderID != "" {
v.Set("orderId", orderID)
}
if lastTradeID != 0 {
v.Set("lastTradedId", strconv.FormatInt(lastTradeID, 10))
}
type resp struct {
Data SwapOrderFills `json:"data"`
}
var r resp
path := coinbeneAPIVersion + coinbeneOrderFills
err := c.SendAuthHTTPRequest(ctx,
exchange.RestSwap,
http.MethodGet,
path,
coinbeneOrderFills,
true,
v,
&r,
contractGetOrderFills)
if err != nil {
return nil, err
}
return r.Data, nil
}
// GetSwapFundingRates returns a list of funding rates
func (c *Coinbene) GetSwapFundingRates(ctx context.Context, pageNum, pageSize int) ([]SwapFundingRate, error) {
v := url.Values{}
if pageNum != 0 {
v.Set("pageNum", strconv.Itoa(pageNum))
}
if pageSize != 0 {
v.Set("pageSize", strconv.Itoa(pageSize))
}
type resp struct {
Data []SwapFundingRate `json:"data"`
}
var r resp
path := coinbeneAPIVersion + coinbenePositionFeeRate
err := c.SendAuthHTTPRequest(ctx,
exchange.RestSwap,
http.MethodGet,
path,
coinbenePositionFeeRate,
true,
v,
&r,
contractGetFundingRates)
if err != nil {
return nil, err
}
return r.Data, nil
}
// SendHTTPRequest sends an unauthenticated HTTP request
func (c *Coinbene) SendHTTPRequest(ctx context.Context, ep exchange.URL, path string, f request.EndpointLimit, result interface{}) error {
endpoint, err := c.API.Endpoints.GetURL(ep)
if err != nil {
return err
}
var resp json.RawMessage
item := &request.Item{
Method: http.MethodGet,
Path: endpoint + path,
Result: &resp,
Verbose: c.Verbose,
HTTPDebugging: c.HTTPDebugging,
HTTPRecording: c.HTTPRecording,
}
if err := c.SendPayload(ctx, f, func() (*request.Item, error) {
return item, nil
}); err != nil {
return err
}
errCap := struct {
Code int `json:"code"`
Message string `json:"message"`
}{}
if err := json.Unmarshal(resp, &errCap); err == nil {
if errCap.Code != 200 && errCap.Message != "" {
return errors.New(errCap.Message)
}
}
return json.Unmarshal(resp, result)
}
// SendAuthHTTPRequest sends an authenticated HTTP request
func (c *Coinbene) SendAuthHTTPRequest(ctx context.Context, ep exchange.URL, method, path, epPath string, isSwap bool,
params, result interface{}, f request.EndpointLimit) error {
if !c.AllowAuthenticatedRequest() {
return fmt.Errorf("%s %w", c.Name, exchange.ErrAuthenticatedRequestWithoutCredentialsSet)
}
endpoint, err := c.API.Endpoints.GetURL(ep)
if err != nil {
return err
}
authPath := coinbeneAuthPath
if isSwap {
authPath = coinbeneSwapAuthPath
}
var resp json.RawMessage
newRequest := func() (*request.Item, error) {
timestamp := time.Now().UTC().Format("2006-01-02T15:04:05.999Z")
var finalBody io.Reader
var preSign string
var fullPath = path
switch {
case params != nil && method == http.MethodGet:
p, ok := params.(url.Values)
if !ok {
return nil, errors.New("params is not of type url.Values")
}
preSign = timestamp + method + authPath + epPath + "?" + p.Encode()
fullPath = common.EncodeURLValues(path, p)
case params != nil:
var i interface{}
switch p := params.(type) {
case url.Values:
m := make(map[string]string)
for k, v := range p {
m[k] = strings.Join(v, "")
}
i = m
default:
i = p
}
tempBody, err2 := json.Marshal(i)
if err2 != nil {
return nil, err2
}
finalBody = bytes.NewBufferString(string(tempBody))
preSign = timestamp + method + authPath + epPath + string(tempBody)
default:
preSign = timestamp + method + authPath + epPath
}
tempSign, err := crypto.GetHMAC(crypto.HashSHA256,
[]byte(preSign),
[]byte(c.API.Credentials.Secret))
if err != nil {
return nil, err
}
headers := make(map[string]string)
headers["Content-Type"] = "application/json"
headers["ACCESS-KEY"] = c.API.Credentials.Key
headers["ACCESS-SIGN"] = crypto.HexEncodeToString(tempSign)
headers["ACCESS-TIMESTAMP"] = timestamp
return &request.Item{
Method: method,
Path: endpoint + fullPath,
Headers: headers,
Body: finalBody,
Result: &resp,
AuthRequest: true,
Verbose: c.Verbose,
HTTPDebugging: c.HTTPDebugging,
HTTPRecording: c.HTTPRecording,
}, nil
}
if err := c.SendPayload(ctx, f, newRequest); err != nil {
return err
}
errCap := struct {
Code int `json:"code"`
Message string `json:"message"`
}{}
if err := json.Unmarshal(resp, &errCap); err == nil &&
errCap.Code != 200 &&
errCap.Message != "" {
return errors.New(errCap.Message)
}
return json.Unmarshal(resp, result)
}