Files
gocryptotrader/exchanges/coinbene/coinbene.go
Scott 5ea5245afb Improvement: Subsystem separation (#664)
* Initial codes for a trade tracker

* Moving everything in a broken fashion

* Removes tradetracker. Removes some errors for subsystems

* Cleans up some subsystems, renames stuttering types. Removes some global Bot usage

* More basic subsystem renaming and file moving

* Removes engine dependency from events,ntpserver,ordermanager,comms manager

* Exports eventManager, fixes rpcserver. puts rpcserver back for now

* Removes redundant error message, further removes engine dependencies

* experimental end of day interface usage

* adds ability to build the application

* Withdraw and event manager handling

* cleans up apiserver and communications manager

* Cleans up some start/setup processes. Though should separate

* More consistency with Setup Start Stop IsRunning funcs

* Final consistency pass before testing phase

* Fixes engine tests. Fixes stop nil issue

* api server tests

* Communications manager testing

* Connection manager tests and nilsubsystem error

* End of day currencypairsyncer tests

* Adds databaseconnection/databaseconnection_test.go

* Adds withdrawal manager tests

* Deposit address testing. Moved orderbook sync first as its more important

* Adds test for event manager

* More full eventmanager testing

* Adds testfile. Enables skipped test.

* ntp manager tests

* Adds ordermanager tests, Extracts a whole new subsystem from engine and fanangles import cycles

* Adds websocket routine manager tests

* Basic portfolio manager testing

* Fixes issue with currency pair sync startup

* Fixes issue with event manager startup

* Starts the order manager before backtester starts

* Fixes fee tests. Expands testing. Doesnt fix races

* Fixes most test races

* Resolves data races

* Fixes subsystem test issues

* currency pair syncer coverage tests

* Refactors portfolio. Fixes tests. Withdraw validation

Portfolio didn't need to exist with a portfolio manager. Now the porfolio manager
is in charge how the portfolio is handled and all portfolio functions are attached
to the base instead of just exported at the package level

Withdrawal validation occurred at the exchange level when it can just be run at the
withdrawal manager level. All withdrawal requests go through that endpoint

* lint -fix

* golang lint fixes

* lints and comments everything

* Updates GCT logo, adds documentation for some subsystems

* More documentation and more logo updates

* Fixes backtesting and apiserver errors encountered

* Fixes errors and typos from reviewing

* More minor fixes

* Changes %h verb to %w

* reverbs to %s

* Humbly begins reverting to more flat engine package

The main reasoning for this is that the subsystem split doesn't make sense
in a golang environment. The subsystems are only meant to be used with engine
and so by placing them in a non-engine area, it does not work and is
inconsistent with the rest of the application's package layout.

This will begin salvaging the changes made by reverting to a flat
engine package, but maintaining the consistent designs introduced.
Further, I will look to remove any TestMains and decrease the scope
of testing to be more local and decrease the issues that have been
caused from our style of testing.

* Manages to re-flatten things. Everything is within its own file

* mini fixes

* Fixes tests and data races and lints

* Updates docs tool for engine to create filename readmes

* os -> ioutil

* remove err

* Appveyor version increase test

* Removes tCleanup as its unsupported on appveyor

* Adds stuff that I thought was in previous merge master commit

* Removes cancel from test

* Fixes really fun test-exclusive data race

* minor nit fixes

* niterinos

* docs gen

* rm;rf test

* Remove typoline. expands startstop helper. Splits apiserver

* Removes accidental folder

* Uses update instead of replace for order upsert

* addresses nits. Renames files. Regenerates documentation.

* lint and removal of comments

* Add new test for default scenario

* Fixes typo

* regen docs
2021-05-31 10:17:12 +10:00

1204 lines
30 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() ([]PairData, error) {
resp := struct {
Data []PairData `json:"data"`
}{}
path := coinbeneAPIVersion + coinbeneGetAllPairs
return resp.Data, c.SendHTTPRequest(exchange.RestSpot, path, spotPairs, &resp)
}
// GetPairInfo gets info about a single pair
func (c *Coinbene) GetPairInfo(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(exchange.RestSpot, path, spotPairInfo, &resp)
}
// GetOrderbook gets and stores orderbook data for given pair
func (c *Coinbene) GetOrderbook(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(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(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(exchange.RestSpot, path, spotSpecificTicker, &resp)
}
// GetTickers gets and all spot tickers supported by the exchange
func (c *Coinbene) GetTickers() ([]TickerData, error) {
resp := struct {
TickerData []TickerData `json:"data"`
}{}
path := coinbeneAPIVersion + coinbeneGetTickersSpot
return resp.TickerData, c.SendHTTPRequest(exchange.RestSpot, path, spotTickerList, &resp)
}
// GetTrades gets recent trades from the exchange
func (c *Coinbene) GetTrades(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(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() ([]UserBalanceData, error) {
resp := struct {
Data []UserBalanceData `json:"data"`
}{}
path := coinbeneAPIVersion + coinbeneGetUserBalance
err := c.SendAuthHTTPRequest(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(symbol string) (UserBalanceData, error) {
v := url.Values{}
v.Set("asset", symbol)
resp := struct {
Data UserBalanceData `json:"data"`
}{}
path := coinbeneAPIVersion + coinbeneAccountBalanceOne
err := c.SendAuthHTTPRequest(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(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(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(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(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(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(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(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(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(orderID string) (OrderInfo, error) {
resp := struct {
Data OrderInfo `json:"data"`
}{}
params := url.Values{}
params.Set("orderId", orderID)
path := coinbeneAPIVersion + coinbeneOrderInfo
err := c.SendAuthHTTPRequest(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(orderID string) ([]OrderFills, error) {
resp := struct {
Data []OrderFills `json:"data"`
}{}
params := url.Values{}
params.Set("orderId", orderID)
path := coinbeneAPIVersion + coinbeneTradeFills
err := c.SendAuthHTTPRequest(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(orderID string) (string, error) {
resp := struct {
Data string `json:"data"`
}{}
req := make(map[string]interface{})
req["orderId"] = orderID
path := coinbeneAPIVersion + coinbeneCancelOrder
err := c.SendAuthHTTPRequest(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(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(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() (SwapTickers, error) {
type resp struct {
Data SwapTickers `json:"data"`
}
var r resp
path := coinbeneAPIVersion + coinbeneGetTickers
err := c.SendHTTPRequest(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(symbol string) (SwapTicker, error) {
tickers, err := c.GetSwapTickers()
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() ([]Instrument, error) {
resp := struct {
Data []Instrument `json:"data"`
}{}
return resp.Data, c.SendHTTPRequest(exchange.RestSwap,
coinbeneAPIVersion+coinbeneGetInstruments, contractInstruments, &resp)
}
// GetSwapOrderbook returns an orderbook for the specified currency
func (c *Coinbene) GetSwapOrderbook(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(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(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(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(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(exchange.RestSwap, path, contractKline, &resp); err != nil {
return
}
return
}
// GetSwapTrades returns a list of trades for a swap symbol
func (c *Coinbene) GetSwapTrades(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(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() (SwapAccountInfo, error) {
type resp struct {
Data SwapAccountInfo `json:"data"`
}
var r resp
path := coinbeneAPIVersion + coinbeneAccountInfo
err := c.SendAuthHTTPRequest(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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
errCap := struct {
Code int `json:"code"`
Message string `json:"message"`
}{}
if err := c.SendPayload(context.Background(), &request.Item{
Method: http.MethodGet,
Path: endpoint + path,
Result: &resp,
Verbose: c.Verbose,
HTTPDebugging: c.HTTPDebugging,
HTTPRecording: c.HTTPRecording,
Endpoint: f,
}); err != nil {
return err
}
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(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
}
now := time.Now()
timestamp := now.UTC().Format("2006-01-02T15:04:05.999Z")
var finalBody io.Reader
var preSign string
switch {
case params != nil && method == http.MethodGet:
p, ok := params.(url.Values)
if !ok {
return fmt.Errorf("params is not of type url.Values")
}
preSign = timestamp + method + authPath + epPath + "?" + p.Encode()
path = 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, err := json.Marshal(i)
if err != nil {
return err
}
finalBody = bytes.NewBufferString(string(tempBody))
preSign = timestamp + method + authPath + epPath + string(tempBody)
default:
preSign = timestamp + method + authPath + epPath
}
tempSign := crypto.GetHMAC(crypto.HashSHA256,
[]byte(preSign),
[]byte(c.API.Credentials.Secret))
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
var resp json.RawMessage
errCap := struct {
Code int `json:"code"`
Message string `json:"message"`
}{}
// Expiry of timestamp doesn't appear to be documented, so making a reasonable assumption
ctx, cancel := context.WithDeadline(context.Background(), now.Add(15*time.Second))
defer cancel()
if err := c.SendPayload(ctx, &request.Item{
Method: method,
Path: endpoint + path,
Headers: headers,
Body: finalBody,
Result: &resp,
AuthRequest: true,
Verbose: c.Verbose,
HTTPDebugging: c.HTTPDebugging,
HTTPRecording: c.HTTPRecording,
Endpoint: f,
}); err != nil {
return err
}
if err := json.Unmarshal(resp, &errCap); err == nil {
if errCap.Code != 200 && errCap.Message != "" {
return errors.New(errCap.Message)
}
}
return json.Unmarshal(resp, result)
}