Add Coinbene exchange support (#366)

Add Coinbene exchange support
This commit is contained in:
Adam
2019-11-06 16:07:11 +11:00
committed by Adrian Gallagher
parent 5ad68c38cb
commit 0501455a2e
17 changed files with 1936 additions and 16 deletions

View File

@@ -9,9 +9,9 @@ vadimzhukck | https://github.com/vadimzhukck
140am | https://github.com/140am
marcofranssen | https://github.com/marcofranssen
cranktakular | https://github.com/cranktakular
MadCozBadd | https://github.com/MadCozBadd
leilaes | https://github.com/leilaes
crackcomm | https://github.com/crackcomm
MadCozBadd | https://github.com/MadCozBadd
andreygrehov | https://github.com/andreygrehov
bretep | https://github.com/bretep
woshidama323 | https://github.com/woshidama323
@@ -30,8 +30,8 @@ frankzougc | https://github.com/frankzougc
starit | https://github.com/starit
Jimexist | https://github.com/Jimexist
lookfirst | https://github.com/lookfirst
| mattkanwisher | https://github.com/mattkanwisher
| mKurrels | https://github.com/mKurrels
| m1kola | https://github.com/m1kola
| cavapoo2 | https://github.com/cavapoo2
| zeldrinn | https://github.com/zeldrinn
mattkanwisher | https://github.com/mattkanwisher
mKurrels | https://github.com/mKurrels
m1kola | https://github.com/m1kola
cavapoo2 | https://github.com/cavapoo2
zeldrinn | https://github.com/zeldrinn

View File

@@ -130,19 +130,18 @@ Binaries will be published once the codebase reaches a stable condition.
|User|Github|Contribution Amount|
|--|--|--|
| thrasher- | https://github.com/thrasher- | 543 |
| shazbert | https://github.com/shazbert | 174 |
| gloriousCode | https://github.com/gloriousCode | 154 |
| thrasher- | https://github.com/thrasher- | 548 |
| shazbert | https://github.com/shazbert | 176 |
| gloriousCode | https://github.com/gloriousCode | 155 |
| xtda | https://github.com/xtda | 18 |
| ermalguni | https://github.com/ermalguni | 14 |
| xtda | https://github.com/xtda | 11 |
| vadimzhukck | https://github.com/vadimzhukck | 10 |
| 140am | https://github.com/140am | 8 |
| marcofranssen | https://github.com/marcofranssen | 8 |
| cranktakular | https://github.com/cranktakular | 5 |
| MadCozBadd | https://github.com/MadCozBadd | 3 |
| leilaes | https://github.com/leilaes | 3 |
| crackcomm | https://github.com/crackcomm | 3 |
| MadCozBadd | https://github.com/MadCozBadd | 2 |
| andreygrehov | https://github.com/andreygrehov | 2 |
| bretep | https://github.com/bretep | 2 |
| woshidama323 | https://github.com/woshidama323 | 2 |
@@ -165,4 +164,6 @@ Binaries will be published once the codebase reaches a stable condition.
| mKurrels | https://github.com/mKurrels | 1 |
| m1kola | https://github.com/m1kola | 1 |
| cavapoo2 | https://github.com/cavapoo2 | 1 |
| zeldrinn | https://github.com/zeldrinn | 1 |
| zeldrinn | https://github.com/zeldrinn | 1 |

View File

@@ -13,7 +13,7 @@ import (
const (
// Default number of enabled exchanges. Modify this whenever an exchange is
// added or removed
defaultEnabledExchanges = 27
defaultEnabledExchanges = 28
)
func TestGetCurrencyConfig(t *testing.T) {

View File

@@ -752,6 +752,53 @@
}
]
},
{
"name": "Coinbene",
"enabled": true,
"verbose": false,
"websocket": false,
"useSandbox": false,
"restPollingDelay": 10,
"httpTimeout": 15000000000,
"websocketResponseCheckTimeout": 30000000,
"websocketResponseMaxLimit": 7000000000,
"websocketOrderbookBufferLimit": 5,
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
"apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
"proxyAddress": "",
"websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API",
"clientId": "ClientID",
"availablePairs": "ABBC/BTC,ABT/ETH,ABT/USDT,ABYSS/ETH,ACDC/BTC,ACDC/USDT,ADI/ETH,ADK/BTC,ADN/BTC,AE/BTC,AE/USDT,AID/BTC,AIDOC/BTC,AION/BTC,AIPE/USDT,AIT/USDT,ALGO/USDT,ALI/ETH,ALX/ETH,APL/ETH,ATX/BTC,B2G/BTC,B91/USDT,BAAS/BTC,BAT/BTC,BCHABC/USDT,BCHSV/USDT,BEAUTY/ETH,BETHER/ETH,BEZ/BTC,BGC/USDT,BKG/BTC,BNT/BTC,BOA/USDT,BSTN/ETH,BTC/USDT,BTFM/USDT,BTNT/BTC,BTSC/BTC,BTT/USDT,BU/ETH,BVT/ETH,C3W/ETH,CAN/ETH,CCC/ETH,CCE/USDT,CC/USDT,CEDEX/ETH,CENT/BTC,CFT/USDT,CLO/BTC,CMT/ETH,CMT/USDT,CNN/BTC,CNN/ETH,CNN/USDT,CONI/USDT,COSM/BTC,COSM/ETH,COZP/BTC,CPC/BTC,CPMS/USDT,CREDO/ETH,CRN/BTC,CS/ETH,CS/USDT,CTXC/ETH,CUST/USDT,CVC/BTC,CXC/USDT,CXP/BTC,DCA/ETH,DCT/BTC,DENT/BTC,DGD/BTC,DOCK/ETH,DSCB/USDT,DTA/ETH,DUC/BTC,DVC/ETH,EBC/BTC,EBC/ETH,EBC/USDT,ECA/BTC,EDC/BTC,EDR/ETH,ELF/BTC,EMT/USDT,EOS/BTC,EOS/USDT,EQUAD/BTC,ETC/BTC,ETC/USDT,ETH/BTC,ETH/USDT,ETK/BTC,ETN/BTC,FAB/ETH,FACC/ETH,FCC/BTC,FDS/USDT,FND/ETH,FNKOS/ETH,FTN/BTC,FTN/USDT,FTT/BTC,FXT/ETH,GETX/ETH,GLDR/ETH,GMTK/ETH,GOM/USDT,GRAM/USDT,GRIN/BTC,GRN/BTC,GSTT/USDT,GUSD/USDT,GVT/BTC,HAPPY/BTC,HDAC/BTC,HMB/USDT,HNB/USDT,HPT/ETH,HUP/USDT,INCX/ETH,IOST/BTC,IOTE/USDT,ISR/BTC,ISR/ETH,IVY/ETH,JOB/BTC,KBC/BTC,KBC/USDT,KMD/BTC,KNT/ETH,KST/BTC,KUE/BTC,KUE/ETH,KUKY/BTC,LAMB/USDT,LATX/BTC,LBK/BTC,LINK/BTC,LOOM/BTC,LTC/BTC,LTC/USDT,LUC/ETH,LUX/BTC,LVTC/ETH,MDC/USDT,MGC/USDT,MIB/BTC,MINX/BTC,MINX/ETH,MOAC/USDT,MPL/BTC,MTC/BTC,MT/ETH,MTN/ETH,MT/USDT,MVL/ETH,MVPT/ETH,MWT/USDT,NANO/BTC,NBAI/ETH,NCASH/BTC,NEO/BTC,NEO/USDT,NOBS/BTC,NPXS/ETH,NPXS/USDT,NTY/ETH,ODC/USDT,OMG/BTC,OMX/ETH,OVC/ETH,OZX/ETH,PAL/ETH,PAT/ETH,PAX/USDT,PKX/BTC,PLAY/BTC,PMA/ETH,POLL/BTC,POLY/BTC,PPT/BTC,PSM/BTC,QKC/BTC,QTUM/BTC,QTUM/USDT,RBG/BTC,RBG/ETH,RBG/USDT,RBTC/BTC,RBZ/USDT,RCOIN/BTC,RCOIN/USDT,REP/BTC,REV/BTC,RIF/BTC,SALT/BTC,SCC/BTC,SCO/BTC,SEN/BTC,SENC/ETH,SHE/BTC,SHVR/BTC,SIM/BTC,SKB/BTC,SKM/ETH,SKYM/USDT,SLT/ETH,SMARTUP/ETH,SMARTUP/USDT,SMART/USDT,SORO/USDT,SRCOIN/BTC,SRCOIN/ETH,STORJ/BTC,STQ/BTC,SWET/BTC,SWTC/USDT,TCT/BTC,TEMCO/USDT,TEN/BTC,TEN/ETH,THM/ETH,TIB/BTC,TIMO/USDT,TMTG/BTC,TOC/ETH,TOSC/BTC,TRUE/ETH,TRX/BTC,TRX/USDT,TSL/BTC,TVB/USDT,UTNP/BTC,VBT/USDT,VEEN/BTC,VME/BTC,VME/ETH,VOLLAR/USDT,VSC/ETH,W12/BTC,W12/ETH,WBL/BTC,WFX/BTC,XEM/BTC,XLM/BTC,XMCT/ETH,XMCT/USDT,XMR/BTC,XNK/ETH,XRP/BTC,XRP/USDT,XSR/USDT,YTA/USDT,ZAT/ETH,ZDC/BTC,ZEC/BTC,ZGC/BTC,ZRX/BTC",
"enabledPairs": "BTC/USDT",
"baseCurrencies": "USD",
"assetTypes": "SPOT",
"supportsAutoPairUpdates": true,
"configCurrencyPairFormat": {
"uppercase": true,
"delimiter": "/"
},
"requestCurrencyPairFormat": {
"uppercase": true,
"delimiter": "/"
},
"bankAccounts": [
{
"bankName": "",
"bankAddress": "",
"accountName": "",
"accountNumber": "",
"swiftCode": "",
"iban": "",
"supportedCurrencies": ""
}
]
},
{
"name": "GateIO",
"enabled": true,

View File

@@ -18,6 +18,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/btcmarkets"
"github.com/thrasher-corp/gocryptotrader/exchanges/btse"
"github.com/thrasher-corp/gocryptotrader/exchanges/coinbasepro"
"github.com/thrasher-corp/gocryptotrader/exchanges/coinbene"
"github.com/thrasher-corp/gocryptotrader/exchanges/coinut"
"github.com/thrasher-corp/gocryptotrader/exchanges/exmo"
"github.com/thrasher-corp/gocryptotrader/exchanges/gateio"
@@ -151,6 +152,8 @@ func LoadExchange(name string, useWG bool, wg *sync.WaitGroup) error {
exch = new(btcmarkets.BTCMarkets)
case "btse":
exch = new(btse.BTSE)
case "coinbene":
exch = new(coinbene.Coinbene)
case "coinut":
exch = new(coinut.COINUT)
case "exmo":

View File

@@ -164,9 +164,7 @@ func (b *Bitfinex) WsConnect() error {
func (b *Bitfinex) WsDataHandler() {
b.Websocket.Wg.Add(1)
defer func() {
b.Websocket.Wg.Done()
}()
defer b.Websocket.Wg.Done()
for {
select {

View File

@@ -0,0 +1,141 @@
# GoCryptoTrader package Coinbene
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/page-logo.png?raw=true" width="350px" height="350px" hspace="70">
[![Build Status](https://travis-ci.org/thrasher-corp/gocryptotrader.svg?branch=master)](https://travis-ci.org/thrasher-corp/gocryptotrader)
[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/exchanges/coinbene)
[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader)
This coinbene package is part of the GoCryptoTrader codebase.
## This is still in active development
You can track ideas, planned features and what's in progresss on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader).
Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk)
## Coinbene Exchange
### Current Features
+ REST Support
+ Websocket Support
### How to enable
+ [Enable via configuration](https://github.com/thrasher-corp/gocryptotrader/tree/master/config#enable-exchange-via-config-example)
+ Individual package example below:
```go
// Exchanges will be abstracted out in further updates and examples will be
// supplied then
```
### How to do REST public/private calls
+ If enabled via "configuration".json file the exchange will be added to the
IBotExchange array in the ```go var bot Bot``` and you will only be able to use
the wrapper interface functions for accessing exchange data. View routines.go
for an example of integration usage with GoCryptoTrader. Rudimentary example
below:
main.go
```go
var c exchange.IBotExchange
for i := range bot.exchanges {
if bot.exchanges[i].GetName() == "Coinbene" {
c = bot.exchanges[i]
}
}
// Public calls - wrapper functions
// Fetches current ticker information
tick, err := c.GetTickerPrice()
if err != nil {
// Handle error
}
// Fetches current orderbook information
ob, err := c.GetOrderbookEx()
if err != nil {
// Handle error
}
// Private calls - wrapper functions - make sure your APIKEY and APISECRET are
// set and AuthenticatedAPISupport is set to true
// Fetches current account information
accountInfo, err := c.GetAccountInfo()
if err != nil {
// Handle error
}
```
+ If enabled via individually importing package, rudimentary example below:
```go
// Public calls
// Fetches current ticker information
ticker, err := c.GetTicker()
if err != nil {
// Handle error
}
// Fetches current orderbook information
ob, err := c.GetOrderBook()
if err != nil {
// Handle error
}
// Private calls - make sure your APIKEY and APISECRET are set and
// AuthenticatedAPISupport is set to true
// GetUserInfo returns account info
accountInfo, err := c.GetUserInfo(...)
if err != nil {
// Handle error
}
// Submits an order and the exchange and returns its tradeID
tradeID, err := c.Trade(...)
if err != nil {
// Handle error
}
```
### How to do Websocket public/private calls
```go
// Exchanges will be abstracted out in further updates and examples will be
// supplied then
```
### Please click GoDocs chevron above to view current GoDoc information for this package
## Contribution
Please feel free to submit any pull requests or suggest any desired features to be added.
When submitting a PR, please abide by our coding guidelines:
+ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)).
+ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines.
+ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md).
+ Pull requests need to be based on and opened against the `master` branch.
## Donations
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB***

View File

@@ -0,0 +1,395 @@
package coinbene
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/config"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
log "github.com/thrasher-corp/gocryptotrader/logger"
)
// Coinbene is the overarching type across this package
type Coinbene struct {
exchange.Base
WebsocketConn *wshandler.WebsocketConnection
}
const (
coinbeneAPIURL = "https://openapi-exchange.coinbene.com/api/exchange/"
coinbeneAuthPath = "/api/exchange/v2"
coinbeneAPIVersion = "v2"
buy = "buy"
sell = "sell"
// Public endpoints
coinbeneFetchTicker = "/market/ticker/one"
coinbeneFetchOrderBook = "/market/orderBook"
coinbeneGetTrades = "/market/trades"
coinbeneGetAllPairs = "/market/tradePair/list"
coinbenePairInfo = "/market/tradePair/one"
// Authenticated endpoints
coinbeneGetUserBalance = "/account/list"
coinbenePlaceOrder = "/order/place"
coinbeneOrderInfo = "/order/info"
coinbeneRemoveOrder = "/order/cancel"
coinbeneOpenOrders = "/order/openOrders"
coinbeneClosedOrders = "/order/closedOrders"
authRateLimit = 150
unauthRateLimit = 10
)
// SetDefaults sets the basic defaults for Coinbene
func (c *Coinbene) SetDefaults() {
c.Name = "Coinbene"
c.Enabled = false
c.Verbose = false
c.RESTPollingDelay = 10
c.RequestCurrencyPairFormat.Delimiter = "/"
c.RequestCurrencyPairFormat.Uppercase = true
c.ConfigCurrencyPairFormat.Delimiter = "/"
c.ConfigCurrencyPairFormat.Uppercase = true
c.AssetTypes = []string{ticker.Spot}
c.SupportsAutoPairUpdating = true
c.SupportsRESTTickerBatching = false
c.Requester = request.New(c.Name,
request.NewRateLimit(time.Minute, authRateLimit),
request.NewRateLimit(time.Second, unauthRateLimit),
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
c.APIUrlDefault = coinbeneAPIURL
c.APIUrl = c.APIUrlDefault
c.Websocket = wshandler.New()
c.WebsocketURL = coinbeneWsURL
c.Websocket.Functionality = wshandler.WebsocketTickerSupported |
wshandler.WebsocketTradeDataSupported |
wshandler.WebsocketKlineSupported |
wshandler.WebsocketAccountDataSupported |
wshandler.WebsocketOrderbookSupported |
wshandler.WebsocketSubscribeSupported |
wshandler.WebsocketUnsubscribeSupported |
wshandler.WebsocketAuthenticatedEndpointsSupported
c.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit
c.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout
c.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit
}
// Setup takes in the supplied exchange configuration details and sets params
func (c *Coinbene) Setup(exch *config.ExchangeConfig) {
if !exch.Enabled {
c.SetEnabled(false)
} else {
c.Enabled = true
c.AuthenticatedAPISupport = exch.AuthenticatedAPISupport
c.AuthenticatedWebsocketAPISupport = exch.AuthenticatedWebsocketAPISupport
c.SetAPIKeys(exch.APIKey, exch.APISecret, "", false)
c.SetHTTPClientTimeout(exch.HTTPTimeout)
c.SetHTTPClientUserAgent(exch.HTTPUserAgent)
c.RESTPollingDelay = exch.RESTPollingDelay
c.Verbose = exch.Verbose
c.Websocket.SetWsStatusAndConnection(exch.Websocket)
c.BaseCurrencies = exch.BaseCurrencies
c.AvailablePairs = exch.AvailablePairs
c.EnabledPairs = exch.EnabledPairs
err := c.SetCurrencyPairFormat()
if err != nil {
log.Fatal(err)
}
err = c.SetAssetTypes()
if err != nil {
log.Fatal(err)
}
err = c.SetAutoPairDefaults()
if err != nil {
log.Fatal(err)
}
err = c.SetAPIURL(exch)
if err != nil {
log.Fatal(err)
}
err = c.SetClientProxyAddress(exch.ProxyAddress)
if err != nil {
log.Fatal(err)
}
err = c.Websocket.Setup(c.WsConnect,
c.Subscribe,
c.Unsubscribe,
exch.Name,
exch.Websocket,
exch.Verbose,
coinbeneWsURL,
exch.WebsocketURL,
exch.AuthenticatedWebsocketAPISupport)
if err != nil {
log.Fatal(err)
}
c.WebsocketConn = &wshandler.WebsocketConnection{
ExchangeName: c.Name,
URL: c.Websocket.GetWebsocketURL(),
ProxyURL: c.Websocket.GetProxyAddress(),
Verbose: c.Verbose,
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
}
c.Websocket.Orderbook.Setup(
exch.WebsocketOrderbookBufferLimit,
true,
true,
false,
false,
exch.Name)
}
}
// FetchTicker gets and stores ticker data for a currency pair
func (c *Coinbene) FetchTicker(symbol string) (TickerResponse, error) {
var t TickerResponse
params := url.Values{}
params.Set("symbol", symbol)
path := common.EncodeURLValues(c.APIUrl+coinbeneAPIVersion+coinbeneFetchTicker, params)
return t, c.SendHTTPRequest(path, &t)
}
// FetchOrderbooks gets and stores orderbook data for given pair
func (c *Coinbene) FetchOrderbooks(symbol string, size int64) (OrderbookResponse, error) {
var o OrderbookResponse
params := url.Values{}
params.Set("symbol", symbol)
params.Set("depth", strconv.FormatInt(size, 10))
path := common.EncodeURLValues(c.APIUrl+coinbeneAPIVersion+coinbeneFetchOrderBook, params)
return o, c.SendHTTPRequest(path, &o)
}
// GetTrades gets recent trades from the exchange
func (c *Coinbene) GetTrades(symbol string) (TradeResponse, error) {
var t TradeResponse
params := url.Values{}
params.Set("symbol", symbol)
path := common.EncodeURLValues(c.APIUrl+coinbeneAPIVersion+coinbeneGetTrades, params)
return t, c.SendHTTPRequest(path, &t)
}
// GetPairInfo gets info about a single pair
func (c *Coinbene) GetPairInfo(symbol string) (PairResponse, error) {
var resp PairResponse
params := url.Values{}
params.Set("symbol", symbol)
path := common.EncodeURLValues(c.APIUrl+coinbeneAPIVersion+coinbenePairInfo, params)
return resp, c.SendHTTPRequest(path, &resp)
}
// GetAllPairs gets all pairs on the exchange
func (c *Coinbene) GetAllPairs() (AllPairResponse, error) {
var a AllPairResponse
path := c.APIUrl + coinbeneAPIVersion + coinbeneGetAllPairs
return a, c.SendHTTPRequest(path, &a)
}
// GetUserBalance gets user balanace info
func (c *Coinbene) GetUserBalance() (UserBalanceResponse, error) {
var resp UserBalanceResponse
path := c.APIUrl + coinbeneAPIVersion + coinbeneGetUserBalance
err := c.SendAuthHTTPRequest(http.MethodGet, path, coinbeneGetUserBalance, nil, &resp)
if err != nil {
return resp, err
}
if resp.Code != 200 {
return resp, fmt.Errorf(resp.Message)
}
return resp, nil
}
// PlaceOrder creates an order
func (c *Coinbene) PlaceOrder(price, quantity float64, symbol, direction, clientID string) (PlaceOrderResponse, error) {
var resp PlaceOrderResponse
path := c.APIUrl + coinbeneAPIVersion + coinbenePlaceOrder
params := url.Values{}
params.Set("symbol", symbol)
switch direction {
case sell:
params.Set("direction", "2")
case buy:
params.Set("direction", "1")
default:
return resp,
fmt.Errorf("passed in direction %s is invalid must be 'buy' or 'sell'",
direction)
}
params.Set("price", strconv.FormatFloat(price, 'f', -1, 64))
params.Set("quantity", strconv.FormatFloat(quantity, 'f', -1, 64))
params.Set("clientId", clientID)
err := c.SendAuthHTTPRequest(http.MethodPost,
path,
coinbenePlaceOrder,
params,
&resp)
if err != nil {
return resp, err
}
if resp.Code != 200 {
return resp, fmt.Errorf(resp.Message)
}
return resp, nil
}
// FetchOrderInfo gets order info
func (c *Coinbene) FetchOrderInfo(orderID string) (OrderInfoResponse, error) {
var resp OrderInfoResponse
params := url.Values{}
params.Set("orderId", orderID)
path := c.APIUrl + coinbeneAPIVersion + coinbeneOrderInfo
err := c.SendAuthHTTPRequest(http.MethodGet, path, coinbeneOrderInfo, params, &resp)
if err != nil {
return resp, err
}
if resp.Code != 200 {
return resp, fmt.Errorf(resp.Message)
}
if resp.Order.OrderID != orderID {
return resp, fmt.Errorf("%s orderID doesn't match the returned orderID %s",
orderID, resp.Order.OrderID)
}
return resp, nil
}
// RemoveOrder removes a given order
func (c *Coinbene) RemoveOrder(orderID string) (RemoveOrderResponse, error) {
var resp RemoveOrderResponse
params := url.Values{}
params.Set("orderId", orderID)
path := c.APIUrl + coinbeneAPIVersion + coinbeneRemoveOrder
err := c.SendAuthHTTPRequest(http.MethodPost, path, coinbeneRemoveOrder, params, &resp)
if err != nil {
return resp, err
}
if resp.Code != 200 {
return resp, fmt.Errorf(resp.Message)
}
return resp, nil
}
// FetchOpenOrders finds open orders
func (c *Coinbene) FetchOpenOrders(symbol string) (OpenOrderResponse, error) {
var resp OpenOrderResponse
params := url.Values{}
params.Set("symbol", symbol)
path := c.APIUrl + coinbeneAPIVersion + coinbeneOpenOrders
for i := int64(1); ; i++ {
var temp OpenOrderResponse
params.Set("pageNum", strconv.FormatInt(i, 10))
err := c.SendAuthHTTPRequest(http.MethodGet, path, coinbeneOpenOrders, params, &temp)
if err != nil {
return resp, err
}
if temp.Code != 200 {
return resp, fmt.Errorf(temp.Message)
}
for j := range temp.OpenOrders {
resp.OpenOrders = append(resp.OpenOrders, temp.OpenOrders[j])
}
if len(temp.OpenOrders) != 20 {
break
}
}
return resp, nil
}
// FetchClosedOrders finds open orders
func (c *Coinbene) FetchClosedOrders(symbol, latestID string) (ClosedOrderResponse, error) {
var resp ClosedOrderResponse
params := url.Values{}
params.Set("symbol", symbol)
params.Set("latestOrderId", latestID)
path := c.APIUrl + coinbeneAPIVersion + coinbeneClosedOrders
for i := int64(1); ; i++ {
var temp ClosedOrderResponse
params.Set("pageNum", strconv.FormatInt(i, 10))
err := c.SendAuthHTTPRequest(http.MethodGet, path, coinbeneClosedOrders, params, &temp)
if err != nil {
return resp, err
}
if temp.Code != 200 {
return resp, fmt.Errorf(temp.Message)
}
for j := range temp.Data {
resp.Data = append(resp.Data, temp.Data[j])
}
if len(temp.Data) != 20 {
break
}
}
return resp, nil
}
// SendHTTPRequest sends an unauthenticated HTTP request
func (c *Coinbene) SendHTTPRequest(path string, result interface{}) error {
return c.SendPayload(http.MethodGet,
path,
nil,
nil,
&result,
false,
false,
c.Verbose,
c.HTTPDebugging,
c.HTTPRecording)
}
// SendAuthHTTPRequest sends an authenticated HTTP request
func (c *Coinbene) SendAuthHTTPRequest(method, path, epPath string, params url.Values, result interface{}) error {
if params == nil {
params = url.Values{}
}
timestamp := time.Now().UTC().Format("2006-01-02T15:04:05.999Z")
var finalBody io.Reader
var preSign string
switch {
case len(params) != 0 && method == http.MethodGet:
preSign = fmt.Sprintf("%s%s%s%s?%s", timestamp, method, coinbeneAuthPath, epPath, params.Encode())
path = common.EncodeURLValues(path, params)
case len(params) != 0:
m := make(map[string]string)
for k, v := range params {
m[k] = strings.Join(v, "")
}
tempBody, err := json.Marshal(m)
if err != nil {
return err
}
finalBody = bytes.NewBufferString(string(tempBody))
preSign = timestamp + method + coinbeneAuthPath + epPath + string(tempBody)
case len(params) == 0:
preSign = timestamp + method + coinbeneAuthPath + epPath
}
tempSign := common.GetHMAC(common.HashSHA256, []byte(preSign), []byte(c.APISecret))
headers := make(map[string]string)
headers["Content-Type"] = "application/json"
headers["ACCESS-KEY"] = c.APIKey
headers["ACCESS-SIGN"] = common.HexEncodeToString(tempSign)
headers["ACCESS-TIMESTAMP"] = timestamp
return c.SendPayload(method,
path,
headers,
finalBody,
&result,
true,
false,
c.Verbose,
c.HTTPDebugging,
c.HTTPRecording)
}

View File

@@ -0,0 +1,184 @@
package coinbene
import (
"log"
"os"
"testing"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
)
// Please supply your own keys here for due diligence testing
const (
testAPIKey = ""
testAPISecret = ""
canManipulateRealOrders = false
btcusdt = "BTC/USDT"
)
var c Coinbene
func TestMain(m *testing.M) {
c.SetDefaults()
cfg := config.GetConfig()
err := cfg.LoadConfig("../../testdata/configtest.json")
if err != nil {
log.Fatalf("Test Failed - Coinbene Setup() init error:, %v", err)
}
coinbeneConfig, err := cfg.GetExchangeConfig("Coinbene")
if err != nil {
log.Fatalf("Test Failed - Coinbene Setup() init error: %v", err)
}
coinbeneConfig.Websocket = true
coinbeneConfig.AuthenticatedAPISupport = true
coinbeneConfig.APISecret = testAPISecret
coinbeneConfig.APIKey = testAPIKey
c.Setup(&coinbeneConfig)
os.Exit(m.Run())
}
func areTestAPIKeysSet() bool {
if c.APIKey != "" && c.APIKey != "Key" &&
c.APISecret != "" && c.APISecret != "Secret" {
return true
}
return false
}
func TestFetchTicker(t *testing.T) {
t.Parallel()
_, err := c.FetchTicker(btcusdt)
if err != nil {
t.Error(err)
}
}
func TestFetchOrderbooks(t *testing.T) {
t.Parallel()
_, err := c.FetchOrderbooks(btcusdt, 100)
if err != nil {
t.Error(err)
}
}
func TestGetTrades(t *testing.T) {
t.Parallel()
_, err := c.GetTrades(btcusdt)
if err != nil {
t.Error(err)
}
}
func TestGetAllPairs(t *testing.T) {
t.Parallel()
_, err := c.GetAllPairs()
if err != nil {
t.Error(err)
}
}
func TestGetPairInfo(t *testing.T) {
t.Parallel()
_, err := c.GetPairInfo(btcusdt)
if err != nil {
t.Error(err)
}
}
func TestGetUserBalance(t *testing.T) {
t.Parallel()
if !areTestAPIKeysSet() {
t.Skip("API keys required but not set, skipping test")
}
_, err := c.GetUserBalance()
if err != nil {
t.Error(err)
}
}
func TestPlaceOrder(t *testing.T) {
t.Parallel()
if !areTestAPIKeysSet() || !canManipulateRealOrders {
t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly")
}
_, err := c.PlaceOrder(140, 1, btcusdt, "buy", "")
if err != nil {
t.Error(err)
}
}
func TestFetchOrderInfo(t *testing.T) {
t.Parallel()
if !areTestAPIKeysSet() {
t.Skip("API keys required but not set, skipping test")
}
_, err := c.FetchOrderInfo("adfjashjgsag")
if err != nil {
t.Error(err)
}
}
func TestRemoveOrder(t *testing.T) {
t.Parallel()
if !areTestAPIKeysSet() || !canManipulateRealOrders {
t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly")
}
_, err := c.RemoveOrder("adfjashjgsag")
if err != nil {
t.Error(err)
}
}
func TestFetchOpenOrders(t *testing.T) {
t.Parallel()
if !areTestAPIKeysSet() {
t.Skip("API keys required but not set, skipping test")
}
_, err := c.FetchOpenOrders(btcusdt)
if err != nil {
t.Error(err)
}
}
func TestFetchClosedOrders(t *testing.T) {
t.Parallel()
if !areTestAPIKeysSet() {
t.Skip("API keys required but not set, skipping test")
}
_, err := c.FetchClosedOrders(btcusdt, "")
if err != nil {
t.Error(err)
}
}
func TestUpdateTicker(t *testing.T) {
t.Parallel()
cp := currency.NewPairWithDelimiter("BTC", "USDT", "/")
_, err := c.UpdateTicker(cp, "spot")
if err != nil {
t.Error(err)
}
}
func TestGetAccountInfo(t *testing.T) {
t.Parallel()
if !areTestAPIKeysSet() {
t.Skip("API keys required but not set, skipping test")
}
_, err := c.GetAccountInfo()
if err != nil {
t.Error(err)
}
}
func TestUpdateOrderbook(t *testing.T) {
t.Parallel()
cp := currency.NewPairWithDelimiter("BTC", "USDT", "/")
_, err := c.UpdateOrderbook(cp, "spot")
if err != nil {
t.Error(err)
}
}

View File

@@ -0,0 +1,248 @@
package coinbene
// TickerData stores ticker data
type TickerData struct {
Symbol string `json:"symbol"`
LatestPrice float64 `json:"latestPrice,string"`
BestBid float64 `json:"bestBid,string"`
BestAsk float64 `json:"bestAsk,string"`
DailyHigh float64 `json:"high24h,string"`
DailyLow float64 `json:"low24h,string"`
DailyVolume float64 `json:"volume24h,string"`
}
// TickerResponse stores ticker response data
type TickerResponse struct {
Code int64 `json:"code"`
Message string `json:"message"`
TickerData `json:"data"`
}
// Orderbook stores orderbook info
type Orderbook struct {
Asks [][]string `json:"asks"`
Bids [][]string `json:"bids"`
}
// OrderbookResponse stores data from fetched orderbooks
type OrderbookResponse struct {
Code int64 `json:"code"`
Message string `json:"message"`
Orderbook `json:"data"`
}
// TradeResponse stores trade data
type TradeResponse struct {
Code int64 `json:"code"`
Message string `json:"message"`
Trades [][]string `json:"data"`
}
// AllPairData stores pair data
type AllPairData struct {
Symbol string `json:"symbol"`
BaseAsset string `json:"baseAsset"`
QuoteAsset string `json:"quoteAsset"`
PricePrecision int64 `json:"pricePrecision,string"`
AmountPrecision int64 `json:"amountPrecision,string"`
TakerFeeRate float64 `json:"takerFeeRate,string"`
MakerFeeRate float64 `json:"makerFeeRate,string"`
MinAmount float64 `json:"minAmount,string"`
Site string `json:"site"`
PriceFluctuation float64 `json:"priceFluctuation,string"`
}
// AllPairResponse stores data for all pairs enabled on exchange
type AllPairResponse struct {
Code int64 `json:"code"`
Message string `json:"message"`
Data []AllPairData `json:"data"`
}
// PairResponse stores data for a single queried pair
type PairResponse struct {
Code int64 `json:"code"`
Message string `json:"message"`
Data AllPairData `json:"data"`
}
// UserBalanceData stores user balance data
type UserBalanceData struct {
Asset string `json:"asset"`
Available float64 `json:"available,string"`
Reserved float64 `json:"reserved,string"`
Total float64 `json:"total,string"`
}
// UserBalanceResponse stores user balance data
type UserBalanceResponse struct {
Code int64 `json:"code"`
Message string `json:"message"`
Data []UserBalanceData `json:"data"`
}
// PlaceOrderResponse stores data for a placed order
type PlaceOrderResponse struct {
Code int64 `json:"code"`
Message string `json:"message"`
Status string `json:"status"`
Timestamp int64 `json:"timestamp"`
OrderID string `json:"orderid"`
}
// OrderInfoData stores order info
type OrderInfoData struct {
OrderID string `json:"orderId"`
BaseAsset string `json:"baseAsset"`
QuoteAsset string `json:"quoteAsset"`
OrderType string `json:"orderDirection"`
Quantity float64 `json:"quntity,string"`
Amount float64 `json:"amout,string"`
FilledAmount float64 `json:"filledAmount"`
TakerRate float64 `json:"takerFeeRate,string"`
MakerRate float64 `json:"makerRate,string"`
AvgPrice float64 `json:"avgPrice,string"`
OrderPrice float64 `json:"orderPrice,string"`
OrderStatus string `json:"orderStatus"`
OrderTime string `json:"orderTime"`
TotalFee float64 `json:"totalFee"`
}
// OrderInfoResponse stores orderinfo data
type OrderInfoResponse struct {
Order OrderInfoData `json:"data"`
Code int64 `json:"code"`
Message string `json:"message"`
}
// RemoveOrderResponse stores data for the remove request
type RemoveOrderResponse struct {
Code int64 `json:"code"`
Message string `json:"message"`
OrderID string `json:"data"`
}
// OpenOrderResponse stores data for open orders
type OpenOrderResponse struct {
Code int64 `json:"code"`
Message string `json:"message"`
OpenOrders []OrderInfoData `json:"data"`
}
// ClosedOrderResponse stores data for closed orders
type ClosedOrderResponse struct {
Code int64 `json:"code"`
Message string `json:"message"`
Data []OrderInfoData `json:"data"`
}
// WsSub stores subscription data
type WsSub struct {
Operation string `json:"op"`
Arguments []string `json:"args"`
}
// WsTickerData stores websocket ticker data
type WsTickerData struct {
Symbol string `json:"symbol"`
LastPrice float64 `json:"lastPrice,string"`
MarkPrice float64 `json:"markPrice,string"`
BestAskPrice float64 `json:"bestAskPrice,string"`
BestBidPrice float64 `json:"bestBidPrice,string"`
BestAskVolume float64 `json:"bestAskVolume,string"`
BestBidVolume float64 `json:"bestBidVolume,string"`
High24h float64 `json:"high24h,string"`
Low24h float64 `json:"low24h,string"`
Volume24h float64 `json:"volume,string"`
Timestamp string `json:"timestamp"`
}
// WsTicker stores websocket ticker
type WsTicker struct {
Topic string `json:"topic"`
Data []WsTickerData `json:"data"`
}
// WsTradeList stores websocket tradelist data
type WsTradeList struct {
Topic string `json:"topic"`
Data [][]string `json:"data"`
}
// WsOrderbook stores websocket orderbook data
type WsOrderbook struct {
Topic string `json:"topic"`
Action string `json:"action"`
Data []Orderbook `json:"data"`
Version int64 `json:"version,string"`
Timestamp string `json:"timestamp"`
}
// WsKline stores websocket kline data
type WsKline struct {
Topic string `json:"topic"`
Data [][]interface{} `json:"data"`
}
// WsUserData stores websocket user data
type WsUserData struct {
Asset string `json:"string"`
Available float64 `json:"availableBalance"`
Locked float64 `json:"frozenBalance"`
Total float64 `json:"balance"`
Timestamp string `json:"timestamp"`
}
// WsUserInfo stores websocket user info
type WsUserInfo struct {
Topic string `json:"topic"`
Data []WsUserData `json:"data"`
}
// WsPositionData stores websocket info on user's position
type WsPositionData struct {
AvailableQuantity float64 `json:"availableQuantity"`
AveragePrice float64 `json:"avgPrice"`
Leverage float64 `json:"leverage"`
LiquidationPrice float64 `json:"liquidationPrice"`
MarkPrice float64 `json:"markPrice"`
PositionMargin float64 `json:"positionMargin"`
Quantity float64 `json:"quantity"`
RealisedPNL float64 `json:"realisedPnl"`
Side string `json:"side"`
Symbol string `json:"symbol"`
MarginMode int64 `json:"marginMode"`
CreateTime string `json:"createTime"`
}
// WsPosition stores websocket info on user's positions
type WsPosition struct {
Topic string `json:"topic"`
Data []WsPositionData `json:"data"`
}
// WsOrderData stores websocket user order data
type WsOrderData struct {
OrderID string `json:"orderId"`
Direction string `json:"direction"`
Leverage float64 `json:"leverage"`
Symbol string `json:"symbol"`
OrderType string `json:"orderType"`
Quantity float64 `json:"quantity"`
OrderPrice float64 `json:"orderPrice"`
OrderValue float64 `json:"orderValue"`
Fee float64 `json:"fee"`
FilledQuantity float64 `json:"filledQuantity"`
AveragePrice float64 `json:"averagePrice"`
OrderTime string `json:"orderTime"`
Status string `json:"status"`
LastFillQuantity float64 `json:"lastFillQuantity"`
LastFillPrice float64 `json:"lastFillPrice"`
LastFillTime string `json:"lastFillTime"`
}
// WsUserOrders stores websocket user orders' data
type WsUserOrders struct {
Topic string `json:"topic"`
Data []WsOrderData `json:"data"`
}

View File

@@ -0,0 +1,336 @@
package coinbene
import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/gorilla/websocket"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wsorderbook"
)
const (
coinbeneWsURL = "wss://ws-contract.coinbene.vip/openapi/ws"
event = "event"
topic = "topic"
)
// WsConnect connects to websocket
func (c *Coinbene) WsConnect() error {
if !c.Websocket.IsEnabled() || !c.IsEnabled() {
return errors.New(wshandler.WebsocketNotEnabled)
}
var dialer websocket.Dialer
err := c.WebsocketConn.Dial(&dialer, http.Header{})
if err != nil {
return err
}
go c.WsDataHandler()
if c.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
err = c.Login()
if err != nil {
c.Websocket.DataHandler <- err
c.Websocket.SetCanUseAuthenticatedEndpoints(false)
}
}
c.GenerateDefaultSubscriptions()
return nil
}
// GenerateDefaultSubscriptions generates stuff
func (c *Coinbene) GenerateDefaultSubscriptions() {
var channels = []string{"orderBook.%s.100", "tradeList.%s", "ticker.%s", "kline.%s"}
var subscriptions []wshandler.WebsocketChannelSubscription
for x := range channels {
for y := range c.EnabledPairs {
c.EnabledPairs[y].Delimiter = ""
subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{
Channel: fmt.Sprintf(channels[x], c.EnabledPairs[y]),
Currency: c.EnabledPairs[y],
})
}
}
c.Websocket.SubscribeToChannels(subscriptions)
}
// GenerateAuthSubs generates auth subs
func (c *Coinbene) GenerateAuthSubs() {
var subscriptions []wshandler.WebsocketChannelSubscription
var sub wshandler.WebsocketChannelSubscription
var userChannels = []string{"user.account", "user.position", "user.order"}
for z := range userChannels {
sub.Channel = userChannels[z]
subscriptions = append(subscriptions, sub)
}
c.Websocket.SubscribeToChannels(subscriptions)
}
// WsDataHandler handles websocket data
func (c *Coinbene) WsDataHandler() {
c.Websocket.Wg.Add(1)
defer c.Websocket.Wg.Done()
for {
select {
case <-c.Websocket.ShutdownC:
return
default:
stream, err := c.WebsocketConn.ReadMessage()
if err != nil {
c.Websocket.DataHandler <- err
return
}
c.Websocket.TrafficAlert <- struct{}{}
if string(stream.Raw) == "ping" {
c.WebsocketConn.Lock()
c.WebsocketConn.Connection.WriteMessage(websocket.TextMessage, []byte("pong"))
c.WebsocketConn.Unlock()
continue
}
var result map[string]interface{}
err = common.JSONDecode(stream.Raw, &result)
if err != nil {
c.Websocket.DataHandler <- err
}
_, ok := result[event]
switch {
case ok && (result[event].(string) == "subscribe" || result[event].(string) == "unsubscribe"):
continue
case ok && result[event].(string) == "error":
c.Websocket.DataHandler <- fmt.Errorf("message: %s. code: %v", result["message"], result["code"])
continue
}
if ok && strings.Contains(result[event].(string), "login") {
if result["success"].(bool) {
c.Websocket.SetCanUseAuthenticatedEndpoints(true)
c.GenerateAuthSubs()
continue
}
c.Websocket.SetCanUseAuthenticatedEndpoints(false)
c.Websocket.DataHandler <- fmt.Errorf("message: %s. code: %v", result["message"], result["code"])
continue
}
switch {
case strings.Contains(result[topic].(string), "ticker"):
var ticker WsTicker
err = common.JSONDecode(stream.Raw, &ticker)
if err != nil {
c.Websocket.DataHandler <- err
continue
}
for x := range ticker.Data {
c.Websocket.DataHandler <- wshandler.TickerData{
Quantity: ticker.Data[x].Volume24h,
ClosePrice: ticker.Data[x].LastPrice,
HighPrice: ticker.Data[x].High24h,
LowPrice: ticker.Data[x].Low24h,
Pair: currency.NewPairFromString(ticker.Data[x].Symbol),
Exchange: c.Name,
AssetType: orderbook.Swap,
}
}
case strings.Contains(result[topic].(string), "tradeList"):
var tradeList WsTradeList
err = common.JSONDecode(stream.Raw, &tradeList)
if err != nil {
c.Websocket.DataHandler <- err
continue
}
var t time.Time
var price, amount float64
t, err = time.Parse(time.RFC3339, tradeList.Data[0][3])
if err != nil {
c.Websocket.DataHandler <- err
continue
}
price, err = strconv.ParseFloat(tradeList.Data[0][0], 64)
if err != nil {
c.Websocket.DataHandler <- err
continue
}
amount, err = strconv.ParseFloat(tradeList.Data[0][2], 64)
if err != nil {
c.Websocket.DataHandler <- err
continue
}
c.Websocket.DataHandler <- wshandler.TradeData{
CurrencyPair: currency.NewPairFromString(strings.Replace(tradeList.Topic, "tradeList.", "", 1)),
Timestamp: t,
Price: price,
Amount: amount,
Exchange: c.Name,
AssetType: orderbook.Swap,
Side: tradeList.Data[0][1],
}
case strings.Contains(result[topic].(string), "orderBook"):
var orderBook WsOrderbook
err = common.JSONDecode(stream.Raw, &orderBook)
if err != nil {
c.Websocket.DataHandler <- err
continue
}
var amount, price float64
var asks, bids []orderbook.Item
for i := range orderBook.Data[0].Asks {
amount, err = strconv.ParseFloat(orderBook.Data[0].Asks[i][1], 64)
if err != nil {
c.Websocket.DataHandler <- err
continue
}
price, err = strconv.ParseFloat(orderBook.Data[0].Asks[i][0], 64)
if err != nil {
c.Websocket.DataHandler <- err
continue
}
asks = append(asks, orderbook.Item{
Amount: amount,
Price: price,
})
}
for j := range orderBook.Data[0].Bids {
amount, err = strconv.ParseFloat(orderBook.Data[0].Bids[j][1], 64)
if err != nil {
c.Websocket.DataHandler <- err
continue
}
price, err = strconv.ParseFloat(orderBook.Data[0].Bids[j][0], 64)
if err != nil {
c.Websocket.DataHandler <- err
continue
}
bids = append(bids, orderbook.Item{
Amount: amount,
Price: price,
})
}
if orderBook.Action == "insert" {
var newOB orderbook.Base
newOB.Asks = asks
newOB.Bids = bids
newOB.AssetType = orderbook.Swap
newOB.Pair = currency.NewPairFromString(strings.Replace(orderBook.Topic, "tradeList.", "", 1))
newOB.ExchangeName = c.Name
err = c.Websocket.Orderbook.LoadSnapshot(&newOB, true)
if err != nil {
c.Websocket.DataHandler <- err
continue
}
c.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: newOB.Pair,
Asset: orderbook.Swap,
Exchange: c.Name,
}
} else if orderBook.Action == "update" {
newOB := wsorderbook.WebsocketOrderbookUpdate{
Asks: asks,
Bids: bids,
AssetType: orderbook.Swap,
CurrencyPair: currency.NewPairFromString(strings.Replace(orderBook.Topic, "tradeList.", "", 1)),
UpdateID: orderBook.Version,
}
err = c.Websocket.Orderbook.Update(&newOB)
if err != nil {
c.Websocket.DataHandler <- err
continue
}
c.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: newOB.CurrencyPair,
Asset: orderbook.Swap,
Exchange: c.Name,
}
}
case strings.Contains(result[topic].(string), "kline"):
var kline WsKline
var tempFloat float64
var tempKline []float64
err = common.JSONDecode(stream.Raw, &kline)
if err != nil {
c.Websocket.DataHandler <- err
continue
}
for x := 2; x < len(kline.Data[0]); x++ {
tempFloat, err = strconv.ParseFloat(kline.Data[0][x].(string), 64)
if err != nil {
c.Websocket.DataHandler <- err
continue
}
tempKline = append(tempKline, tempFloat)
}
c.Websocket.DataHandler <- wshandler.KlineData{
Timestamp: time.Unix(int64(kline.Data[0][1].(float64)), 0),
Pair: currency.NewPairFromString(kline.Data[0][0].(string)),
AssetType: orderbook.Swap,
Exchange: c.Name,
OpenPrice: tempKline[0],
ClosePrice: tempKline[1],
HighPrice: tempKline[2],
LowPrice: tempKline[3],
Volume: tempKline[4],
}
case strings.Contains(result[topic].(string), "user.account"):
var userinfo WsUserInfo
err = common.JSONDecode(stream.Raw, &userinfo)
if err != nil {
c.Websocket.DataHandler <- err
continue
}
c.Websocket.DataHandler <- userinfo
case strings.Contains(result[topic].(string), "user.position"):
var position WsPosition
err = common.JSONDecode(stream.Raw, &position)
if err != nil {
c.Websocket.DataHandler <- err
continue
}
c.Websocket.DataHandler <- position
case strings.Contains(result[topic].(string), "user.order"):
var orders WsUserOrders
err = common.JSONDecode(stream.Raw, &orders)
if err != nil {
c.Websocket.DataHandler <- err
continue
}
c.Websocket.DataHandler <- orders
default:
c.Websocket.DataHandler <- fmt.Errorf("%s - unhandled response '%s'", c.Name, stream.Raw)
}
}
}
}
// Subscribe sends a websocket message to receive data from the channel
func (c *Coinbene) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error {
var sub WsSub
sub.Operation = "subscribe"
sub.Arguments = []string{channelToSubscribe.Channel}
return c.WebsocketConn.SendMessage(sub)
}
// Unsubscribe sends a websocket message to receive data from the channel
func (c *Coinbene) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error {
var sub WsSub
sub.Operation = "unsubscribe"
sub.Arguments = []string{channelToSubscribe.Channel}
return c.WebsocketConn.SendMessage(sub)
}
// Login logs in
func (c *Coinbene) Login() error {
var sub WsSub
expTime := time.Now().Add(time.Minute * 10).Format("2006-01-02T15:04:05Z")
signMsg := expTime + http.MethodGet + "/login"
tempSign := common.GetHMAC(common.HashSHA256, []byte(signMsg), []byte(c.APISecret))
sign := common.HexEncodeToString(tempSign)
sub.Operation = "login"
sub.Arguments = []string{c.APIKey, expTime, sign}
return c.WebsocketConn.SendMessage(sub)
}

View File

@@ -0,0 +1,409 @@
package coinbene
import (
"fmt"
"strconv"
"sync"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
log "github.com/thrasher-corp/gocryptotrader/logger"
)
// Start starts the Coinbene go routine
func (c *Coinbene) Start(wg *sync.WaitGroup) {
wg.Add(1)
go func() {
c.Run()
wg.Done()
}()
}
// Run implements the Coinbene wrapper
func (c *Coinbene) Run() {
if c.Verbose {
log.Debugf("%s Websocket: %s. (url: %s).\n", c.Name, common.IsEnabled(c.Websocket.IsEnabled()), c.Websocket.GetWebsocketURL())
log.Debugf("%s polling delay: %ds.\n", c.Name, c.RESTPollingDelay)
log.Debugf("%s %d currencies enabled: %s.\n", c.Name, len(c.EnabledPairs), c.EnabledPairs)
}
exchangeCurrencies, err := c.GetAllPairs()
if err != nil {
log.Errorf("%s Failed to get available symbols.\n", c.Name)
} else {
var newExchangeCurrencies currency.Pairs
for p := range exchangeCurrencies.Data {
newExchangeCurrencies = append(newExchangeCurrencies,
currency.NewPairFromString(exchangeCurrencies.Data[p].Symbol))
}
err = c.UpdateCurrencies(newExchangeCurrencies, false, false)
if err != nil {
log.Errorf("%s Failed to update available currencies %s.\n",
c.Name,
err)
}
}
}
// UpdateTicker updates and returns the ticker for a currency pair
func (c *Coinbene) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, error) {
var resp ticker.Price
allPairs := c.GetEnabledCurrencies()
for x := range allPairs {
tempResp, err := c.FetchTicker(exchange.FormatExchangeCurrency(c.Name,
allPairs[x]).String())
if err != nil {
return resp, err
}
resp.Pair = allPairs[x]
resp.Last = tempResp.TickerData.LatestPrice
resp.High = tempResp.TickerData.DailyHigh
resp.Low = tempResp.TickerData.DailyLow
resp.Bid = tempResp.TickerData.BestBid
resp.Ask = tempResp.TickerData.BestAsk
resp.Volume = tempResp.TickerData.DailyVolume
resp.LastUpdated = time.Now()
err = ticker.ProcessTicker(c.Name, &resp, assetType)
if err != nil {
return resp, err
}
}
return ticker.GetTicker(c.Name, p, assetType)
}
// GetTickerPrice returns the ticker for a currency pair
func (c *Coinbene) GetTickerPrice(p currency.Pair, assetType string) (ticker.Price, error) {
tickerNew, err := ticker.GetTicker(c.Name, p, assetType)
if err != nil {
return c.UpdateTicker(p, assetType)
}
return tickerNew, nil
}
// GetOrderbookEx returns orderbook base on the currency pair
func (c *Coinbene) GetOrderbookEx(currency currency.Pair, assetType string) (orderbook.Base, error) {
ob, err := orderbook.Get(c.Name, currency, assetType)
if err != nil {
return c.UpdateOrderbook(currency, assetType)
}
return ob, nil
}
// UpdateOrderbook updates and returns the orderbook for a currency pair
func (c *Coinbene) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Base, error) {
var resp orderbook.Base
strPair := exchange.FormatExchangeCurrency(c.Name, p).String()
tempResp, err := c.FetchOrderbooks(strPair, 100)
if err != nil {
return resp, err
}
resp.ExchangeName = c.Name
resp.Pair = p
resp.AssetType = assetType
var amount, price float64
for i := range tempResp.Orderbook.Asks {
amount, err = strconv.ParseFloat(tempResp.Orderbook.Asks[i][1], 64)
if err != nil {
return resp, err
}
price, err = strconv.ParseFloat(tempResp.Orderbook.Asks[i][0], 64)
if err != nil {
return resp, err
}
resp.Asks = append(resp.Asks, orderbook.Item{
Price: price,
Amount: amount})
}
for j := range tempResp.Orderbook.Bids {
amount, err = strconv.ParseFloat(tempResp.Orderbook.Bids[j][1], 64)
if err != nil {
return resp, err
}
price, err = strconv.ParseFloat(tempResp.Orderbook.Bids[j][0], 64)
if err != nil {
return resp, err
}
resp.Bids = append(resp.Bids, orderbook.Item{
Price: price,
Amount: amount})
}
err = resp.Process()
if err != nil {
return resp, err
}
return orderbook.Get(c.Name, p, assetType)
}
// GetAccountInfo retrieves balances for all enabled currencies for the
// Coinbene exchange
func (c *Coinbene) GetAccountInfo() (exchange.AccountInfo, error) {
var info exchange.AccountInfo
data, err := c.GetUserBalance()
if err != nil {
return info, err
}
var account exchange.Account
for key := range data.Data {
c := currency.NewCode(data.Data[key].Asset)
hold := data.Data[key].Reserved
available := data.Data[key].Available
account.Currencies = append(account.Currencies,
exchange.AccountCurrencyInfo{CurrencyName: c,
TotalValue: hold + available,
Hold: hold})
}
info.Accounts = append(info.Accounts, account)
info.Exchange = c.Name
return info, nil
}
// GetFundingHistory returns funding history, deposits and
// withdrawals
func (c *Coinbene) GetFundingHistory() ([]exchange.FundHistory, error) {
return nil, common.ErrFunctionNotSupported
}
// GetExchangeHistory returns historic trade data since exchange opening.
func (c *Coinbene) GetExchangeHistory(p currency.Pair, assetType string) ([]exchange.TradeHistory, error) {
return nil, common.ErrFunctionNotSupported
}
// SubmitOrder submits a new order
func (c *Coinbene) SubmitOrder(p currency.Pair, side exchange.OrderSide, orderType exchange.OrderType, amount, price float64, clientID string) (exchange.SubmitOrderResponse, error) {
var resp exchange.SubmitOrderResponse
if side != exchange.BuyOrderSide && side != exchange.SellOrderSide {
return resp,
fmt.Errorf("%s orderside is not supported by this exchange", side)
}
tempResp, err := c.PlaceOrder(price,
amount,
exchange.FormatExchangeCurrency(c.Name, p).String(),
orderType.ToString(),
clientID)
if err != nil {
return resp, err
}
resp.IsOrderPlaced = true
resp.OrderID = tempResp.OrderID
return resp, nil
}
// ModifyOrder will allow of changing orderbook placement and limit to
// market conversion
func (c *Coinbene) ModifyOrder(action *exchange.ModifyOrder) (string, error) {
return "", common.ErrFunctionNotSupported
}
// CancelOrder cancels an order by its corresponding ID number
func (c *Coinbene) CancelOrder(order *exchange.OrderCancellation) error {
_, err := c.RemoveOrder(order.OrderID)
return err
}
// CancelAllOrders cancels all orders associated with a currency pair
func (c *Coinbene) CancelAllOrders(orderCancellation *exchange.OrderCancellation) (exchange.CancelAllOrdersResponse, error) {
var resp exchange.CancelAllOrdersResponse
tempMap := make(map[string]string)
orders, err := c.FetchOpenOrders(exchange.FormatExchangeCurrency(c.Name,
orderCancellation.CurrencyPair).String())
if err != nil {
return resp, err
}
for x := range orders.OpenOrders {
_, err := c.RemoveOrder(orders.OpenOrders[x].OrderID)
if err != nil {
tempMap[orders.OpenOrders[x].OrderID] = "Failed"
} else {
tempMap[orders.OpenOrders[x].OrderID] = "Success"
}
}
resp.OrderStatus = tempMap
return resp, nil
}
// GetOrderInfo returns information on a current open order
func (c *Coinbene) GetOrderInfo(orderID string) (exchange.OrderDetail, error) {
var resp exchange.OrderDetail
tempResp, err := c.FetchOrderInfo(orderID)
if err != nil {
return resp, err
}
var t time.Time
resp.Exchange = c.Name
resp.ID = orderID
resp.CurrencyPair = currency.NewPairWithDelimiter(tempResp.Order.BaseAsset,
"/",
tempResp.Order.QuoteAsset)
t, err = time.Parse(time.RFC3339, tempResp.Order.OrderTime)
if err != nil {
return resp, err
}
resp.Price = tempResp.Order.OrderPrice
resp.OrderDate = t
resp.ExecutedAmount = tempResp.Order.FilledAmount
resp.Fee = tempResp.Order.TotalFee
return resp, nil
}
// GetDepositAddress returns a deposit address for a specified currency
func (c *Coinbene) GetDepositAddress(cryptocurrency currency.Code, accountID string) (string, error) {
return "", common.ErrFunctionNotSupported
}
// WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is
// submitted
func (c *Coinbene) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) {
return "", common.ErrFunctionNotSupported
}
// WithdrawFiatFunds returns a withdrawal ID when a withdrawal is
// submitted
func (c *Coinbene) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) {
return "", common.ErrFunctionNotSupported
}
// WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a withdrawal is
// submitted
func (c *Coinbene) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.WithdrawRequest) (string, error) {
return "", common.ErrFunctionNotSupported
}
// GetWebsocket returns a pointer to the exchange websocket
func (c *Coinbene) GetWebsocket() (*wshandler.Websocket, error) {
return c.Websocket, nil
}
// GetActiveOrders retrieves any orders that are active/open
func (c *Coinbene) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) {
var resp []exchange.OrderDetail
var tempResp exchange.OrderDetail
var tempData OpenOrderResponse
if len(getOrdersRequest.Currencies) == 0 {
allPairs, err := c.GetAllPairs()
if err != nil {
return resp, err
}
for a := range allPairs.Data {
getOrdersRequest.Currencies = append(getOrdersRequest.Currencies, currency.NewPairFromString(allPairs.Data[a].Symbol))
}
}
var err error
for x := range getOrdersRequest.Currencies {
tempData, err = c.FetchOpenOrders(exchange.FormatExchangeCurrency(c.Name, getOrdersRequest.Currencies[x]).String())
if err != nil {
return resp, err
}
var t time.Time
for y := range tempData.OpenOrders {
tempResp.Exchange = c.Name
tempResp.CurrencyPair = getOrdersRequest.Currencies[x]
tempResp.OrderSide = buy
if tempData.OpenOrders[y].OrderType == sell {
tempResp.OrderSide = sell
}
t, err = time.Parse(time.RFC3339, tempData.OpenOrders[y].OrderTime)
if err != nil {
return resp, err
}
tempResp.OrderDate = t
tempResp.Status = tempData.OpenOrders[y].OrderStatus
tempResp.Price = tempData.OpenOrders[y].OrderPrice
tempResp.Amount = tempData.OpenOrders[y].Amount
tempResp.ExecutedAmount = tempData.OpenOrders[y].FilledAmount
tempResp.RemainingAmount = tempData.OpenOrders[y].Amount - tempData.OpenOrders[y].FilledAmount
tempResp.Fee = tempData.OpenOrders[y].TotalFee
resp = append(resp, tempResp)
}
}
return resp, nil
}
// GetOrderHistory retrieves account order information
// Can Limit response to specific order status
func (c *Coinbene) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) {
var resp []exchange.OrderDetail
var tempResp exchange.OrderDetail
var tempData ClosedOrderResponse
if len(getOrdersRequest.Currencies) == 0 {
allPairs, err := c.GetAllPairs()
if err != nil {
return resp, err
}
for a := range allPairs.Data {
getOrdersRequest.Currencies = append(getOrdersRequest.Currencies, currency.NewPairFromString(allPairs.Data[a].Symbol))
}
}
var err error
for x := range getOrdersRequest.Currencies {
tempData, err = c.FetchClosedOrders(exchange.FormatExchangeCurrency(c.Name, getOrdersRequest.Currencies[x]).String(), "")
if err != nil {
return resp, err
}
var t time.Time
for y := range tempData.Data {
tempResp.Exchange = c.Name
tempResp.CurrencyPair = getOrdersRequest.Currencies[x]
tempResp.OrderSide = exchange.BuyOrderSide
if tempData.Data[y].OrderType == sell {
tempResp.OrderSide = exchange.SellOrderSide
}
t, err = time.Parse(time.RFC3339, tempData.Data[y].OrderTime)
if err != nil {
return resp, err
}
tempResp.OrderDate = t
tempResp.Status = tempData.Data[y].OrderStatus
tempResp.Price = tempData.Data[y].OrderPrice
tempResp.Amount = tempData.Data[y].Amount
tempResp.ExecutedAmount = tempData.Data[y].FilledAmount
tempResp.RemainingAmount = tempData.Data[y].Amount - tempData.Data[y].FilledAmount
tempResp.Fee = tempData.Data[y].TotalFee
resp = append(resp, tempResp)
}
}
return resp, nil
}
// GetFeeByType returns an estimate of fee based on the type of transaction
func (c *Coinbene) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) {
var fee float64
tempData, err := c.GetPairInfo(exchange.FormatExchangeCurrency(c.Name, feeBuilder.Pair).String())
if err != nil {
return fee, err
}
switch feeBuilder.IsMaker {
case true:
fee = feeBuilder.PurchasePrice * feeBuilder.Amount * tempData.Data.MakerFeeRate
case false:
fee = feeBuilder.PurchasePrice * feeBuilder.Amount * tempData.Data.TakerFeeRate
}
return fee, nil
}
// SubscribeToWebsocketChannels appends to ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle subscribing
func (c *Coinbene) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
c.Websocket.SubscribeToChannels(channels)
return nil
}
// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle unsubscribing
func (c *Coinbene) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
c.Websocket.RemoveSubscribedChannels(channels)
return nil
}
// GetSubscriptions returns a copied list of subscriptions
func (c *Coinbene) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) {
return c.Websocket.GetSubscriptions(), nil
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (c *Coinbene) AuthenticateWebsocket() error {
return c.Login()
}

View File

@@ -881,6 +881,9 @@ func (e *Base) SetAPIURL(ec *config.ExchangeConfig) error {
if ec.APIURL != config.APIURLNonDefaultMessage {
e.APIUrl = ec.APIURL
}
if !strings.Contains(e.APIUrl, "https") {
log.Warnf("%s is using HTTP instead of HTTPS for API functionality, an attacker could eavesdrop on this connection. Use at your own risk", e.Name)
}
if ec.APIURLSecondary != config.APIURLNonDefaultMessage {
e.APIUrlSecondary = ec.APIURLSecondary
}

View File

@@ -17,6 +17,7 @@ const (
errQuoteCurrencyNotFound = "orderbook quote currency not found"
Spot = "SPOT"
Swap = "SWAP"
)
// Vars for the orderbook package

View File

@@ -1400,6 +1400,52 @@
"supportedCurrencies": ""
}
]
},
{
"name": "Coinbene",
"enabled": true,
"verbose": false,
"websocket": false,
"useSandbox": false,
"restPollingDelay": 10,
"httpTimeout": 0,
"websocketResponseCheckTimeout": 0,
"websocketResponseMaxLimit": 0,
"websocketOrderbookBufferLimit": 0,
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "",
"apiUrlSecondary": "",
"proxyAddress": "",
"websocketUrl": "",
"availablePairs": "BTC/USDT",
"enabledPairs": "BTC/USDT",
"baseCurrencies": "USD",
"assetTypes": "SPOT",
"supportsAutoPairUpdates": true,
"configCurrencyPairFormat": {
"uppercase": true,
"delimiter": "/"
},
"requestCurrencyPairFormat": {
"uppercase": true,
"delimiter": "/"
},
"bankAccounts": [
{
"bankName": "",
"bankAddress": "",
"accountName": "",
"accountNumber": "",
"swiftCode": "",
"iban": "",
"supportedCurrencies": ""
}
]
}
],
"bankAccounts": [

View File

@@ -54,6 +54,7 @@ const (
bittrex = "..%s..%sexchanges%sbittrex%s"
btcmarkets = "..%s..%sexchanges%sbtcmarkets%s"
coinbasepro = "..%s..%sexchanges%scoinbasepro%s"
coinbene = "..%s..%sexchanges%scoinbene%s"
coinut = "..%s..%sexchanges%scoinut%s"
exmo = "..%s..%sexchanges%sexmo%s"
gateio = "..%s..%sexchanges%sgateio%s"
@@ -225,6 +226,7 @@ func addPaths() {
codebasePaths["exchanges coinut"] = fmt.Sprintf(coinut, path, path, path, path)
codebasePaths["exchanges exmo"] = fmt.Sprintf(exmo, path, path, path, path)
codebasePaths["exchanges coinbasepro"] = fmt.Sprintf(coinbasepro, path, path, path, path)
codebasePaths["exchanges coinbene"] = fmt.Sprintf(coinbene, path, path, path, path)
codebasePaths["exchanges gateio"] = fmt.Sprintf(gateio, path, path, path, path)
codebasePaths["exchanges gemini"] = fmt.Sprintf(gemini, path, path, path, path)
codebasePaths["exchanges hitbtc"] = fmt.Sprintf(hitbtc, path, path, path, path)

View File

@@ -0,0 +1,106 @@
{{define "exchanges coinbene" -}}
{{template "header" .}}
## Coinbene Exchange
### Current Features
+ REST Support
+ Websocket Support
### How to enable
+ [Enable via configuration](https://github.com/thrasher-corp/gocryptotrader/tree/master/config#enable-exchange-via-config-example)
+ Individual package example below:
```go
// Exchanges will be abstracted out in further updates and examples will be
// supplied then
```
### How to do REST public/private calls
+ If enabled via "configuration".json file the exchange will be added to the
IBotExchange array in the ```go var bot Bot``` and you will only be able to use
the wrapper interface functions for accessing exchange data. View routines.go
for an example of integration usage with GoCryptoTrader. Rudimentary example
below:
main.go
```go
var c exchange.IBotExchange
for i := range bot.exchanges {
if bot.exchanges[i].GetName() == "Coinbene" {
c = bot.exchanges[i]
}
}
// Public calls - wrapper functions
// Fetches current ticker information
tick, err := c.GetTickerPrice()
if err != nil {
// Handle error
}
// Fetches current orderbook information
ob, err := c.GetOrderbookEx()
if err != nil {
// Handle error
}
// Private calls - wrapper functions - make sure your APIKEY and APISECRET are
// set and AuthenticatedAPISupport is set to true
// Fetches current account information
accountInfo, err := c.GetAccountInfo()
if err != nil {
// Handle error
}
```
+ If enabled via individually importing package, rudimentary example below:
```go
// Public calls
// Fetches current ticker information
ticker, err := c.GetTicker()
if err != nil {
// Handle error
}
// Fetches current orderbook information
ob, err := c.GetOrderBook()
if err != nil {
// Handle error
}
// Private calls - make sure your APIKEY and APISECRET are set and
// AuthenticatedAPISupport is set to true
// GetUserInfo returns account info
accountInfo, err := c.GetUserInfo(...)
if err != nil {
// Handle error
}
// Submits an order and the exchange and returns its tradeID
tradeID, err := c.Trade(...)
if err != nil {
// Handle error
}
```
### How to do Websocket public/private calls
```go
// Exchanges will be abstracted out in further updates and examples will be
// supplied then
```
### Please click GoDocs chevron above to view current GoDoc information for this package
{{template "contributions"}}
{{template "donations"}}
{{end}}