Merge branch 'master' into engine

This commit is contained in:
Adrian Gallagher
2019-11-07 15:08:00 +11:00
16 changed files with 2060 additions and 4 deletions

View File

@@ -32,6 +32,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader
| COINUT | Yes | Yes | NA |
| Exmo | Yes | NA | NA |
| CoinbasePro | Yes | Yes | No|
| Coinbene | Yes | No | No |
| GateIO | Yes | Yes | NA |
| Gemini | Yes | Yes | No |
| HitBTC | Yes | Yes | No |

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.FetchTicker()
if err != nil {
// Handle error
}
// Fetches current orderbook information
ob, err := c.FetchOrderbook()
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
resp, err := c.SubmitOrder(...)
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}}

View File

@@ -33,6 +33,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader
| COINUT | Yes | Yes | NA |
| Exmo | Yes | NA | NA |
| CoinbasePro | Yes | Yes | No|
| Coinbene | Yes | No | No |
| GateIO | Yes | Yes | NA |
| Gemini | Yes | Yes | No |
| HitBTC | Yes | Yes | No |

View File

@@ -16,7 +16,7 @@ import (
const (
// Default number of enabled exchanges. Modify this whenever an exchange is
// added or removed
defaultEnabledExchanges = 27
defaultEnabledExchanges = 28
testFakeExchangeName = "Stampbit"
)

View File

@@ -778,6 +778,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

@@ -166,9 +166,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,292 @@
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/common/crypto"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
)
// 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
)
// GetTicker gets and stores ticker data for a currency pair
func (c *Coinbene) GetTicker(symbol string) (TickerResponse, error) {
var t TickerResponse
params := url.Values{}
params.Set("symbol", symbol)
path := common.EncodeURLValues(c.API.Endpoints.URL+coinbeneAPIVersion+coinbeneFetchTicker, params)
return t, c.SendHTTPRequest(path, &t)
}
// GetOrderbook gets and stores orderbook data for given pair
func (c *Coinbene) GetOrderbook(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.API.Endpoints.URL+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.API.Endpoints.URL+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.API.Endpoints.URL+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.API.Endpoints.URL + coinbeneAPIVersion + coinbeneGetAllPairs
return a, c.SendHTTPRequest(path, &a)
}
// GetUserBalance gets user balanace info
func (c *Coinbene) GetUserBalance() (UserBalanceResponse, error) {
var resp UserBalanceResponse
path := c.API.Endpoints.URL + 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.API.Endpoints.URL + 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.API.Endpoints.URL + 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.API.Endpoints.URL + 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.API.Endpoints.URL + 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.API.Endpoints.URL + 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 := 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
return c.SendPayload(method,
path,
headers,
finalBody,
&result,
true,
false,
c.Verbose,
c.HTTPDebugging,
c.HTTPRecording)
}

View File

@@ -0,0 +1,179 @@
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", true)
if err != nil {
log.Fatal(err)
}
coinbeneConfig, err := cfg.GetExchangeConfig("Coinbene")
if err != nil {
log.Fatal(err)
}
coinbeneConfig.API.AuthenticatedWebsocketSupport = true
coinbeneConfig.API.AuthenticatedSupport = true
coinbeneConfig.API.Credentials.Secret = testAPISecret
coinbeneConfig.API.Credentials.Key = testAPIKey
c.Setup(coinbeneConfig)
os.Exit(m.Run())
}
func areTestAPIKeysSet() bool {
return c.AllowAuthenticatedRequest()
}
func TestGetTicker(t *testing.T) {
t.Parallel()
_, err := c.GetTicker(btcusdt)
if err != nil {
t.Error(err)
}
}
func TestGetOrderbook(t *testing.T) {
t.Parallel()
_, err := c.GetOrderbook(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,353 @@
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/common/crypto"
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"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
pairs := c.GetEnabledPairs(asset.PerpetualSwap)
for x := range channels {
for y := range pairs {
pairs[y].Delimiter = ""
subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{
Channel: fmt.Sprintf(channels[x], pairs[y]),
Currency: pairs[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{
Volume: ticker.Data[x].Volume24h,
Last: ticker.Data[x].LastPrice,
High: ticker.Data[x].High24h,
Low: ticker.Data[x].Low24h,
Pair: currency.NewPairFromFormattedPairs(ticker.Data[x].Symbol,
c.GetEnabledPairs(asset.PerpetualSwap),
c.GetPairFormat(asset.PerpetualSwap, true)),
Exchange: c.Name,
AssetType: asset.PerpetualSwap,
}
}
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
}
p := strings.Replace(tradeList.Topic, "tradeList.", "", 1)
c.Websocket.DataHandler <- wshandler.TradeData{
CurrencyPair: currency.NewPairFromFormattedPairs(p,
c.GetEnabledPairs(asset.PerpetualSwap),
c.GetPairFormat(asset.PerpetualSwap, true)),
Timestamp: t,
Price: price,
Amount: amount,
Exchange: c.Name,
AssetType: asset.PerpetualSwap,
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
}
p := strings.Replace(orderBook.Topic, "tradeList.", "", 1)
cp := currency.NewPairFromFormattedPairs(p,
c.GetEnabledPairs(asset.PerpetualSwap),
c.GetPairFormat(asset.PerpetualSwap, true))
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 = asset.PerpetualSwap
newOB.Pair = cp
newOB.ExchangeName = c.Name
err = c.Websocket.Orderbook.LoadSnapshot(&newOB)
if err != nil {
c.Websocket.DataHandler <- err
continue
}
c.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: newOB.Pair,
Asset: asset.PerpetualSwap,
Exchange: c.Name,
}
} else if orderBook.Action == "update" {
newOB := wsorderbook.WebsocketOrderbookUpdate{
Asks: asks,
Bids: bids,
Asset: asset.PerpetualSwap,
Pair: cp,
UpdateID: orderBook.Version,
}
err = c.Websocket.Orderbook.Update(&newOB)
if err != nil {
c.Websocket.DataHandler <- err
continue
}
c.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: newOB.Pair,
Asset: asset.PerpetualSwap,
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)
}
p := currency.NewPairFromFormattedPairs(kline.Data[0][0].(string),
c.GetEnabledPairs(asset.PerpetualSwap),
c.GetPairFormat(asset.PerpetualSwap, true))
c.Websocket.DataHandler <- wshandler.KlineData{
Timestamp: time.Unix(int64(kline.Data[0][1].(float64)), 0),
Pair: p,
AssetType: asset.PerpetualSwap,
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 := crypto.GetHMAC(crypto.HashSHA256,
[]byte(signMsg),
[]byte(c.API.Credentials.Secret))
sign := crypto.HexEncodeToString(tempSign)
sub.Operation = "login"
sub.Arguments = []string{c.API.Credentials.Key, expTime, sign}
return c.WebsocketConn.SendMessage(sub)
}

View File

@@ -0,0 +1,620 @@
package coinbene
import (
"fmt"
"strconv"
"sync"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"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"
)
// GetDefaultConfig returns a default exchange config
func (c *Coinbene) GetDefaultConfig() (*config.ExchangeConfig, error) {
c.SetDefaults()
exchCfg := new(config.ExchangeConfig)
exchCfg.Name = c.Name
exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout
exchCfg.BaseCurrencies = c.BaseCurrencies
err := c.SetupDefaults(exchCfg)
if err != nil {
return nil, err
}
if c.Features.Supports.RESTCapabilities.AutoPairUpdates {
err = c.UpdateTradablePairs(true)
if err != nil {
return nil, err
}
}
return exchCfg, nil
}
// SetDefaults sets the basic defaults for Coinbene
func (c *Coinbene) SetDefaults() {
c.Name = "Coinbene"
c.Enabled = true
c.Verbose = true
c.API.CredentialsValidator.RequiresKey = true
c.API.CredentialsValidator.RequiresSecret = true
c.CurrencyPairs = currency.PairsManager{
AssetTypes: asset.Items{
asset.Spot,
},
UseGlobalFormat: true,
RequestFormat: &currency.PairFormat{
Uppercase: true,
Delimiter: "/",
},
ConfigFormat: &currency.PairFormat{
Uppercase: true,
Delimiter: "/",
},
}
c.Features = exchange.Features{
Supports: exchange.FeaturesSupported{
REST: true,
Websocket: false, // Purposely disabled until SWAP is supported
RESTCapabilities: protocol.Features{
TickerFetching: true,
TradeFetching: true,
OrderbookFetching: true,
AccountBalance: true,
AutoPairUpdates: true,
GetOrder: true,
GetOrders: true,
CancelOrder: true,
CancelOrders: true,
SubmitOrder: true,
TradeFee: true,
},
WebsocketCapabilities: protocol.Features{
TickerFetching: true,
AccountBalance: true,
AccountInfo: true,
OrderbookFetching: true,
TradeFetching: true,
KlineFetching: true,
Subscribe: true,
Unsubscribe: true,
AuthenticatedEndpoints: true,
},
WithdrawPermissions: exchange.NoFiatWithdrawals |
exchange.WithdrawCryptoViaWebsiteOnly,
},
Enabled: exchange.FeaturesEnabled{
AutoPairUpdates: true,
},
}
c.Requester = request.New(c.Name,
request.NewRateLimit(time.Minute, authRateLimit),
request.NewRateLimit(time.Second, unauthRateLimit),
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
c.API.Endpoints.URLDefault = coinbeneAPIURL
c.API.Endpoints.URL = c.API.Endpoints.URLDefault
c.API.Endpoints.WebsocketURL = coinbeneWsURL
c.Websocket = wshandler.New()
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) error {
if !exch.Enabled {
c.SetEnabled(false)
return nil
}
err := c.SetupDefaults(exch)
if err != nil {
return err
}
// TO-DO: Remove this once SWAP is supported
if exch.Features.Enabled.Websocket {
log.Warnf(log.ExchangeSys,
"%s websocket only supports SWAP which GoCryptoTrader currently "+
"does not. Disabling.\n",
c.Name)
exch.Features.Enabled.Websocket = false
}
err = c.Websocket.Setup(
&wshandler.WebsocketSetup{
Enabled: exch.Features.Enabled.Websocket,
Verbose: exch.Verbose,
AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport,
WebsocketTimeout: exch.WebsocketTrafficTimeout,
DefaultURL: coinbeneWsURL,
ExchangeName: exch.Name,
RunningURL: exch.API.Endpoints.WebsocketURL,
Connector: c.WsConnect,
Subscriber: c.Subscribe,
UnSubscriber: c.Unsubscribe,
})
if err != nil {
return 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)
return nil
}
// 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(log.ExchangeSys,
"%s Websocket: %s. (url: %s).\n",
c.Name,
common.IsEnabled(c.Websocket.IsEnabled()),
c.Websocket.GetWebsocketURL(),
)
c.PrintEnabledPairs()
}
if !c.GetEnabledFeatures().AutoPairUpdates {
return
}
err := c.UpdateTradablePairs(false)
if err != nil {
log.Errorf(log.ExchangeSys,
"%s Failed to update tradable pairs. Error: %s",
c.Name,
err)
}
}
// FetchTradablePairs returns a list of exchange tradable pairs
func (c *Coinbene) FetchTradablePairs(a asset.Item) ([]string, error) {
pairs, err := c.GetAllPairs()
if err != nil {
return nil, err
}
var currencies []string
for x := range pairs.Data {
currencies = append(currencies, pairs.Data[x].Symbol)
}
return currencies, nil
}
// UpdateTradablePairs updates the exchanges available pairs and stores
// them
func (c *Coinbene) UpdateTradablePairs(forceUpdate bool) error {
pairs, err := c.FetchTradablePairs(asset.Spot)
if err != nil {
return err
}
return c.UpdatePairs(currency.NewPairsFromStrings(pairs),
asset.Spot,
false,
forceUpdate)
}
// UpdateTicker updates and returns the ticker for a currency pair
func (c *Coinbene) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) {
var resp ticker.Price
allPairs := c.GetEnabledPairs(assetType)
for x := range allPairs {
tempResp, err := c.GetTicker(c.FormatExchangeCurrency(allPairs[x],
assetType).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)
}
// FetchTicker returns the ticker for a currency pair
func (c *Coinbene) FetchTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) {
tickerNew, err := ticker.GetTicker(c.Name, p, assetType)
if err != nil {
return c.UpdateTicker(p, assetType)
}
return tickerNew, nil
}
// FetchOrderbook returns orderbook base on the currency pair
func (c *Coinbene) FetchOrderbook(currency currency.Pair, assetType asset.Item) (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 asset.Item) (orderbook.Base, error) {
var resp orderbook.Base
tempResp, err := c.GetOrderbook(
c.FormatExchangeCurrency(p, assetType).String(),
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 asset.Item) ([]exchange.TradeHistory, error) {
return nil, common.ErrFunctionNotSupported
}
// SubmitOrder submits a new order
func (c *Coinbene) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) {
var resp order.SubmitResponse
if err := s.Validate(); err != nil {
return resp, err
}
if s.OrderSide != order.Buy && s.OrderSide != order.Sell {
return resp,
fmt.Errorf("%s orderside is not supported by this exchange",
s.OrderSide)
}
if s.OrderType != order.Limit {
return resp, fmt.Errorf("only limit order is supported by this exchange")
}
tempResp, err := c.PlaceOrder(s.Price,
s.Amount,
c.FormatExchangeCurrency(s.Pair, asset.Spot).String(),
s.OrderType.String(),
s.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 *order.Modify) (string, error) {
return "", common.ErrFunctionNotSupported
}
// CancelOrder cancels an order by its corresponding ID number
func (c *Coinbene) CancelOrder(order *order.Cancel) error {
_, err := c.RemoveOrder(order.OrderID)
return err
}
// CancelAllOrders cancels all orders associated with a currency pair
func (c *Coinbene) CancelAllOrders(orderCancellation *order.Cancel) (order.CancelAllResponse, error) {
var resp order.CancelAllResponse
tempMap := make(map[string]string)
orders, err := c.FetchOpenOrders(
c.FormatExchangeCurrency(orderCancellation.CurrencyPair,
asset.Spot).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.Status = tempMap
return resp, nil
}
// GetOrderInfo returns information on a current open order
func (c *Coinbene) GetOrderInfo(orderID string) (order.Detail, error) {
var resp order.Detail
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.CryptoWithdrawRequest) (string, error) {
return "", common.ErrFunctionNotSupported
}
// WithdrawFiatFunds returns a withdrawal ID when a withdrawal is
// submitted
func (c *Coinbene) WithdrawFiatFunds(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) {
return "", common.ErrFunctionNotSupported
}
// WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a withdrawal is
// submitted
func (c *Coinbene) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.FiatWithdrawRequest) (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 *order.GetOrdersRequest) ([]order.Detail, error) {
var resp []order.Detail
var tempResp order.Detail
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(
c.FormatExchangeCurrency(
getOrdersRequest.Currencies[x],
asset.Spot).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 = order.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 *order.GetOrdersRequest) ([]order.Detail, error) {
var resp []order.Detail
var tempResp order.Detail
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(
c.FormatExchangeCurrency(
getOrdersRequest.Currencies[x],
asset.Spot).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 = order.Buy
if tempData.Data[y].OrderType == sell {
tempResp.OrderSide = order.Sell
}
t, err = time.Parse(time.RFC3339, tempData.Data[y].OrderTime)
if err != nil {
return resp, err
}
tempResp.OrderDate = t
tempResp.Status = order.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(
c.FormatExchangeCurrency(
feeBuilder.Pair, asset.Spot).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

@@ -623,11 +623,25 @@ func (e *Base) SetAPIURL() error {
if e.Config.API.Endpoints.URL == "" || e.Config.API.Endpoints.URLSecondary == "" {
return fmt.Errorf("exchange %s: SetAPIURL error. URL vals are empty", e.Name)
}
checkInsecureEndpoint := func(endpoint string) {
if !strings.Contains(endpoint, "https") {
return
}
log.Warnf(log.ExchangeSys,
"%s is using HTTP instead of HTTPS [%s] for API functionality, an"+
" attacker could eavesdrop on this connection. Use at your"+
" own risk.",
e.Name, endpoint)
}
if e.Config.API.Endpoints.URL != config.APIURLNonDefaultMessage {
e.API.Endpoints.URL = e.Config.API.Endpoints.URL
checkInsecureEndpoint(e.API.Endpoints.URL)
}
if e.Config.API.Endpoints.URLSecondary != config.APIURLNonDefaultMessage {
e.API.Endpoints.URLSecondary = e.Config.API.Endpoints.URLSecondary
checkInsecureEndpoint(e.API.Endpoints.URLSecondary)
}
return nil
}

View File

@@ -1176,6 +1176,13 @@ func TestSetAPIURL(t *testing.T) {
if tester.GetAPIURLSecondaryDefault() != testURLSecondaryDefault {
t.Error("incorrect return URL")
}
tester.Config.API.Endpoints.URL = "http://insecureino.com"
tester.Config.API.Endpoints.URLSecondary = tester.Config.API.Endpoints.URL
err = tester.SetAPIURL()
if err != nil {
t.Error(err)
}
}
func BenchmarkSetAPIURL(b *testing.B) {

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": [