diff --git a/CONTRIBUTORS b/CONTRIBUTORS index f192ce33..3f7eeb20 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -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 \ No newline at end of file diff --git a/README.md b/README.md index 86708b4c..76104bbc 100644 --- a/README.md +++ b/README.md @@ -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 | \ No newline at end of file +| zeldrinn | https://github.com/zeldrinn | 1 | + + diff --git a/config/config_test.go b/config/config_test.go index 24a48434..c5f59b18 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -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) { diff --git a/config_example.json b/config_example.json index 2cb350a9..17d4a32c 100644 --- a/config_example.json +++ b/config_example.json @@ -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, diff --git a/exchange.go b/exchange.go index 1d36f001..053e572f 100644 --- a/exchange.go +++ b/exchange.go @@ -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": diff --git a/exchanges/bitfinex/bitfinex_websocket.go b/exchanges/bitfinex/bitfinex_websocket.go index 1e173d1f..e1138267 100644 --- a/exchanges/bitfinex/bitfinex_websocket.go +++ b/exchanges/bitfinex/bitfinex_websocket.go @@ -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 { diff --git a/exchanges/coinbene/README.md b/exchanges/coinbene/README.md new file mode 100644 index 00000000..44f6ef31 --- /dev/null +++ b/exchanges/coinbene/README.md @@ -0,0 +1,141 @@ +# GoCryptoTrader package Coinbene + + + + +[![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 + + + +If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: + +***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** + diff --git a/exchanges/coinbene/coinbene.go b/exchanges/coinbene/coinbene.go new file mode 100644 index 00000000..b1909785 --- /dev/null +++ b/exchanges/coinbene/coinbene.go @@ -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) +} diff --git a/exchanges/coinbene/coinbene_test.go b/exchanges/coinbene/coinbene_test.go new file mode 100644 index 00000000..34e38a98 --- /dev/null +++ b/exchanges/coinbene/coinbene_test.go @@ -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) + } +} diff --git a/exchanges/coinbene/coinbene_types.go b/exchanges/coinbene/coinbene_types.go new file mode 100644 index 00000000..04dc583c --- /dev/null +++ b/exchanges/coinbene/coinbene_types.go @@ -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"` +} diff --git a/exchanges/coinbene/coinbene_websocket.go b/exchanges/coinbene/coinbene_websocket.go new file mode 100644 index 00000000..7f83df30 --- /dev/null +++ b/exchanges/coinbene/coinbene_websocket.go @@ -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) +} diff --git a/exchanges/coinbene/coinbene_wrapper.go b/exchanges/coinbene/coinbene_wrapper.go new file mode 100644 index 00000000..90fd1c92 --- /dev/null +++ b/exchanges/coinbene/coinbene_wrapper.go @@ -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() +} diff --git a/exchanges/exchange.go b/exchanges/exchange.go index 19e34fd3..e6225344 100644 --- a/exchanges/exchange.go +++ b/exchanges/exchange.go @@ -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 } diff --git a/exchanges/orderbook/orderbook.go b/exchanges/orderbook/orderbook.go index d8393e2f..572c98e5 100644 --- a/exchanges/orderbook/orderbook.go +++ b/exchanges/orderbook/orderbook.go @@ -17,6 +17,7 @@ const ( errQuoteCurrencyNotFound = "orderbook quote currency not found" Spot = "SPOT" + Swap = "SWAP" ) // Vars for the orderbook package diff --git a/testdata/configtest.json b/testdata/configtest.json index 0677a111..6e33885b 100644 --- a/testdata/configtest.json +++ b/testdata/configtest.json @@ -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": [ diff --git a/tools/documentation/documentation.go b/tools/documentation/documentation.go index db794179..707460d3 100644 --- a/tools/documentation/documentation.go +++ b/tools/documentation/documentation.go @@ -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) diff --git a/tools/documentation/exchanges_templates/coinbene.tmpl b/tools/documentation/exchanges_templates/coinbene.tmpl new file mode 100644 index 00000000..f9de7d4c --- /dev/null +++ b/tools/documentation/exchanges_templates/coinbene.tmpl @@ -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}}