mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 23:16:45 +00:00
Initial overhaul of websocket connection and feeds (#189)
* Initial overhaul of websocket connection and feeds * Added proxy support * Piped to routines.go * Added new websocket file in exchanges Refactored orderbook handling into exchange_websocket.go Added better error responses for binance_websocket.go General clean for binance_websocket.go * General fixes - bitfinex_websocket.go Refactored orderbook cache code - bitfinex_websocket.go Removed fatal error with unhandled type - routines.go * Added general improvements to bitmex_websocket.go Refactored orderbook handling to exchange_websocket.go Added variable in Item struct in orderbook.go for looking up orders by ID * Fix issue when routines are blocked due to Data Handler not started Updated traffic handler General fixes for bitstamp_websocket.go * General fixes for coinbasepro_websocket.go * General fixes for coinut_websocket.go Fixed error return in exchange_websocket.go * Removed comments in coinut_wrapper.go Refactor orderbook logic from hitbtc_websocket.go to exchange_websocket.go * General fixes * Removed comments General fixes * Updated routines.go * After rebase fix * Fixed update config pairs in okcoin.go * fixed config currency issue in okcoin.go for okcoin China * exchange_websocket.go *Removed unused const dec *Removed state change routine *Improved trafficMonitor routine *Increased verbosity for error returns *Removed uneeded mutex locks exchange_websocket_test.go *Added new tests for websocket and orderbook updating routines.go *Removed string cased * Fixed race conditions on sync.waitgroup in exchanges_websocket.go * Changes variable name in config.go * Removes unnecessary comment * Removes indefinite lock on error return * Removes unnecessary comment * Adds support for BTCC websocket Drops support for BTCC REST * Rewords comment in exchange_websocket.go Moves types to poloniex_types.go * Moves types to coinut_types.go * Removes uneeded range for accessing array variables for coinbase_websocket.go Removes comments in coinut_types.go * Adds verbosity flag to GCT Suppresses verbose output from routines.go * Fixes setting proxy for REST and Websocket per exchange Upgrades error handling Drops unused *url.Url variable in exchange type * Adds test for setting proxy * Fixes bug that closes connection due to incorrect timeout time through a proxy connection * Clarify verbose flag message
This commit is contained in:
committed by
Adrian Gallagher
parent
7315e6604c
commit
d3c2800fe0
@@ -56,7 +56,8 @@ const (
|
||||
WarningExchangeAuthAPIDefaultOrEmptyValues = "WARNING -- Exchange %s: Authenticated API support disabled due to default/empty APIKey/Secret/ClientID values."
|
||||
WarningCurrencyExchangeProvider = "WARNING -- Currency exchange provider invalid valid. Reset to Fixer."
|
||||
WarningPairsLastUpdatedThresholdExceeded = "WARNING -- Exchange %s: Last manual update of available currency pairs has exceeded %d days. Manual update required!"
|
||||
APIURLDefaultMessage = "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API"
|
||||
APIURLNonDefaultMessage = "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API"
|
||||
WebsocketURLNonDefaultMessage = "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API"
|
||||
)
|
||||
|
||||
// Variables here are used for configuration
|
||||
@@ -129,6 +130,8 @@ type ExchangeConfig struct {
|
||||
APIAuthPEMKey string `json:"apiAuthPemKey,omitempty"`
|
||||
APIURL string `json:"apiUrl"`
|
||||
APIURLSecondary string `json:"apiUrlSecondary"`
|
||||
ProxyAddress string `json:"proxyAddress"`
|
||||
WebsocketURL string `json:"websocketUrl"`
|
||||
ClientID string `json:"clientId,omitempty"`
|
||||
AvailablePairs string `json:"availablePairs"`
|
||||
EnabledPairs string `json:"enabledPairs"`
|
||||
@@ -672,17 +675,23 @@ func (c *Config) CheckExchangeConfigValues() error {
|
||||
c.Exchanges[i].Name = "CoinbasePro"
|
||||
}
|
||||
|
||||
if exch.APIURL != APIURLDefaultMessage {
|
||||
if exch.APIURL == "" {
|
||||
// Set default if nothing set
|
||||
c.Exchanges[i].APIURL = APIURLDefaultMessage
|
||||
if exch.WebsocketURL != WebsocketURLNonDefaultMessage {
|
||||
if exch.WebsocketURL == "" {
|
||||
c.Exchanges[i].WebsocketURL = WebsocketURLNonDefaultMessage
|
||||
}
|
||||
}
|
||||
|
||||
if exch.APIURLSecondary != APIURLDefaultMessage {
|
||||
if exch.APIURL != APIURLNonDefaultMessage {
|
||||
if exch.APIURL == "" {
|
||||
// Set default if nothing set
|
||||
c.Exchanges[i].APIURL = APIURLNonDefaultMessage
|
||||
}
|
||||
}
|
||||
|
||||
if exch.APIURLSecondary != APIURLNonDefaultMessage {
|
||||
if exch.APIURLSecondary == "" {
|
||||
// Set default if nothing set
|
||||
c.Exchanges[i].APIURLSecondary = APIURLDefaultMessage
|
||||
c.Exchanges[i].APIURLSecondary = APIURLNonDefaultMessage
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,10 @@ func (a *Alphapoint) SetDefaults() {
|
||||
a.AssetTypes = []string{ticker.Spot}
|
||||
a.SupportsAutoPairUpdating = false
|
||||
a.SupportsRESTTickerBatching = false
|
||||
a.Requester = request.New(a.Name, request.NewRateLimit(time.Minute*10, alphapointAuthRate), request.NewRateLimit(time.Minute*10, alphapointUnauthRate), common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
|
||||
a.Requester = request.New(a.Name,
|
||||
request.NewRateLimit(time.Minute*10, alphapointAuthRate),
|
||||
request.NewRateLimit(time.Minute*10, alphapointUnauthRate),
|
||||
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
|
||||
}
|
||||
|
||||
// GetTicker returns current ticker information from Alphapoint for a selected
|
||||
|
||||
@@ -14,7 +14,7 @@ const (
|
||||
|
||||
// WebsocketClient starts a new webstocket connection
|
||||
func (a *Alphapoint) WebsocketClient() {
|
||||
for a.Enabled && a.Websocket {
|
||||
for a.Enabled {
|
||||
var Dialer websocket.Dialer
|
||||
var err error
|
||||
a.WebsocketConn, _, err = Dialer.Dial(a.WebsocketURL, http.Header{})
|
||||
@@ -35,7 +35,7 @@ func (a *Alphapoint) WebsocketClient() {
|
||||
return
|
||||
}
|
||||
|
||||
for a.Enabled && a.Websocket {
|
||||
for a.Enabled {
|
||||
msgType, resp, err := a.WebsocketConn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
|
||||
@@ -171,3 +171,8 @@ func (a *Alphapoint) WithdrawCryptoExchangeFunds(address string, cryptocurrency
|
||||
func (a *Alphapoint) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount float64) (string, error) {
|
||||
return "", errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// GetWebsocket returns a pointer to the exchange websocket
|
||||
func (a *Alphapoint) GetWebsocket() (*exchange.Websocket, error) {
|
||||
return nil, errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@ func (a *ANX) SetDefaults() {
|
||||
a.TakerFee = 0.6
|
||||
a.MakerFee = 0.3
|
||||
a.Verbose = false
|
||||
a.Websocket = false
|
||||
a.RESTPollingDelay = 10
|
||||
a.RequestCurrencyPairFormat.Delimiter = ""
|
||||
a.RequestCurrencyPairFormat.Uppercase = true
|
||||
@@ -64,6 +63,7 @@ func (a *ANX) SetDefaults() {
|
||||
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
|
||||
a.APIUrlDefault = anxAPIURL
|
||||
a.APIUrl = a.APIUrlDefault
|
||||
a.WebsocketInit()
|
||||
}
|
||||
|
||||
//Setup is run on startup to setup exchange with config values
|
||||
@@ -78,7 +78,6 @@ func (a *ANX) Setup(exch config.ExchangeConfig) {
|
||||
a.SetHTTPClientUserAgent(exch.HTTPUserAgent)
|
||||
a.RESTPollingDelay = exch.RESTPollingDelay
|
||||
a.Verbose = exch.Verbose
|
||||
a.Websocket = exch.Websocket
|
||||
a.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",")
|
||||
a.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",")
|
||||
a.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",")
|
||||
@@ -98,6 +97,10 @@ func (a *ANX) Setup(exch config.ExchangeConfig) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = a.SetClientProxyAddress(exch.ProxyAddress)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ func TestSetDefaults(t *testing.T) {
|
||||
if anx.Verbose != false {
|
||||
t.Error("Test Failed - ANX SetDefaults() incorrect values set")
|
||||
}
|
||||
if anx.Websocket != false {
|
||||
if anx.Websocket.IsEnabled() != false {
|
||||
t.Error("Test Failed - ANX SetDefaults() incorrect values set")
|
||||
}
|
||||
if anx.RESTPollingDelay != 10 {
|
||||
@@ -61,7 +61,7 @@ func TestSetup(t *testing.T) {
|
||||
if anx.Verbose != false {
|
||||
t.Error("Test Failed - ANX Setup() incorrect values set")
|
||||
}
|
||||
if anx.Websocket != false {
|
||||
if anx.Websocket.IsEnabled() != false {
|
||||
t.Error("Test Failed - ANX Setup() incorrect values set")
|
||||
}
|
||||
if len(anx.BaseCurrencies) <= 0 {
|
||||
|
||||
@@ -244,3 +244,8 @@ func (a *ANX) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount float
|
||||
func (a *ANX) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) {
|
||||
return "", errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// GetWebsocket returns a pointer to the exchange websocket
|
||||
func (a *ANX) GetWebsocket() (*exchange.Websocket, error) {
|
||||
return nil, errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ type Binance struct {
|
||||
exchange.Base
|
||||
WebsocketConn *websocket.Conn
|
||||
|
||||
// valid string list that a required by the exchange
|
||||
// Valid string list that is required by the exchange
|
||||
validLimits []int
|
||||
validIntervals []TimeInterval
|
||||
}
|
||||
@@ -61,7 +61,6 @@ func (b *Binance) SetDefaults() {
|
||||
b.Name = "Binance"
|
||||
b.Enabled = false
|
||||
b.Verbose = false
|
||||
b.Websocket = false
|
||||
b.RESTPollingDelay = 10
|
||||
b.RequestCurrencyPairFormat.Delimiter = ""
|
||||
b.RequestCurrencyPairFormat.Uppercase = true
|
||||
@@ -77,6 +76,7 @@ func (b *Binance) SetDefaults() {
|
||||
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
|
||||
b.APIUrlDefault = apiURL
|
||||
b.APIUrl = b.APIUrlDefault
|
||||
b.WebsocketInit()
|
||||
}
|
||||
|
||||
// Setup takes in the supplied exchange configuration details and sets params
|
||||
@@ -91,7 +91,6 @@ func (b *Binance) Setup(exch config.ExchangeConfig) {
|
||||
b.SetHTTPClientUserAgent(exch.HTTPUserAgent)
|
||||
b.RESTPollingDelay = exch.RESTPollingDelay
|
||||
b.Verbose = exch.Verbose
|
||||
b.Websocket = exch.Websocket
|
||||
b.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",")
|
||||
b.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",")
|
||||
b.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",")
|
||||
@@ -111,6 +110,18 @@ func (b *Binance) Setup(exch config.ExchangeConfig) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = b.SetClientProxyAddress(exch.ProxyAddress)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = b.WebsocketSetup(b.WSConnect,
|
||||
exch.Name,
|
||||
exch.Websocket,
|
||||
binanceDefaultWebsocketURL,
|
||||
exch.WebsocketURL)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,6 +210,8 @@ func (b *Binance) GetOrderBook(obd OrderBookDataRequestParams) (OrderBook, error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
orderbook.LastUpdateID = resp.LastUpdateID
|
||||
return orderbook, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -61,9 +61,10 @@ type OrderBookData struct {
|
||||
|
||||
// OrderBook actual structured data that can be used for orderbook
|
||||
type OrderBook struct {
|
||||
Code int
|
||||
Msg string
|
||||
Bids []struct {
|
||||
LastUpdateID int64
|
||||
Code int
|
||||
Msg string
|
||||
Bids []struct {
|
||||
Price float64
|
||||
Quantity float64
|
||||
}
|
||||
@@ -73,6 +74,24 @@ type OrderBook struct {
|
||||
}
|
||||
}
|
||||
|
||||
// DepthUpdateParams is used as an embedded type for WebsocketDepthStream
|
||||
type DepthUpdateParams []struct {
|
||||
PriceLevel float64
|
||||
Quantity float64
|
||||
ingnore []interface{}
|
||||
}
|
||||
|
||||
// WebsocketDepthStream is the difference for the update depth stream
|
||||
type WebsocketDepthStream struct {
|
||||
Event string `json:"e"`
|
||||
Timestamp int64 `json:"E"`
|
||||
Pair string `json:"s"`
|
||||
FirstUpdateID int64 `json:"U"`
|
||||
LastUpdateID int64 `json:"u"`
|
||||
UpdateBids []interface{} `json:"b"`
|
||||
UpdateAsks []interface{} `json:"a"`
|
||||
}
|
||||
|
||||
// RecentTradeRequestParams represents Klines request data.
|
||||
type RecentTradeRequestParams struct {
|
||||
Symbol string `json:"symbol"` // Required field. example LTCBTC, BTCUSDT
|
||||
|
||||
@@ -1,98 +1,358 @@
|
||||
package binance
|
||||
|
||||
import (
|
||||
"log"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/thrasher-/gocryptotrader/common"
|
||||
"github.com/thrasher-/gocryptotrader/currency/pair"
|
||||
exchange "github.com/thrasher-/gocryptotrader/exchanges"
|
||||
"github.com/thrasher-/gocryptotrader/exchanges/orderbook"
|
||||
)
|
||||
|
||||
const (
|
||||
binanceDefaultWebsocketURL = "wss://stream.binance.com:9443"
|
||||
binancePingPeriod = 20 * time.Second
|
||||
)
|
||||
|
||||
// WebsocketClient starts and handles the websocket client connection
|
||||
func (b *Binance) WebsocketClient() {
|
||||
for b.Enabled && b.Websocket {
|
||||
var Dialer websocket.Dialer
|
||||
var err error
|
||||
// myenabledPairs := strings.ToLower(strings.Replace(strings.Join(b.EnabledPairs, "@ticker/"), "-", "", -1)) + "@trade"
|
||||
var lastUpdateID map[string]int64
|
||||
var m sync.Mutex
|
||||
|
||||
myenabledPairsTicker := strings.ToLower(strings.Replace(strings.Join(b.EnabledPairs, "@ticker/"), "-", "", -1)) + "@ticker"
|
||||
myenabledPairsTrade := strings.ToLower(strings.Replace(strings.Join(b.EnabledPairs, "@trade/"), "-", "", -1)) + "@trade"
|
||||
myenabledPairsKline := strings.ToLower(strings.Replace(strings.Join(b.EnabledPairs, "@kline_1m/"), "-", "", -1)) + "@kline_1m"
|
||||
wsurl := b.WebsocketURL + "/stream?streams=" + myenabledPairsTicker + "/" + myenabledPairsTrade + "/" + myenabledPairsKline
|
||||
// SeedLocalCache seeds depth data
|
||||
func (b *Binance) SeedLocalCache(p pair.CurrencyPair) error {
|
||||
var newOrderBook orderbook.Base
|
||||
|
||||
// b.WebsocketConn, _, err = Dialer.Dial(binanceDefaultWebsocketURL+myenabledPairs, http.Header{})
|
||||
b.WebsocketConn, _, err = Dialer.Dial(wsurl, http.Header{})
|
||||
formattedPair := exchange.FormatExchangeCurrency(b.Name, p)
|
||||
|
||||
orderbookNew, err := b.GetOrderBook(
|
||||
OrderBookDataRequestParams{
|
||||
Symbol: formattedPair.String(),
|
||||
Limit: 1000,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.Lock()
|
||||
if lastUpdateID == nil {
|
||||
lastUpdateID = make(map[string]int64)
|
||||
}
|
||||
|
||||
lastUpdateID[formattedPair.String()] = orderbookNew.LastUpdateID
|
||||
m.Unlock()
|
||||
|
||||
for _, bids := range orderbookNew.Bids {
|
||||
newOrderBook.Bids = append(newOrderBook.Bids,
|
||||
orderbook.Item{Amount: bids.Quantity, Price: bids.Price})
|
||||
}
|
||||
for _, Asks := range orderbookNew.Asks {
|
||||
newOrderBook.Asks = append(newOrderBook.Asks,
|
||||
orderbook.Item{Amount: Asks.Quantity, Price: Asks.Price})
|
||||
}
|
||||
|
||||
newOrderBook.Pair = pair.NewCurrencyPairFromString(formattedPair.String())
|
||||
newOrderBook.CurrencyPair = formattedPair.String()
|
||||
newOrderBook.LastUpdated = time.Now()
|
||||
newOrderBook.AssetType = "SPOT"
|
||||
|
||||
return b.Websocket.Orderbook.LoadSnapshot(newOrderBook, b.GetName())
|
||||
}
|
||||
|
||||
// UpdateLocalCache updates and returns the most recent iteration of the orderbook
|
||||
func (b *Binance) UpdateLocalCache(ob WebsocketDepthStream) error {
|
||||
m.Lock()
|
||||
ID, ok := lastUpdateID[ob.Pair]
|
||||
if !ok {
|
||||
m.Unlock()
|
||||
return errors.New("binance_websocket.go - Unable to find lastUpdateID")
|
||||
}
|
||||
|
||||
if ob.LastUpdateID+1 <= ID || ID >= ob.LastUpdateID+1 {
|
||||
// Drop update, out of order
|
||||
m.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
lastUpdateID[ob.Pair] = ob.LastUpdateID
|
||||
m.Unlock()
|
||||
|
||||
var updateBid, updateAsk []orderbook.Item
|
||||
|
||||
for _, bidsToUpdate := range ob.UpdateBids {
|
||||
var priceToBeUpdated orderbook.Item
|
||||
for i, bids := range bidsToUpdate.([]interface{}) {
|
||||
switch i {
|
||||
case 0:
|
||||
priceToBeUpdated.Price, _ = strconv.ParseFloat(bids.(string), 64)
|
||||
case 1:
|
||||
priceToBeUpdated.Amount, _ = strconv.ParseFloat(bids.(string), 64)
|
||||
}
|
||||
}
|
||||
updateBid = append(updateBid, priceToBeUpdated)
|
||||
}
|
||||
|
||||
for _, asksToUpdate := range ob.UpdateAsks {
|
||||
var priceToBeUpdated orderbook.Item
|
||||
for i, asks := range asksToUpdate.([]interface{}) {
|
||||
switch i {
|
||||
case 0:
|
||||
priceToBeUpdated.Price, _ = strconv.ParseFloat(asks.(string), 64)
|
||||
case 1:
|
||||
priceToBeUpdated.Amount, _ = strconv.ParseFloat(asks.(string), 64)
|
||||
}
|
||||
}
|
||||
updateAsk = append(updateBid, priceToBeUpdated)
|
||||
}
|
||||
|
||||
updatedTime := time.Unix(ob.Timestamp, 0)
|
||||
currencyPair := pair.NewCurrencyPairFromString(ob.Pair)
|
||||
|
||||
return b.Websocket.Orderbook.Update(updateBid,
|
||||
updateAsk,
|
||||
currencyPair,
|
||||
updatedTime,
|
||||
b.GetName(),
|
||||
"SPOT")
|
||||
}
|
||||
|
||||
// WSConnect intiates a websocket connection
|
||||
func (b *Binance) WSConnect() error {
|
||||
if !b.Websocket.IsEnabled() || !b.IsEnabled() {
|
||||
return errors.New(exchange.WebsocketNotEnabled)
|
||||
}
|
||||
|
||||
var Dialer websocket.Dialer
|
||||
var err error
|
||||
|
||||
ticker := strings.ToLower(
|
||||
strings.Replace(
|
||||
strings.Join(b.EnabledPairs, "@ticker/"), "-", "", -1)) + "@ticker"
|
||||
trade := strings.ToLower(
|
||||
strings.Replace(
|
||||
strings.Join(b.EnabledPairs, "@trade/"), "-", "", -1)) + "@trade"
|
||||
kline := strings.ToLower(
|
||||
strings.Replace(
|
||||
strings.Join(b.EnabledPairs, "@kline_1m/"), "-", "", -1)) + "@kline_1m"
|
||||
depth := strings.ToLower(
|
||||
strings.Replace(
|
||||
strings.Join(b.EnabledPairs, "@depth/"), "-", "", -1)) + "@depth"
|
||||
|
||||
wsurl := b.Websocket.GetWebsocketURL() +
|
||||
"/stream?streams=" +
|
||||
ticker +
|
||||
"/" +
|
||||
trade +
|
||||
"/" +
|
||||
kline +
|
||||
"/" +
|
||||
depth
|
||||
|
||||
if b.Websocket.GetProxyAddress() != "" {
|
||||
url, err := url.Parse(b.Websocket.GetProxyAddress())
|
||||
if err != nil {
|
||||
log.Printf("%s Unable to connect to Websocket. Error: %s\n", b.Name, err)
|
||||
continue
|
||||
return fmt.Errorf("binance_websocket.go - Unable to connect to parse proxy address. Error: %s",
|
||||
err)
|
||||
}
|
||||
|
||||
if b.Verbose {
|
||||
log.Printf("%s Connected to Websocket.\n", b.Name)
|
||||
}
|
||||
Dialer.Proxy = http.ProxyURL(url)
|
||||
}
|
||||
|
||||
for b.Enabled && b.Websocket {
|
||||
for _, ePair := range b.GetEnabledCurrencies() {
|
||||
err := b.SeedLocalCache(ePair)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
b.WebsocketConn, _, err = Dialer.Dial(wsurl, http.Header{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("binance_websocket.go - Unable to connect to Websocket. Error: %s",
|
||||
err)
|
||||
}
|
||||
|
||||
go b.WsHandleData()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WSReadData reads from the websocket connection
|
||||
func (b *Binance) WSReadData() {
|
||||
b.Websocket.Wg.Add(1)
|
||||
|
||||
defer func() {
|
||||
err := b.WebsocketConn.Close()
|
||||
if err != nil {
|
||||
b.Websocket.DataHandler <- fmt.Errorf("binance_websocket.go - Unable to to close Websocket connection. Error: %s",
|
||||
err)
|
||||
}
|
||||
b.Websocket.Wg.Done()
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-b.Websocket.ShutdownC:
|
||||
return
|
||||
|
||||
default:
|
||||
msgType, resp, err := b.WebsocketConn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
break
|
||||
b.Websocket.DataHandler <- fmt.Errorf("binance_websocket.go - Websocket Read Data. Error: %s",
|
||||
err)
|
||||
return
|
||||
}
|
||||
|
||||
switch msgType {
|
||||
b.Websocket.TrafficAlert <- struct{}{}
|
||||
b.Websocket.Intercomm <- exchange.WebsocketResponse{Type: msgType, Raw: resp}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WsHandleData handles websocket data from WsReadData
|
||||
func (b *Binance) WsHandleData() {
|
||||
b.Websocket.Wg.Add(1)
|
||||
defer b.Websocket.Wg.Done()
|
||||
|
||||
go b.WSReadData()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-b.Websocket.ShutdownC:
|
||||
return
|
||||
|
||||
case read := <-b.Websocket.Intercomm:
|
||||
switch read.Type {
|
||||
case websocket.TextMessage:
|
||||
multiStreamData := MultiStreamData{}
|
||||
|
||||
err := common.JSONDecode(resp, &multiStreamData)
|
||||
|
||||
err := common.JSONDecode(read.Raw, &multiStreamData)
|
||||
if err != nil {
|
||||
log.Println("Could not load multi stream data.", string(resp))
|
||||
b.Websocket.DataHandler <- fmt.Errorf("binance_websocket.go - Could not load multi stream data: %s",
|
||||
string(read.Raw))
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(multiStreamData.Stream, "trade") {
|
||||
trade := TradeStream{}
|
||||
err := common.JSONDecode(multiStreamData.Data, &trade)
|
||||
|
||||
err := common.JSONDecode(multiStreamData.Data, &trade)
|
||||
if err != nil {
|
||||
log.Println("Could not convert to a TradeStream structure")
|
||||
b.Websocket.DataHandler <- fmt.Errorf("binance_websocket.go - Could not unmarshal trade data: %s",
|
||||
err)
|
||||
continue
|
||||
}
|
||||
log.Println("Trade received", trade.Symbol, trade.TimeStamp, trade.TradeID, trade.Price, trade.Quantity)
|
||||
|
||||
price, err := strconv.ParseFloat(trade.Price, 64)
|
||||
if err != nil {
|
||||
b.Websocket.DataHandler <- fmt.Errorf("binance_websocket.go - price conversion error: %s",
|
||||
err)
|
||||
continue
|
||||
}
|
||||
|
||||
amount, err := strconv.ParseFloat(trade.Quantity, 64)
|
||||
if err != nil {
|
||||
b.Websocket.DataHandler <- fmt.Errorf("binance_websocket.go - amount conversion error: %s",
|
||||
err)
|
||||
continue
|
||||
}
|
||||
|
||||
b.Websocket.DataHandler <- exchange.TradeData{
|
||||
CurrencyPair: pair.NewCurrencyPairFromString(trade.Symbol),
|
||||
Timestamp: time.Unix(0, trade.TimeStamp),
|
||||
Price: price,
|
||||
Amount: amount,
|
||||
Exchange: b.GetName(),
|
||||
AssetType: "SPOT",
|
||||
Side: trade.EventType,
|
||||
}
|
||||
continue
|
||||
|
||||
} else if strings.Contains(multiStreamData.Stream, "ticker") {
|
||||
ticker := TickerStream{}
|
||||
|
||||
err := common.JSONDecode(multiStreamData.Data, &ticker)
|
||||
if err != nil {
|
||||
log.Println("Could not convert to a TickerStream structure")
|
||||
b.Websocket.DataHandler <- fmt.Errorf("binance_websocket.go - Could not convert to a TickerStream structure %s",
|
||||
err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
log.Println("Ticker received", ticker.Symbol, ticker.EventTime, ticker.TotalTradedVolume, ticker.LastTradeID)
|
||||
var wsTicker exchange.TickerData
|
||||
|
||||
wsTicker.Timestamp = time.Unix(0, ticker.EventTime)
|
||||
wsTicker.Pair = pair.NewCurrencyPairFromString(ticker.Symbol)
|
||||
wsTicker.AssetType = "SPOT"
|
||||
wsTicker.Exchange = b.GetName()
|
||||
wsTicker.ClosePrice, _ = strconv.ParseFloat(ticker.CurrDayClose, 64)
|
||||
wsTicker.Quantity, _ = strconv.ParseFloat(ticker.TotalTradedVolume, 64)
|
||||
wsTicker.OpenPrice, _ = strconv.ParseFloat(ticker.OpenPrice, 64)
|
||||
wsTicker.HighPrice, _ = strconv.ParseFloat(ticker.HighPrice, 64)
|
||||
wsTicker.LowPrice, _ = strconv.ParseFloat(ticker.LowPrice, 64)
|
||||
|
||||
b.Websocket.DataHandler <- wsTicker
|
||||
continue
|
||||
|
||||
} else if strings.Contains(multiStreamData.Stream, "kline") {
|
||||
kline := KlineStream{}
|
||||
|
||||
err := common.JSONDecode(multiStreamData.Data, &kline)
|
||||
if err != nil {
|
||||
log.Println("Could not convert to a KlineStream structure")
|
||||
b.Websocket.DataHandler <- fmt.Errorf("binance_websocket.go - Could not convert to a KlineStream structure %s",
|
||||
err)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Println("Kline received", kline.Symbol, kline.EventTime, kline.Kline.HighPrice, kline.Kline.LowPrice)
|
||||
}
|
||||
type MsgType struct {
|
||||
MessageType string `json:"messageType"`
|
||||
var wsKline exchange.KlineData
|
||||
|
||||
wsKline.Timestamp = time.Unix(0, kline.EventTime)
|
||||
wsKline.Pair = pair.NewCurrencyPairFromString(kline.Symbol)
|
||||
wsKline.AssetType = "SPOT"
|
||||
wsKline.Exchange = b.GetName()
|
||||
wsKline.StartTime = time.Unix(0, kline.Kline.StartTime)
|
||||
wsKline.CloseTime = time.Unix(0, kline.Kline.CloseTime)
|
||||
wsKline.Interval = kline.Kline.Interval
|
||||
wsKline.OpenPrice, _ = strconv.ParseFloat(kline.Kline.OpenPrice, 64)
|
||||
wsKline.ClosePrice, _ = strconv.ParseFloat(kline.Kline.ClosePrice, 64)
|
||||
wsKline.HighPrice, _ = strconv.ParseFloat(kline.Kline.HighPrice, 64)
|
||||
wsKline.LowPrice, _ = strconv.ParseFloat(kline.Kline.LowPrice, 64)
|
||||
wsKline.Volume, _ = strconv.ParseFloat(kline.Kline.Volume, 64)
|
||||
|
||||
b.Websocket.DataHandler <- wsKline
|
||||
continue
|
||||
|
||||
} else if common.StringContains(multiStreamData.Stream, "depth") {
|
||||
depth := WebsocketDepthStream{}
|
||||
|
||||
err := common.JSONDecode(multiStreamData.Data, &depth)
|
||||
if err != nil {
|
||||
b.Websocket.DataHandler <- fmt.Errorf("binance_websocket.go - Could not convert to depthStream structure %s",
|
||||
err)
|
||||
continue
|
||||
}
|
||||
|
||||
err = b.UpdateLocalCache(depth)
|
||||
if err != nil {
|
||||
b.Websocket.DataHandler <- fmt.Errorf("binance_websocket.go - UpdateLocalCache error: %s",
|
||||
err)
|
||||
continue
|
||||
}
|
||||
|
||||
currencyPair := pair.NewCurrencyPairFromString(depth.Pair)
|
||||
|
||||
b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{
|
||||
Pair: currencyPair,
|
||||
Asset: "SPOT",
|
||||
Exchange: b.GetName(),
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
b.WebsocketConn.Close()
|
||||
log.Printf("%s Websocket client disconnected.", b.Name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,15 +24,11 @@ func (b *Binance) Start(wg *sync.WaitGroup) {
|
||||
// Run implements the OKEX wrapper
|
||||
func (b *Binance) Run() {
|
||||
if b.Verbose {
|
||||
log.Printf("%s Websocket: %s. (url: %s).\n", b.GetName(), common.IsEnabled(b.Websocket), b.WebsocketURL)
|
||||
log.Printf("%s Websocket: %s. (url: %s).\n", b.GetName(), common.IsEnabled(b.Websocket.IsEnabled()), b.Websocket.GetWebsocketURL())
|
||||
log.Printf("%s polling delay: %ds.\n", b.GetName(), b.RESTPollingDelay)
|
||||
log.Printf("%s %d currencies enabled: %s.\n", b.GetName(), len(b.EnabledPairs), b.EnabledPairs)
|
||||
}
|
||||
|
||||
if b.Websocket {
|
||||
go b.WebsocketClient()
|
||||
}
|
||||
|
||||
symbols, err := b.GetExchangeValidCurrencyPairs()
|
||||
if err != nil {
|
||||
log.Printf("%s Failed to get exchange info.\n", b.GetName())
|
||||
@@ -193,3 +189,8 @@ func (b *Binance) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount f
|
||||
func (b *Binance) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) {
|
||||
return "", errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// GetWebsocket returns a pointer to the exchange websocket
|
||||
func (b *Binance) GetWebsocket() (*exchange.Websocket, error) {
|
||||
return b.Websocket, nil
|
||||
}
|
||||
|
||||
@@ -91,7 +91,6 @@ func (b *Bitfinex) SetDefaults() {
|
||||
b.Name = "Bitfinex"
|
||||
b.Enabled = false
|
||||
b.Verbose = false
|
||||
b.Websocket = false
|
||||
b.RESTPollingDelay = 10
|
||||
b.WebsocketSubdChannels = make(map[int]WebsocketChanInfo)
|
||||
b.RequestCurrencyPairFormat.Delimiter = ""
|
||||
@@ -107,6 +106,7 @@ func (b *Bitfinex) SetDefaults() {
|
||||
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
|
||||
b.APIUrlDefault = bitfinexAPIURLBase
|
||||
b.APIUrl = b.APIUrlDefault
|
||||
b.WebsocketInit()
|
||||
}
|
||||
|
||||
// Setup takes in the supplied exchange configuration details and sets params
|
||||
@@ -121,7 +121,7 @@ func (b *Bitfinex) Setup(exch config.ExchangeConfig) {
|
||||
b.SetHTTPClientUserAgent(exch.HTTPUserAgent)
|
||||
b.RESTPollingDelay = exch.RESTPollingDelay
|
||||
b.Verbose = exch.Verbose
|
||||
b.Websocket = exch.Websocket
|
||||
b.Websocket.SetEnabled(exch.Websocket)
|
||||
b.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",")
|
||||
b.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",")
|
||||
b.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",")
|
||||
@@ -141,6 +141,18 @@ func (b *Bitfinex) Setup(exch config.ExchangeConfig) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = b.SetClientProxyAddress(exch.ProxyAddress)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = b.WebsocketSetup(b.WsConnect,
|
||||
exch.Name,
|
||||
exch.Websocket,
|
||||
bitfinexWebsocket,
|
||||
exch.WebsocketURL)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ func TestSetup(t *testing.T) {
|
||||
b.Setup(bfxConfig)
|
||||
|
||||
if !b.Enabled || b.AuthenticatedAPISupport || b.RESTPollingDelay != time.Duration(10) ||
|
||||
b.Verbose || b.Websocket || len(b.BaseCurrencies) < 1 ||
|
||||
b.Verbose || b.Websocket.IsEnabled() || len(b.BaseCurrencies) < 1 ||
|
||||
len(b.AvailablePairs) < 1 || len(b.EnabledPairs) < 1 {
|
||||
t.Error("Test Failed - Bitfinex Setup values not set correctly")
|
||||
}
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
package bitfinex
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/thrasher-/gocryptotrader/common"
|
||||
"github.com/thrasher-/gocryptotrader/currency/pair"
|
||||
"github.com/thrasher-/gocryptotrader/exchanges"
|
||||
"github.com/thrasher-/gocryptotrader/exchanges/orderbook"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -36,26 +42,35 @@ const (
|
||||
bitfinexWebsocketUnknownChannel = "10302"
|
||||
)
|
||||
|
||||
// WebsocketPingHandler sends a ping request to the websocket server
|
||||
func (b *Bitfinex) WebsocketPingHandler() error {
|
||||
// WebsocketHandshake defines the communication between the websocket API for
|
||||
// initial connection
|
||||
type WebsocketHandshake struct {
|
||||
Event string `json:"event"`
|
||||
Code int64 `json:"code"`
|
||||
Version float64 `json:"version"`
|
||||
}
|
||||
|
||||
var pongReceive chan struct{}
|
||||
|
||||
// WsPingHandler sends a ping request to the websocket server
|
||||
func (b *Bitfinex) WsPingHandler() error {
|
||||
request := make(map[string]string)
|
||||
request["event"] = "ping"
|
||||
|
||||
return b.WebsocketSend(request)
|
||||
return b.WsSend(request)
|
||||
}
|
||||
|
||||
// WebsocketSend sends data to the websocket server
|
||||
func (b *Bitfinex) WebsocketSend(data interface{}) error {
|
||||
// WsSend sends data to the websocket server
|
||||
func (b *Bitfinex) WsSend(data interface{}) error {
|
||||
json, err := common.JSONEncode(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.WebsocketConn.WriteMessage(websocket.TextMessage, json)
|
||||
}
|
||||
|
||||
// WebsocketSubscribe subscribes to the websocket channel
|
||||
func (b *Bitfinex) WebsocketSubscribe(channel string, params map[string]string) error {
|
||||
// WsSubscribe subscribes to the websocket channel
|
||||
func (b *Bitfinex) WsSubscribe(channel string, params map[string]string) error {
|
||||
request := make(map[string]string)
|
||||
request["event"] = "subscribe"
|
||||
request["channel"] = channel
|
||||
@@ -65,114 +80,169 @@ func (b *Bitfinex) WebsocketSubscribe(channel string, params map[string]string)
|
||||
request[k] = v
|
||||
}
|
||||
}
|
||||
return b.WebsocketSend(request)
|
||||
return b.WsSend(request)
|
||||
}
|
||||
|
||||
// WebsocketSendAuth sends a autheticated event payload
|
||||
func (b *Bitfinex) WebsocketSendAuth() error {
|
||||
// WsSendAuth sends a autheticated event payload
|
||||
func (b *Bitfinex) WsSendAuth() error {
|
||||
request := make(map[string]interface{})
|
||||
payload := "AUTH" + strconv.FormatInt(time.Now().UnixNano(), 10)[:13]
|
||||
request["event"] = "auth"
|
||||
request["apiKey"] = b.APIKey
|
||||
request["authSig"] = common.HexEncodeToString(common.GetHMAC(common.HashSHA512_384, []byte(payload), []byte(b.APISecret)))
|
||||
|
||||
request["authSig"] = common.HexEncodeToString(
|
||||
common.GetHMAC(
|
||||
common.HashSHA512_384,
|
||||
[]byte(payload),
|
||||
[]byte(b.APISecret)))
|
||||
|
||||
request["authPayload"] = payload
|
||||
|
||||
return b.WebsocketSend(request)
|
||||
return b.WsSend(request)
|
||||
}
|
||||
|
||||
// WebsocketSendUnauth sends an unauthenticated payload
|
||||
func (b *Bitfinex) WebsocketSendUnauth() error {
|
||||
// WsSendUnauth sends an unauthenticated payload
|
||||
func (b *Bitfinex) WsSendUnauth() error {
|
||||
request := make(map[string]string)
|
||||
request["event"] = "unauth"
|
||||
|
||||
return b.WebsocketSend(request)
|
||||
return b.WsSend(request)
|
||||
}
|
||||
|
||||
// WebsocketAddSubscriptionChannel adds a new subscription channel to the
|
||||
// WsAddSubscriptionChannel adds a new subscription channel to the
|
||||
// WebsocketSubdChannels map in bitfinex.go (Bitfinex struct)
|
||||
func (b *Bitfinex) WebsocketAddSubscriptionChannel(chanID int, channel, pair string) {
|
||||
func (b *Bitfinex) WsAddSubscriptionChannel(chanID int, channel, pair string) {
|
||||
chanInfo := WebsocketChanInfo{Pair: pair, Channel: channel}
|
||||
b.WebsocketSubdChannels[chanID] = chanInfo
|
||||
|
||||
if b.Verbose {
|
||||
log.Printf("%s Subscribed to Channel: %s Pair: %s ChannelID: %d\n", b.GetName(), channel, pair, chanID)
|
||||
log.Printf("%s Subscribed to Channel: %s Pair: %s ChannelID: %d\n",
|
||||
b.GetName(),
|
||||
channel,
|
||||
pair,
|
||||
chanID)
|
||||
}
|
||||
}
|
||||
|
||||
// WebsocketClient makes a connection with the websocket server
|
||||
func (b *Bitfinex) WebsocketClient() {
|
||||
channels := []string{"book", "trades", "ticker"}
|
||||
for b.Enabled && b.Websocket {
|
||||
var Dialer websocket.Dialer
|
||||
var err error
|
||||
b.WebsocketConn, _, err = Dialer.Dial(bitfinexWebsocket, http.Header{})
|
||||
// WsConnect starts a new websocket connection
|
||||
func (b *Bitfinex) WsConnect() error {
|
||||
if !b.Websocket.IsEnabled() || !b.IsEnabled() {
|
||||
return errors.New(exchange.WebsocketNotEnabled)
|
||||
}
|
||||
|
||||
var channels = []string{"book", "trades", "ticker"}
|
||||
var Dialer websocket.Dialer
|
||||
var err error
|
||||
|
||||
if b.Websocket.GetProxyAddress() != "" {
|
||||
proxy, err := url.Parse(b.Websocket.GetProxyAddress())
|
||||
if err != nil {
|
||||
log.Printf("%s Unable to connect to Websocket. Error: %s\n", b.GetName(), err)
|
||||
continue
|
||||
return err
|
||||
}
|
||||
Dialer.Proxy = http.ProxyURL(proxy)
|
||||
}
|
||||
|
||||
msgType, resp, err := b.WebsocketConn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Printf("%s Unable to read from Websocket. Error: %s\n", b.GetName(), err)
|
||||
continue
|
||||
}
|
||||
if msgType != websocket.TextMessage {
|
||||
continue
|
||||
}
|
||||
b.WebsocketConn, _, err = Dialer.Dial(b.Websocket.GetWebsocketURL(), http.Header{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to connect to Websocket. Error: %s", err)
|
||||
}
|
||||
|
||||
type WebsocketHandshake struct {
|
||||
Event string `json:"event"`
|
||||
Code int64 `json:"code"`
|
||||
Version float64 `json:"version"`
|
||||
}
|
||||
_, resp, err := b.WebsocketConn.ReadMessage()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to read from Websocket. Error: %s", err)
|
||||
}
|
||||
|
||||
hs := WebsocketHandshake{}
|
||||
err = common.JSONDecode(resp, &hs)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
var hs WebsocketHandshake
|
||||
err = common.JSONDecode(resp, &hs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if hs.Event == "info" {
|
||||
if b.Verbose {
|
||||
log.Printf("%s Connected to Websocket.\n", b.GetName())
|
||||
if hs.Event == "info" {
|
||||
if b.Verbose {
|
||||
log.Printf("%s Connected to Websocket.\n", b.GetName())
|
||||
}
|
||||
}
|
||||
|
||||
for _, x := range channels {
|
||||
for _, y := range b.EnabledPairs {
|
||||
params := make(map[string]string)
|
||||
if x == "book" {
|
||||
params["prec"] = "P0"
|
||||
}
|
||||
}
|
||||
|
||||
for _, x := range channels {
|
||||
for _, y := range b.EnabledPairs {
|
||||
params := make(map[string]string)
|
||||
if x == "book" {
|
||||
params["prec"] = "P0"
|
||||
}
|
||||
params["pair"] = y
|
||||
b.WebsocketSubscribe(x, params)
|
||||
}
|
||||
}
|
||||
|
||||
if b.AuthenticatedAPISupport {
|
||||
err = b.WebsocketSendAuth()
|
||||
params["pair"] = y
|
||||
err := b.WsSubscribe(x, params)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for b.Enabled && b.Websocket {
|
||||
if b.AuthenticatedAPISupport {
|
||||
err = b.WsSendAuth()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
pongReceive = make(chan struct{}, 1)
|
||||
|
||||
go b.WsReadData()
|
||||
go b.WsDataHandler()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WsReadData reads and handles websocket stream data
|
||||
func (b *Bitfinex) WsReadData() {
|
||||
b.Websocket.Wg.Add(1)
|
||||
|
||||
defer func() {
|
||||
err := b.WebsocketConn.Close()
|
||||
if err != nil {
|
||||
b.Websocket.DataHandler <- fmt.Errorf("bitfinex_websocket.go - closing websocket connection error %s",
|
||||
err)
|
||||
}
|
||||
b.Websocket.Wg.Done()
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-b.Websocket.ShutdownC:
|
||||
return
|
||||
default:
|
||||
msgType, resp, err := b.WebsocketConn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
break
|
||||
b.Websocket.DataHandler <- err
|
||||
return
|
||||
}
|
||||
|
||||
switch msgType {
|
||||
b.Websocket.TrafficAlert <- struct{}{}
|
||||
|
||||
b.Websocket.Intercomm <- exchange.WebsocketResponse{
|
||||
Type: msgType,
|
||||
Raw: resp,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WsDataHandler handles data from WsReadData
|
||||
func (b *Bitfinex) WsDataHandler() {
|
||||
b.Websocket.Wg.Add(1)
|
||||
defer b.Websocket.Wg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-b.Websocket.ShutdownC:
|
||||
return
|
||||
|
||||
case stream := <-b.Websocket.Intercomm:
|
||||
|
||||
switch stream.Type {
|
||||
case websocket.TextMessage:
|
||||
var result interface{}
|
||||
err := common.JSONDecode(resp, &result)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
common.JSONDecode(stream.Raw, &result)
|
||||
|
||||
switch reflect.TypeOf(result).String() {
|
||||
case "map[string]interface {}":
|
||||
@@ -181,51 +251,99 @@ func (b *Bitfinex) WebsocketClient() {
|
||||
|
||||
switch event {
|
||||
case "subscribed":
|
||||
b.WebsocketAddSubscriptionChannel(int(eventData["chanId"].(float64)), eventData["channel"].(string), eventData["pair"].(string))
|
||||
b.WsAddSubscriptionChannel(int(eventData["chanId"].(float64)),
|
||||
eventData["channel"].(string),
|
||||
eventData["pair"].(string))
|
||||
|
||||
case "auth":
|
||||
status := eventData["status"].(string)
|
||||
|
||||
if status == "OK" {
|
||||
b.WebsocketAddSubscriptionChannel(0, "account", "N/A")
|
||||
b.WsAddSubscriptionChannel(0, "account", "N/A")
|
||||
|
||||
} else if status == "fail" {
|
||||
log.Printf("%s Websocket unable to AUTH. Error code: %s\n", b.GetName(), eventData["code"].(string))
|
||||
b.Websocket.DataHandler <- fmt.Errorf("bitfinex.go error - Websocket unable to AUTH. Error code: %s",
|
||||
eventData["code"].(string))
|
||||
|
||||
b.AuthenticatedAPISupport = false
|
||||
}
|
||||
}
|
||||
|
||||
case "[]interface {}":
|
||||
chanData := result.([]interface{})
|
||||
chanID := int(chanData[0].(float64))
|
||||
chanInfo, ok := b.WebsocketSubdChannels[chanID]
|
||||
|
||||
chanInfo, ok := b.WebsocketSubdChannels[chanID]
|
||||
if !ok {
|
||||
log.Printf("Unable to locate chanID: %d\n", chanID)
|
||||
b.Websocket.DataHandler <- fmt.Errorf("bitfinex.go error - Unable to locate chanID: %d",
|
||||
chanID)
|
||||
continue
|
||||
} else {
|
||||
if len(chanData) == 2 {
|
||||
if reflect.TypeOf(chanData[1]).String() == "string" {
|
||||
if chanData[1].(string) == bitfinexWebsocketHeartbeat {
|
||||
continue
|
||||
} else if chanData[1].(string) == "pong" {
|
||||
pongReceive <- struct{}{}
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch chanInfo.Channel {
|
||||
case "book":
|
||||
orderbook := []WebsocketBook{}
|
||||
newOrderbook := []WebsocketBook{}
|
||||
switch len(chanData) {
|
||||
case 2:
|
||||
data := chanData[1].([]interface{})
|
||||
for _, x := range data {
|
||||
y := x.([]interface{})
|
||||
orderbook = append(orderbook, WebsocketBook{Price: y[0].(float64), Count: int(y[1].(float64)), Amount: y[2].(float64)})
|
||||
newOrderbook = append(newOrderbook, WebsocketBook{
|
||||
Price: y[0].(float64),
|
||||
Count: int(y[1].(float64)),
|
||||
Amount: y[2].(float64)})
|
||||
}
|
||||
case 4:
|
||||
orderbook = append(orderbook, WebsocketBook{Price: chanData[1].(float64), Count: int(chanData[2].(float64)), Amount: chanData[3].(float64)})
|
||||
}
|
||||
log.Println(orderbook)
|
||||
case "ticker":
|
||||
ticker := WebsocketTicker{Bid: chanData[1].(float64), BidSize: chanData[2].(float64), Ask: chanData[3].(float64), AskSize: chanData[4].(float64),
|
||||
DailyChange: chanData[5].(float64), DialyChangePerc: chanData[6].(float64), LastPrice: chanData[7].(float64), Volume: chanData[8].(float64)}
|
||||
|
||||
log.Printf("Bitfinex %s Websocket Last %f Volume %f\n", chanInfo.Pair, ticker.LastPrice, ticker.Volume)
|
||||
case 4:
|
||||
newOrderbook = append(newOrderbook, WebsocketBook{
|
||||
Price: chanData[1].(float64),
|
||||
Count: int(chanData[2].(float64)),
|
||||
Amount: chanData[3].(float64)})
|
||||
}
|
||||
|
||||
if len(newOrderbook) > 1 {
|
||||
err := b.WsInsertSnapshot(pair.NewCurrencyPairFromString(chanInfo.Pair),
|
||||
"SPOT",
|
||||
newOrderbook)
|
||||
|
||||
if err != nil {
|
||||
b.Websocket.DataHandler <- fmt.Errorf("bitfinex_websocket.go inserting snapshot error: %s",
|
||||
err)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
err := b.WsUpdateOrderbook(pair.NewCurrencyPairFromString(chanInfo.Pair),
|
||||
"SPOT",
|
||||
newOrderbook[0])
|
||||
|
||||
if err != nil {
|
||||
b.Websocket.DataHandler <- fmt.Errorf("bitfinex_websocket.go updating orderbook error: %s",
|
||||
err)
|
||||
}
|
||||
|
||||
case "ticker":
|
||||
b.Websocket.DataHandler <- exchange.TickerData{
|
||||
Quantity: chanData[8].(float64),
|
||||
ClosePrice: chanData[7].(float64),
|
||||
HighPrice: chanData[9].(float64),
|
||||
LowPrice: chanData[10].(float64),
|
||||
Pair: pair.NewCurrencyPairFromString(chanInfo.Pair),
|
||||
Exchange: b.GetName(),
|
||||
AssetType: "SPOT",
|
||||
}
|
||||
|
||||
case "account":
|
||||
switch chanData[1].(string) {
|
||||
case bitfinexWebsocketPositionSnapshot:
|
||||
@@ -233,47 +351,108 @@ func (b *Bitfinex) WebsocketClient() {
|
||||
data := chanData[2].([]interface{})
|
||||
for _, x := range data {
|
||||
y := x.([]interface{})
|
||||
positionSnapshot = append(positionSnapshot, WebsocketPosition{Pair: y[0].(string), Status: y[1].(string), Amount: y[2].(float64), Price: y[3].(float64),
|
||||
MarginFunding: y[4].(float64), MarginFundingType: int(y[5].(float64))})
|
||||
positionSnapshot = append(positionSnapshot,
|
||||
WebsocketPosition{
|
||||
Pair: y[0].(string),
|
||||
Status: y[1].(string),
|
||||
Amount: y[2].(float64),
|
||||
Price: y[3].(float64),
|
||||
MarginFunding: y[4].(float64),
|
||||
MarginFundingType: int(y[5].(float64))})
|
||||
}
|
||||
log.Println(positionSnapshot)
|
||||
|
||||
if len(positionSnapshot) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
b.Websocket.DataHandler <- positionSnapshot
|
||||
|
||||
case bitfinexWebsocketPositionNew, bitfinexWebsocketPositionUpdate, bitfinexWebsocketPositionClose:
|
||||
data := chanData[2].([]interface{})
|
||||
position := WebsocketPosition{Pair: data[0].(string), Status: data[1].(string), Amount: data[2].(float64), Price: data[3].(float64),
|
||||
MarginFunding: data[4].(float64), MarginFundingType: int(data[5].(float64))}
|
||||
log.Println(position)
|
||||
position := WebsocketPosition{
|
||||
Pair: data[0].(string),
|
||||
Status: data[1].(string),
|
||||
Amount: data[2].(float64),
|
||||
Price: data[3].(float64),
|
||||
MarginFunding: data[4].(float64),
|
||||
MarginFundingType: int(data[5].(float64))}
|
||||
|
||||
b.Websocket.DataHandler <- position
|
||||
|
||||
case bitfinexWebsocketWalletSnapshot:
|
||||
data := chanData[2].([]interface{})
|
||||
walletSnapshot := []WebsocketWallet{}
|
||||
for _, x := range data {
|
||||
y := x.([]interface{})
|
||||
walletSnapshot = append(walletSnapshot, WebsocketWallet{Name: y[0].(string), Currency: y[1].(string), Balance: y[2].(float64), UnsettledInterest: y[3].(float64)})
|
||||
walletSnapshot = append(walletSnapshot,
|
||||
WebsocketWallet{
|
||||
Name: y[0].(string),
|
||||
Currency: y[1].(string),
|
||||
Balance: y[2].(float64),
|
||||
UnsettledInterest: y[3].(float64)})
|
||||
}
|
||||
log.Println(walletSnapshot)
|
||||
|
||||
b.Websocket.DataHandler <- walletSnapshot
|
||||
|
||||
case bitfinexWebsocketWalletUpdate:
|
||||
data := chanData[2].([]interface{})
|
||||
wallet := WebsocketWallet{Name: data[0].(string), Currency: data[1].(string), Balance: data[2].(float64), UnsettledInterest: data[3].(float64)}
|
||||
log.Println(wallet)
|
||||
wallet := WebsocketWallet{
|
||||
Name: data[0].(string),
|
||||
Currency: data[1].(string),
|
||||
Balance: data[2].(float64),
|
||||
UnsettledInterest: data[3].(float64)}
|
||||
|
||||
b.Websocket.DataHandler <- wallet
|
||||
|
||||
case bitfinexWebsocketOrderSnapshot:
|
||||
orderSnapshot := []WebsocketOrder{}
|
||||
data := chanData[2].([]interface{})
|
||||
for _, x := range data {
|
||||
y := x.([]interface{})
|
||||
orderSnapshot = append(orderSnapshot, WebsocketOrder{OrderID: int64(y[0].(float64)), Pair: y[1].(string), Amount: y[2].(float64), OrigAmount: y[3].(float64),
|
||||
OrderType: y[4].(string), Status: y[5].(string), Price: y[6].(float64), PriceAvg: y[7].(float64), Timestamp: y[8].(string)})
|
||||
orderSnapshot = append(orderSnapshot,
|
||||
WebsocketOrder{
|
||||
OrderID: int64(y[0].(float64)),
|
||||
Pair: y[1].(string),
|
||||
Amount: y[2].(float64),
|
||||
OrigAmount: y[3].(float64),
|
||||
OrderType: y[4].(string),
|
||||
Status: y[5].(string),
|
||||
Price: y[6].(float64),
|
||||
PriceAvg: y[7].(float64),
|
||||
Timestamp: y[8].(string)})
|
||||
}
|
||||
log.Println(orderSnapshot)
|
||||
|
||||
b.Websocket.DataHandler <- orderSnapshot
|
||||
|
||||
case bitfinexWebsocketOrderNew, bitfinexWebsocketOrderUpdate, bitfinexWebsocketOrderCancel:
|
||||
data := chanData[2].([]interface{})
|
||||
order := WebsocketOrder{OrderID: int64(data[0].(float64)), Pair: data[1].(string), Amount: data[2].(float64), OrigAmount: data[3].(float64),
|
||||
OrderType: data[4].(string), Status: data[5].(string), Price: data[6].(float64), PriceAvg: data[7].(float64), Timestamp: data[8].(string), Notify: int(data[9].(float64))}
|
||||
log.Println(order)
|
||||
order := WebsocketOrder{
|
||||
OrderID: int64(data[0].(float64)),
|
||||
Pair: data[1].(string),
|
||||
Amount: data[2].(float64),
|
||||
OrigAmount: data[3].(float64),
|
||||
OrderType: data[4].(string),
|
||||
Status: data[5].(string),
|
||||
Price: data[6].(float64),
|
||||
PriceAvg: data[7].(float64),
|
||||
Timestamp: data[8].(string),
|
||||
Notify: int(data[9].(float64))}
|
||||
|
||||
b.Websocket.DataHandler <- order
|
||||
|
||||
case bitfinexWebsocketTradeExecuted:
|
||||
data := chanData[2].([]interface{})
|
||||
trade := WebsocketTradeExecuted{TradeID: int64(data[0].(float64)), Pair: data[1].(string), Timestamp: int64(data[2].(float64)), OrderID: int64(data[3].(float64)),
|
||||
AmountExecuted: data[4].(float64), PriceExecuted: data[5].(float64)}
|
||||
log.Println(trade)
|
||||
trade := WebsocketTradeExecuted{
|
||||
TradeID: int64(data[0].(float64)),
|
||||
Pair: data[1].(string),
|
||||
Timestamp: int64(data[2].(float64)),
|
||||
OrderID: int64(data[3].(float64)),
|
||||
AmountExecuted: data[4].(float64),
|
||||
PriceExecuted: data[5].(float64)}
|
||||
|
||||
b.Websocket.DataHandler <- trade
|
||||
}
|
||||
|
||||
case "trades":
|
||||
trades := []WebsocketTrade{}
|
||||
switch len(chanData) {
|
||||
@@ -284,23 +463,174 @@ func (b *Bitfinex) WebsocketClient() {
|
||||
if _, ok := y[0].(string); ok {
|
||||
continue
|
||||
}
|
||||
trades = append(trades, WebsocketTrade{ID: int64(y[0].(float64)), Timestamp: int64(y[1].(float64)), Price: y[2].(float64), Amount: y[3].(float64)})
|
||||
}
|
||||
case 7:
|
||||
trade := WebsocketTrade{ID: int64(chanData[3].(float64)), Timestamp: int64(chanData[4].(float64)), Price: chanData[5].(float64), Amount: chanData[6].(float64)}
|
||||
trades = append(trades, trade)
|
||||
|
||||
if b.Verbose {
|
||||
log.Printf("Bitfinex %s Websocket Trade ID %d Timestamp %d Price %f Amount %f\n", chanInfo.Pair, trade.ID, trade.Timestamp, trade.Price, trade.Amount)
|
||||
id, _ := y[0].(float64)
|
||||
|
||||
trades = append(trades,
|
||||
WebsocketTrade{
|
||||
ID: int64(id),
|
||||
Timestamp: int64(y[1].(float64)),
|
||||
Price: y[2].(float64),
|
||||
Amount: y[3].(float64)})
|
||||
}
|
||||
|
||||
case 7:
|
||||
trade := WebsocketTrade{
|
||||
ID: int64(chanData[3].(float64)),
|
||||
Timestamp: int64(chanData[4].(float64)),
|
||||
Price: chanData[5].(float64),
|
||||
Amount: chanData[6].(float64)}
|
||||
trades = append(trades, trade)
|
||||
}
|
||||
|
||||
if len(trades) > 0 {
|
||||
side := "BUY"
|
||||
newAmount := trades[0].Amount
|
||||
if newAmount < 0 {
|
||||
side = "SELL"
|
||||
newAmount = newAmount * -1
|
||||
}
|
||||
|
||||
b.Websocket.DataHandler <- exchange.TradeData{
|
||||
CurrencyPair: pair.NewCurrencyPairFromString(chanInfo.Pair),
|
||||
Timestamp: time.Unix(trades[0].Timestamp, 0),
|
||||
Price: trades[0].Price,
|
||||
Amount: newAmount,
|
||||
Exchange: b.GetName(),
|
||||
AssetType: "SPOT",
|
||||
Side: side,
|
||||
}
|
||||
}
|
||||
log.Println(trades)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
b.WebsocketConn.Close()
|
||||
log.Printf("%s Websocket client disconnected.\n", b.GetName())
|
||||
}
|
||||
}
|
||||
|
||||
// WsInsertSnapshot add the initial orderbook snapshot when subscribed to a
|
||||
// channel
|
||||
func (b *Bitfinex) WsInsertSnapshot(p pair.CurrencyPair, assetType string, books []WebsocketBook) error {
|
||||
if len(books) == 0 {
|
||||
return errors.New("bitfinex.go error - no orderbooks submitted")
|
||||
}
|
||||
|
||||
var bid, ask []orderbook.Item
|
||||
for _, book := range books {
|
||||
if book.Amount >= 0 {
|
||||
bid = append(bid, orderbook.Item{Amount: book.Amount, Price: book.Price})
|
||||
} else {
|
||||
ask = append(ask, orderbook.Item{Amount: book.Amount * -1, Price: book.Price})
|
||||
}
|
||||
}
|
||||
|
||||
if len(bid) == 0 && len(ask) == 0 {
|
||||
return errors.New("bitfinex.go error - no orderbooks in item lists")
|
||||
}
|
||||
|
||||
var newOrderbook orderbook.Base
|
||||
newOrderbook.Asks = ask
|
||||
newOrderbook.AssetType = assetType
|
||||
newOrderbook.Bids = bid
|
||||
newOrderbook.CurrencyPair = p.Pair().String()
|
||||
newOrderbook.LastUpdated = time.Now()
|
||||
newOrderbook.Pair = p
|
||||
|
||||
err := b.Websocket.Orderbook.LoadSnapshot(newOrderbook, b.GetName())
|
||||
if err != nil {
|
||||
return fmt.Errorf("bitfinex.go error - %s", err)
|
||||
}
|
||||
|
||||
b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{Pair: p,
|
||||
Asset: assetType,
|
||||
Exchange: b.GetName()}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WsUpdateOrderbook updates the orderbook list, removing and adding to the
|
||||
// orderbook sides
|
||||
func (b *Bitfinex) WsUpdateOrderbook(p pair.CurrencyPair, assetType string, book WebsocketBook) error {
|
||||
|
||||
if book.Count > 0 {
|
||||
if book.Amount > 0 {
|
||||
// Update/add bid
|
||||
newBidPrice := orderbook.Item{Price: book.Price, Amount: book.Amount}
|
||||
err := b.Websocket.Orderbook.Update([]orderbook.Item{newBidPrice},
|
||||
nil,
|
||||
p,
|
||||
time.Now(),
|
||||
b.GetName(),
|
||||
assetType)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{Pair: p,
|
||||
Asset: assetType,
|
||||
Exchange: b.GetName()}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update/add ask
|
||||
newAskPrice := orderbook.Item{Price: book.Price, Amount: book.Amount * -1}
|
||||
err := b.Websocket.Orderbook.Update(nil,
|
||||
[]orderbook.Item{newAskPrice},
|
||||
p,
|
||||
time.Now(),
|
||||
b.GetName(),
|
||||
assetType)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{Pair: p,
|
||||
Asset: assetType,
|
||||
Exchange: b.GetName()}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if book.Amount == 1 {
|
||||
// Remove bid
|
||||
bidPriceRemove := orderbook.Item{Price: book.Price, Amount: 0}
|
||||
err := b.Websocket.Orderbook.Update([]orderbook.Item{bidPriceRemove},
|
||||
nil,
|
||||
p,
|
||||
time.Now(),
|
||||
b.GetName(),
|
||||
assetType)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{Pair: p,
|
||||
Asset: assetType,
|
||||
Exchange: b.GetName()}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove from ask
|
||||
askPriceRemove := orderbook.Item{Price: book.Price, Amount: 0}
|
||||
err := b.Websocket.Orderbook.Update(nil,
|
||||
[]orderbook.Item{askPriceRemove},
|
||||
p,
|
||||
time.Now(),
|
||||
b.GetName(),
|
||||
assetType)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{Pair: p,
|
||||
Asset: assetType,
|
||||
Exchange: b.GetName()}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
package bitfinex
|
||||
|
||||
// func TestWebsocketPingHandler(t *testing.T) {
|
||||
// wsPingHandler := Bitfinex{}
|
||||
// var Dialer websocket.Dialer
|
||||
// var err error
|
||||
//
|
||||
// wsPingHandler.WebsocketConn, _, err = Dialer.Dial(bitfinexWebsocket, http.Header{})
|
||||
// if err != nil {
|
||||
// t.Errorf("Test Failed - Bitfinex dialer error: %s", err)
|
||||
// }
|
||||
// err = wsPingHandler.WebsocketPingHandler()
|
||||
// if err != nil {
|
||||
// t.Errorf("Test Failed - Bitfinex WebsocketPingHandler() error: %s", err)
|
||||
// }
|
||||
// err = wsPingHandler.WebsocketConn.Close()
|
||||
// if err != nil {
|
||||
// t.Errorf("Test Failed - Bitfinex websocketConn.Close() error: %s", err)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func TestWebsocketSubscribe(t *testing.T) {
|
||||
// websocketSubcribe := Bitfinex{}
|
||||
// var Dialer websocket.Dialer
|
||||
// var err error
|
||||
// params := make(map[string]string)
|
||||
// params["pair"] = "BTCUSD"
|
||||
//
|
||||
// websocketSubcribe.WebsocketConn, _, err = Dialer.Dial(bitfinexWebsocket, http.Header{})
|
||||
// if err != nil {
|
||||
// t.Errorf("Test Failed - Bitfinex Dialer error: %s", err)
|
||||
// }
|
||||
// err = websocketSubcribe.WebsocketSubscribe("ticker", params)
|
||||
// if err != nil {
|
||||
// t.Errorf("Test Failed - Bitfinex WebsocketSubscribe() error: %s", err)
|
||||
// }
|
||||
//
|
||||
// err = websocketSubcribe.WebsocketConn.Close()
|
||||
// if err != nil {
|
||||
// t.Errorf("Test Failed - Bitfinex websocketConn.Close() error: %s", err)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func TestWebsocketSendAuth(t *testing.T) {
|
||||
// wsSendAuth := Bitfinex{}
|
||||
// var Dialer websocket.Dialer
|
||||
// var err error
|
||||
//
|
||||
// wsSendAuth.WebsocketConn, _, err = Dialer.Dial(bitfinexWebsocket, http.Header{})
|
||||
// if err != nil {
|
||||
// t.Errorf("Test Failed - Bitfinex Dialer error: %s", err)
|
||||
// }
|
||||
// err = wsSendAuth.WebsocketSendAuth()
|
||||
// if err != nil {
|
||||
// t.Errorf("Test Failed - Bitfinex WebsocketSendAuth() error: %s", err)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func TestWebsocketAddSubscriptionChannel(t *testing.T) {
|
||||
// wsAddSubscriptionChannel := Bitfinex{}
|
||||
// wsAddSubscriptionChannel.SetDefaults()
|
||||
// var Dialer websocket.Dialer
|
||||
// var err error
|
||||
//
|
||||
// wsAddSubscriptionChannel.WebsocketConn, _, err = Dialer.Dial(bitfinexWebsocket, http.Header{})
|
||||
// if err != nil {
|
||||
// t.Errorf("Test Failed - Bitfinex Dialer error: %s", err)
|
||||
// }
|
||||
//
|
||||
// wsAddSubscriptionChannel.WebsocketAddSubscriptionChannel(1337, "ticker", "BTCUSD")
|
||||
// if len(wsAddSubscriptionChannel.WebsocketSubdChannels) == 0 {
|
||||
// t.Errorf("Test Failed - Bitfinex WebsocketAddSubscriptionChannel() error: %s", err)
|
||||
// }
|
||||
// if wsAddSubscriptionChannel.WebsocketSubdChannels[1337].Channel != "ticker" {
|
||||
// t.Errorf("Test Failed - Bitfinex WebsocketAddSubscriptionChannel() error: %s", err)
|
||||
// }
|
||||
// if wsAddSubscriptionChannel.WebsocketSubdChannels[1337].Pair != "BTCUSD" {
|
||||
// t.Errorf("Test Failed - Bitfinex WebsocketAddSubscriptionChannel() error: %s", err)
|
||||
// }
|
||||
// }
|
||||
@@ -25,15 +25,11 @@ func (b *Bitfinex) Start(wg *sync.WaitGroup) {
|
||||
// Run implements the Bitfinex wrapper
|
||||
func (b *Bitfinex) Run() {
|
||||
if b.Verbose {
|
||||
log.Printf("%s Websocket: %s.", b.GetName(), common.IsEnabled(b.Websocket))
|
||||
log.Printf("%s Websocket: %s.", b.GetName(), common.IsEnabled(b.Websocket.IsEnabled()))
|
||||
log.Printf("%s polling delay: %ds.\n", b.GetName(), b.RESTPollingDelay)
|
||||
log.Printf("%s %d currencies enabled: %s.\n", b.GetName(), len(b.EnabledPairs), b.EnabledPairs)
|
||||
}
|
||||
|
||||
if b.Websocket {
|
||||
go b.WebsocketClient()
|
||||
}
|
||||
|
||||
exchangeProducts, err := b.GetSymbols()
|
||||
if err != nil {
|
||||
log.Printf("%s Failed to get available symbols.\n", b.GetName())
|
||||
@@ -225,3 +221,8 @@ func (b *Bitfinex) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount
|
||||
func (b *Bitfinex) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) {
|
||||
return "", errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// GetWebsocket returns a pointer to the exchange websocket
|
||||
func (b *Bitfinex) GetWebsocket() (*exchange.Websocket, error) {
|
||||
return b.Websocket, nil
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
package bitfinex
|
||||
|
||||
// func TestStart(t *testing.T) {
|
||||
// start := Bitfinex{}
|
||||
// start.Start(wg *sync.WaitGroup)
|
||||
// }
|
||||
//
|
||||
// func TestRun(t *testing.T) {
|
||||
// run := Bitfinex{}
|
||||
// run.Run()
|
||||
// }
|
||||
//
|
||||
// func TestGetTickerPrice(t *testing.T) {
|
||||
// getTickerPrice := Bitfinex{}
|
||||
// getTickerPrice.EnabledPairs = []string{"BTCUSD", "LTCUSD"}
|
||||
// _, err := getTickerPrice.GetTickerPrice(pair.NewCurrencyPair("BTC", "USD"),
|
||||
// ticker.Spot)
|
||||
// if err != nil {
|
||||
// t.Errorf("Test Failed - Bitfinex GetTickerPrice() error: %s", err)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func TestGetOrderbookEx(t *testing.T) {
|
||||
// getOrderBookEx := Bitfinex{}
|
||||
// _, err := getOrderBookEx.GetOrderbookEx(pair.NewCurrencyPair("BTC", "USD"),
|
||||
// ticker.Spot)
|
||||
// if err != nil {
|
||||
// t.Errorf("Test Failed - Bitfinex GetOrderbookEx() error: %s", err)
|
||||
// }
|
||||
// }
|
||||
@@ -79,7 +79,6 @@ func (b *Bitflyer) SetDefaults() {
|
||||
b.Name = "Bitflyer"
|
||||
b.Enabled = false
|
||||
b.Verbose = false
|
||||
b.Websocket = false
|
||||
b.RESTPollingDelay = 10
|
||||
b.RequestCurrencyPairFormat.Delimiter = "_"
|
||||
b.RequestCurrencyPairFormat.Uppercase = true
|
||||
@@ -96,6 +95,7 @@ func (b *Bitflyer) SetDefaults() {
|
||||
b.APIUrl = b.APIUrlDefault
|
||||
b.APIUrlSecondaryDefault = chainAnalysis
|
||||
b.APIUrlSecondary = b.APIUrlSecondaryDefault
|
||||
b.WebsocketInit()
|
||||
}
|
||||
|
||||
// Setup takes in the supplied exchange configuration details and sets params
|
||||
@@ -110,7 +110,7 @@ func (b *Bitflyer) Setup(exch config.ExchangeConfig) {
|
||||
b.SetHTTPClientUserAgent(exch.HTTPUserAgent)
|
||||
b.RESTPollingDelay = exch.RESTPollingDelay
|
||||
b.Verbose = exch.Verbose
|
||||
b.Websocket = exch.Websocket
|
||||
b.Websocket.SetEnabled(exch.Websocket)
|
||||
b.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",")
|
||||
b.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",")
|
||||
b.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",")
|
||||
@@ -130,6 +130,10 @@ func (b *Bitflyer) Setup(exch config.ExchangeConfig) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = b.SetClientProxyAddress(exch.ProxyAddress)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ func (b *Bitflyer) Start(wg *sync.WaitGroup) {
|
||||
// Run implements the Bitflyer wrapper
|
||||
func (b *Bitflyer) Run() {
|
||||
if b.Verbose {
|
||||
log.Printf("%s Websocket: %s.", b.GetName(), common.IsEnabled(b.Websocket))
|
||||
log.Printf("%s Websocket: %s.", b.GetName(), common.IsEnabled(b.Websocket.IsEnabled()))
|
||||
log.Printf("%s polling delay: %ds.\n", b.GetName(), b.RESTPollingDelay)
|
||||
log.Printf("%s %d currencies enabled: %s.\n", b.GetName(), len(b.EnabledPairs), b.EnabledPairs)
|
||||
}
|
||||
@@ -201,3 +201,8 @@ func (b *Bitflyer) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount
|
||||
func (b *Bitflyer) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) {
|
||||
return "", errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// GetWebsocket returns a pointer to the exchange websocket
|
||||
func (b *Bitflyer) GetWebsocket() (*exchange.Websocket, error) {
|
||||
return nil, errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
@@ -60,7 +60,6 @@ func (b *Bithumb) SetDefaults() {
|
||||
b.Name = "Bithumb"
|
||||
b.Enabled = false
|
||||
b.Verbose = false
|
||||
b.Websocket = false
|
||||
b.RESTPollingDelay = 10
|
||||
b.RequestCurrencyPairFormat.Delimiter = ""
|
||||
b.RequestCurrencyPairFormat.Uppercase = true
|
||||
@@ -76,6 +75,7 @@ func (b *Bithumb) SetDefaults() {
|
||||
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
|
||||
b.APIUrlDefault = apiURL
|
||||
b.APIUrl = b.APIUrlDefault
|
||||
b.WebsocketInit()
|
||||
}
|
||||
|
||||
// Setup takes in the supplied exchange configuration details and sets params
|
||||
@@ -90,7 +90,7 @@ func (b *Bithumb) Setup(exch config.ExchangeConfig) {
|
||||
b.SetHTTPClientUserAgent(exch.HTTPUserAgent)
|
||||
b.RESTPollingDelay = exch.RESTPollingDelay
|
||||
b.Verbose = exch.Verbose
|
||||
b.Websocket = exch.Websocket
|
||||
b.Websocket.SetEnabled(exch.Websocket)
|
||||
b.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",")
|
||||
b.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",")
|
||||
b.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",")
|
||||
@@ -110,6 +110,10 @@ func (b *Bithumb) Setup(exch config.ExchangeConfig) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = b.SetClientProxyAddress(exch.ProxyAddress)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ func (b *Bithumb) Start(wg *sync.WaitGroup) {
|
||||
// Run implements the OKEX wrapper
|
||||
func (b *Bithumb) Run() {
|
||||
if b.Verbose {
|
||||
log.Printf("%s Websocket: %s. (url: %s).\n", b.GetName(), common.IsEnabled(b.Websocket), b.WebsocketURL)
|
||||
log.Printf("%s Websocket: %s. (url: %s).\n", b.GetName(), common.IsEnabled(b.Websocket.IsEnabled()), b.WebsocketURL)
|
||||
log.Printf("%s polling delay: %ds.\n", b.GetName(), b.RESTPollingDelay)
|
||||
log.Printf("%s %d currencies enabled: %s.\n", b.GetName(), len(b.EnabledPairs), b.EnabledPairs)
|
||||
}
|
||||
@@ -188,3 +188,8 @@ func (b *Bithumb) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount f
|
||||
func (b *Bithumb) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) {
|
||||
return "", errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// GetWebsocket returns a pointer to the exchange websocket
|
||||
func (b *Bithumb) GetWebsocket() (*exchange.Websocket, error) {
|
||||
return nil, errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
type Bitmex struct {
|
||||
exchange.Base
|
||||
WebsocketConn *websocket.Conn
|
||||
shutdown *Shutdown
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -114,7 +113,6 @@ func (b *Bitmex) SetDefaults() {
|
||||
b.Name = "Bitmex"
|
||||
b.Enabled = false
|
||||
b.Verbose = false
|
||||
b.Websocket = false
|
||||
b.RESTPollingDelay = 10
|
||||
b.RequestCurrencyPairFormat.Delimiter = ""
|
||||
b.RequestCurrencyPairFormat.Uppercase = true
|
||||
@@ -125,10 +123,10 @@ func (b *Bitmex) SetDefaults() {
|
||||
request.NewRateLimit(time.Second, bitmexAuthRate),
|
||||
request.NewRateLimit(time.Second, bitmexUnauthRate),
|
||||
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
|
||||
b.shutdown = b.NewRoutineManagement()
|
||||
b.APIUrlDefault = bitmexAPIURL
|
||||
b.APIUrl = b.APIUrlDefault
|
||||
b.SupportsAutoPairUpdating = true
|
||||
b.WebsocketInit()
|
||||
}
|
||||
|
||||
// Setup takes in the supplied exchange configuration details and sets params
|
||||
@@ -141,7 +139,7 @@ func (b *Bitmex) Setup(exch config.ExchangeConfig) {
|
||||
b.SetAPIKeys(exch.APIKey, exch.APISecret, "", false)
|
||||
b.RESTPollingDelay = exch.RESTPollingDelay
|
||||
b.Verbose = exch.Verbose
|
||||
b.Websocket = exch.Websocket
|
||||
b.Websocket.SetEnabled(exch.Websocket)
|
||||
b.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",")
|
||||
b.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",")
|
||||
b.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",")
|
||||
@@ -161,6 +159,18 @@ func (b *Bitmex) Setup(exch config.ExchangeConfig) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = b.SetClientProxyAddress(exch.ProxyAddress)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = b.WebsocketSetup(b.WsConnector,
|
||||
exch.Name,
|
||||
exch.Websocket,
|
||||
bitmexWSURL,
|
||||
exch.WebsocketURL)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,17 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/thrasher-/gocryptotrader/exchanges/orderbook"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/thrasher-/gocryptotrader/common"
|
||||
"github.com/thrasher-/gocryptotrader/currency/pair"
|
||||
"github.com/thrasher-/gocryptotrader/exchanges"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -55,15 +61,27 @@ const (
|
||||
|
||||
var (
|
||||
pongChan = make(chan int, 1)
|
||||
timer *time.Timer
|
||||
)
|
||||
|
||||
// WebsocketConnect initiates a new websocket connection
|
||||
func (b *Bitmex) WebsocketConnect() error {
|
||||
// WsConnector initiates a new websocket connection
|
||||
func (b *Bitmex) WsConnector() error {
|
||||
if !b.Websocket.IsEnabled() || !b.IsEnabled() {
|
||||
return errors.New(exchange.WebsocketNotEnabled)
|
||||
}
|
||||
|
||||
var dialer websocket.Dialer
|
||||
var err error
|
||||
|
||||
b.WebsocketConn, _, err = dialer.Dial(bitmexWSURL, nil)
|
||||
if b.Websocket.GetProxyAddress() != "" {
|
||||
proxy, err := url.Parse(b.Websocket.GetProxyAddress())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dialer.Proxy = http.ProxyURL(proxy)
|
||||
}
|
||||
|
||||
b.WebsocketConn, _, err = dialer.Dial(b.Websocket.GetWebsocketURL(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -79,8 +97,6 @@ func (b *Bitmex) WebsocketConnect() error {
|
||||
return err
|
||||
}
|
||||
|
||||
go b.connectionHandler()
|
||||
|
||||
if b.Verbose {
|
||||
log.Printf("Successfully connected to Bitmex %s at time: %s Limit: %d",
|
||||
welcomeResp.Info,
|
||||
@@ -88,309 +104,292 @@ func (b *Bitmex) WebsocketConnect() error {
|
||||
welcomeResp.Limit.Remaining)
|
||||
}
|
||||
|
||||
go b.handleIncomingData()
|
||||
go b.wsHandleIncomingData()
|
||||
go b.wsReadData()
|
||||
|
||||
err = b.websocketSubscribe()
|
||||
if err != nil {
|
||||
b.WebsocketConn.Close()
|
||||
closeError := b.WebsocketConn.Close()
|
||||
if closeError != nil {
|
||||
return fmt.Errorf("bitmex_websocket.go error - Websocket connection could not close %s",
|
||||
closeError)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if b.AuthenticatedAPISupport {
|
||||
err := b.websocketSendAuth()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Timer handles connection loss or failure
|
||||
func (b *Bitmex) connectionHandler() {
|
||||
func (b *Bitmex) wsReadData() {
|
||||
b.Websocket.Wg.Add(1)
|
||||
|
||||
defer func() {
|
||||
if b.Verbose {
|
||||
log.Println("Bitmex websocket: Connection handler routine shutdown")
|
||||
err := b.WebsocketConn.Close()
|
||||
if err != nil {
|
||||
b.Websocket.DataHandler <- fmt.Errorf("bitmex_websocket.go - Unable to close Websocket connection. Error: %s",
|
||||
err)
|
||||
}
|
||||
b.Websocket.Wg.Done()
|
||||
}()
|
||||
|
||||
shutdown := b.shutdown.addRoutine()
|
||||
|
||||
timer = time.NewTimer(5 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-timer.C:
|
||||
timeout := time.After(5 * time.Second)
|
||||
err := b.WebsocketConn.WriteJSON("ping")
|
||||
case <-b.Websocket.ShutdownC:
|
||||
return
|
||||
|
||||
default:
|
||||
_, resp, err := b.WebsocketConn.ReadMessage()
|
||||
if err != nil {
|
||||
b.reconnect()
|
||||
b.Websocket.DataHandler <- fmt.Errorf("bitmex_websocket.go - websocket connection Error: %s",
|
||||
err)
|
||||
return
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-pongChan:
|
||||
if b.Verbose {
|
||||
log.Println("Bitmex websocket: PONG received")
|
||||
}
|
||||
break
|
||||
case <-timeout:
|
||||
log.Println("Bitmex websocket: Connection timed out - Closing connection....")
|
||||
b.WebsocketConn.Close()
|
||||
|
||||
log.Println("Bitmex websocket: Connection timed out - Reconnecting...")
|
||||
b.reconnect()
|
||||
return
|
||||
}
|
||||
b.Websocket.TrafficAlert <- struct{}{}
|
||||
|
||||
b.Websocket.Intercomm <- exchange.WebsocketResponse{
|
||||
Raw: resp,
|
||||
}
|
||||
case <-shutdown:
|
||||
log.Println("Bitmex websocket: shutdown requested - Closing connection....")
|
||||
b.WebsocketConn.Close()
|
||||
log.Println("Bitmex websocket: Sending shutdown message")
|
||||
b.shutdown.routineShutdown()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reconnect handles reconnections to websocket API
|
||||
func (b *Bitmex) reconnect() {
|
||||
for {
|
||||
err := b.WebsocketConnect()
|
||||
if err != nil {
|
||||
log.Println("Bitmex websocket: Connection timed out - Failed to connect, sleeping...")
|
||||
time.Sleep(time.Second * 2)
|
||||
continue
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handleIncomingData services incoming data from the websocket connection
|
||||
func (b *Bitmex) handleIncomingData() {
|
||||
defer func() {
|
||||
if b.Verbose {
|
||||
log.Println("Bitmex websocket: Response data handler routine shutdown")
|
||||
}
|
||||
}()
|
||||
// wsHandleIncomingData services incoming data from the websocket connection
|
||||
func (b *Bitmex) wsHandleIncomingData() {
|
||||
b.Websocket.Wg.Add(1)
|
||||
defer b.Websocket.Wg.Done()
|
||||
|
||||
for {
|
||||
_, resp, err := b.WebsocketConn.ReadMessage()
|
||||
if err != nil {
|
||||
if b.Verbose {
|
||||
log.Println("Bitmex websocket: Connection error", err)
|
||||
}
|
||||
select {
|
||||
case <-b.Websocket.ShutdownC:
|
||||
return
|
||||
}
|
||||
|
||||
message := string(resp)
|
||||
if common.StringContains(message, "pong") {
|
||||
if b.Verbose {
|
||||
log.Println("Bitmex websocket: PONG receieved")
|
||||
}
|
||||
pongChan <- 1
|
||||
continue
|
||||
}
|
||||
|
||||
if common.StringContains(message, "ping") {
|
||||
err = b.WebsocketConn.WriteJSON("pong")
|
||||
if err != nil {
|
||||
if b.Verbose {
|
||||
log.Println("Bitmex websocket error: ", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !timer.Reset(5 * time.Second) {
|
||||
log.Fatal("Bitmex websocket: Timer failed to set")
|
||||
}
|
||||
|
||||
quickCapture := make(map[string]interface{})
|
||||
err = common.JSONDecode(resp, &quickCapture)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var respError WebsocketErrorResponse
|
||||
if _, ok := quickCapture["status"]; ok {
|
||||
err = common.JSONDecode(resp, &respError)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("Bitmex websocket error: %s", respError.Error)
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := quickCapture["success"]; ok {
|
||||
var decodedResp WebsocketSubscribeResp
|
||||
err := common.JSONDecode(resp, &decodedResp)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if decodedResp.Success {
|
||||
if b.Verbose {
|
||||
if len(quickCapture) == 3 {
|
||||
log.Printf("Bitmex Websocket: Successfully subscribed to %s",
|
||||
decodedResp.Subscribe)
|
||||
} else {
|
||||
log.Println("Bitmex Websocket: Successfully authenticated websocket connection")
|
||||
}
|
||||
}
|
||||
case resp := <-b.Websocket.Intercomm:
|
||||
message := string(resp.Raw)
|
||||
if common.StringContains(message, "pong") {
|
||||
pongChan <- 1
|
||||
continue
|
||||
}
|
||||
log.Printf("Bitmex websocket error: Unable to subscribe %s",
|
||||
decodedResp.Subscribe)
|
||||
|
||||
} else if _, ok := quickCapture["table"]; ok {
|
||||
var decodedResp WebsocketMainResponse
|
||||
err := common.JSONDecode(resp, &decodedResp)
|
||||
if common.StringContains(message, "ping") {
|
||||
err := b.WebsocketConn.WriteJSON("pong")
|
||||
if err != nil {
|
||||
b.Websocket.DataHandler <- err
|
||||
}
|
||||
}
|
||||
|
||||
quickCapture := make(map[string]interface{})
|
||||
err := common.JSONDecode(resp.Raw, &quickCapture)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
switch decodedResp.Table {
|
||||
case bitmexWSOrderbookL2:
|
||||
var orderbooks OrderBookData
|
||||
err = common.JSONDecode(resp, &orderbooks)
|
||||
var respError WebsocketErrorResponse
|
||||
if _, ok := quickCapture["status"]; ok {
|
||||
err = common.JSONDecode(resp.Raw, &respError)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = b.processOrderbook(orderbooks.Data, orderbooks.Action)
|
||||
b.Websocket.DataHandler <- errors.New(respError.Error)
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := quickCapture["success"]; ok {
|
||||
var decodedResp WebsocketSubscribeResp
|
||||
err := common.JSONDecode(resp.Raw, &decodedResp)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
case bitmexWSTrade:
|
||||
var trades TradeData
|
||||
err = common.JSONDecode(resp, &trades)
|
||||
|
||||
if decodedResp.Success {
|
||||
if b.Verbose {
|
||||
if len(quickCapture) == 3 {
|
||||
log.Printf("Bitmex Websocket: Successfully subscribed to %s",
|
||||
decodedResp.Subscribe)
|
||||
} else {
|
||||
log.Println("Bitmex Websocket: Successfully authenticated websocket connection")
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
b.Websocket.DataHandler <- fmt.Errorf("Bitmex websocket error: Unable to subscribe %s",
|
||||
decodedResp.Subscribe)
|
||||
|
||||
} else if _, ok := quickCapture["table"]; ok {
|
||||
var decodedResp WebsocketMainResponse
|
||||
err := common.JSONDecode(resp.Raw, &decodedResp)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = b.processTrades(trades.Data, trades.Action)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
||||
switch decodedResp.Table {
|
||||
case bitmexWSOrderbookL2:
|
||||
var orderbooks OrderBookData
|
||||
err = common.JSONDecode(resp.Raw, &orderbooks)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
p := pair.NewCurrencyPairFromString(orderbooks.Data[0].Symbol)
|
||||
err = b.processOrderbook(orderbooks.Data, orderbooks.Action, p, "CONTRACT")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
case bitmexWSTrade:
|
||||
var trades TradeData
|
||||
err = common.JSONDecode(resp.Raw, &trades)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if trades.Action == bitmexActionInitialData {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, trade := range trades.Data {
|
||||
timestamp, err := time.Parse(time.RFC3339, trade.Timestamp)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
b.Websocket.DataHandler <- exchange.TradeData{
|
||||
Timestamp: timestamp,
|
||||
Price: trade.Price,
|
||||
Amount: float64(trade.Size),
|
||||
CurrencyPair: pair.NewCurrencyPairFromString(trade.Symbol),
|
||||
Exchange: b.GetName(),
|
||||
AssetType: "CONTRACT",
|
||||
Side: trade.Side,
|
||||
}
|
||||
}
|
||||
|
||||
case bitmexWSAnnouncement:
|
||||
var announcement AnnouncementData
|
||||
|
||||
err = common.JSONDecode(resp.Raw, &announcement)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if announcement.Action == bitmexActionInitialData {
|
||||
continue
|
||||
}
|
||||
|
||||
b.Websocket.DataHandler <- announcement.Data
|
||||
|
||||
default:
|
||||
log.Fatal("Bitmex websocket error: Table unknown -", decodedResp.Table)
|
||||
}
|
||||
case bitmexWSAnnouncement:
|
||||
var announcement AnnouncementData
|
||||
err = common.JSONDecode(resp, &announcement)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = b.processAnnouncement(announcement.Data, announcement.Action)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
default:
|
||||
log.Fatal("Bitmex websocket error: Table unknown -", decodedResp.Table)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Temporary local cache of Announcements
|
||||
var localAnnouncements []Announcement
|
||||
var partialLoadedAnnouncement bool
|
||||
|
||||
// ProcessAnnouncement process announcements
|
||||
func (b *Bitmex) processAnnouncement(data []Announcement, action string) error {
|
||||
switch action {
|
||||
case bitmexActionInitialData:
|
||||
if !partialLoadedAnnouncement {
|
||||
localAnnouncements = data
|
||||
}
|
||||
partialLoadedAnnouncement = true
|
||||
default:
|
||||
return fmt.Errorf("Bitmex websocket error: ProcessAnnouncement() unallocated action - %s",
|
||||
action)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Temporary local cache of orderbooks
|
||||
var localOb []OrderBookL2
|
||||
var partialLoaded bool
|
||||
var snapshotloaded = make(map[pair.CurrencyPair]map[string]bool)
|
||||
|
||||
// ProcessOrderbook processes orderbook updates
|
||||
func (b *Bitmex) processOrderbook(data []OrderBookL2, action string) error {
|
||||
switch action {
|
||||
case bitmexActionInitialData:
|
||||
if !partialLoaded {
|
||||
localOb = data
|
||||
}
|
||||
partialLoaded = true
|
||||
case bitmexActionUpdateData:
|
||||
if partialLoaded {
|
||||
updated := len(data)
|
||||
for _, elem := range data {
|
||||
for i := range localOb {
|
||||
if localOb[i].ID == elem.ID && localOb[i].Symbol == elem.Symbol {
|
||||
localOb[i].Side = elem.Side
|
||||
localOb[i].Size = elem.Size
|
||||
updated--
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if updated != 0 {
|
||||
return errors.New("Bitmex websocket error: Elements not updated correctly")
|
||||
}
|
||||
}
|
||||
case bitmexActionInsertData:
|
||||
if partialLoaded {
|
||||
updated := len(data)
|
||||
for _, elem := range data {
|
||||
localOb = append(localOb, OrderBookL2{
|
||||
Symbol: elem.Symbol,
|
||||
ID: elem.ID,
|
||||
Side: elem.Side,
|
||||
Size: elem.Size,
|
||||
Price: elem.Price,
|
||||
})
|
||||
updated--
|
||||
}
|
||||
if updated != 0 {
|
||||
return errors.New("Bitmex websocket error: Elements not updated correctly")
|
||||
}
|
||||
}
|
||||
case bitmexActionDeleteData:
|
||||
if partialLoaded {
|
||||
updated := len(data)
|
||||
for _, elem := range data {
|
||||
for i := range localOb {
|
||||
if localOb[i].ID == elem.ID && localOb[i].Symbol == elem.Symbol {
|
||||
localOb[i] = localOb[len(localOb)-1]
|
||||
localOb = localOb[:len(localOb)-1]
|
||||
updated--
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if updated != 0 {
|
||||
return errors.New("Bitmex websocket error: Elements not updated correctly")
|
||||
}
|
||||
}
|
||||
func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, currencyPair pair.CurrencyPair, assetType string) error {
|
||||
if len(data) < 1 {
|
||||
return errors.New("bitmex_websocket.go error - no orderbook data")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Temporary local cache of orderbooks
|
||||
var localTrades []Trade
|
||||
var partialLoadedTrades bool
|
||||
_, ok := snapshotloaded[currencyPair]
|
||||
if !ok {
|
||||
snapshotloaded[currencyPair] = make(map[string]bool)
|
||||
}
|
||||
|
||||
_, ok = snapshotloaded[currencyPair][assetType]
|
||||
if !ok {
|
||||
snapshotloaded[currencyPair][assetType] = false
|
||||
}
|
||||
|
||||
// ProcessTrades processes new trades that have occured
|
||||
func (b *Bitmex) processTrades(data []Trade, action string) error {
|
||||
switch action {
|
||||
case bitmexActionInitialData:
|
||||
if !partialLoadedTrades {
|
||||
localTrades = data
|
||||
}
|
||||
partialLoadedTrades = true
|
||||
case bitmexActionInsertData:
|
||||
if partialLoadedTrades {
|
||||
localTrades = append(localTrades, data...)
|
||||
if !snapshotloaded[currencyPair][assetType] {
|
||||
var newOrderbook orderbook.Base
|
||||
var bids, asks []orderbook.Item
|
||||
|
||||
for _, orderbookItem := range data {
|
||||
if orderbookItem.Side == "Sell" {
|
||||
asks = append(asks, orderbook.Item{
|
||||
Price: orderbookItem.Price,
|
||||
Amount: float64(orderbookItem.Size),
|
||||
})
|
||||
continue
|
||||
}
|
||||
bids = append(bids, orderbook.Item{
|
||||
Price: orderbookItem.Price,
|
||||
Amount: float64(orderbookItem.Size),
|
||||
})
|
||||
}
|
||||
|
||||
if len(bids) == 0 || len(asks) == 0 {
|
||||
return errors.New("bitmex_websocket.go error - snapshot not initialised correctly")
|
||||
}
|
||||
|
||||
newOrderbook.Asks = asks
|
||||
newOrderbook.Bids = bids
|
||||
newOrderbook.AssetType = assetType
|
||||
newOrderbook.CurrencyPair = currencyPair.Pair().String()
|
||||
newOrderbook.LastUpdated = time.Now()
|
||||
newOrderbook.Pair = currencyPair
|
||||
|
||||
err := b.Websocket.Orderbook.LoadSnapshot(newOrderbook, b.GetName())
|
||||
if err != nil {
|
||||
return fmt.Errorf("bitmex_websocket.go process orderbook error - %s",
|
||||
err)
|
||||
}
|
||||
snapshotloaded[currencyPair][assetType] = true
|
||||
b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{
|
||||
Pair: currencyPair,
|
||||
Asset: assetType,
|
||||
Exchange: b.GetName(),
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("Bitmex websocket error: ProcessTrades() unallocated action - %s",
|
||||
action)
|
||||
if snapshotloaded[currencyPair][assetType] {
|
||||
var asks, bids []orderbook.Item
|
||||
for _, orderbookItem := range data {
|
||||
if orderbookItem.Side == "Sell" {
|
||||
asks = append(asks, orderbook.Item{
|
||||
Price: orderbookItem.Price,
|
||||
Amount: float64(orderbookItem.Size),
|
||||
})
|
||||
continue
|
||||
}
|
||||
bids = append(bids, orderbook.Item{
|
||||
Price: orderbookItem.Price,
|
||||
Amount: float64(orderbookItem.Size),
|
||||
})
|
||||
}
|
||||
|
||||
err := b.Websocket.Orderbook.UpdateUsingID(bids,
|
||||
asks,
|
||||
currencyPair,
|
||||
time.Now(),
|
||||
b.GetName(),
|
||||
assetType,
|
||||
action)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{
|
||||
Pair: currencyPair,
|
||||
Asset: assetType,
|
||||
Exchange: b.GetName(),
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -444,51 +443,3 @@ func (b *Bitmex) websocketSendAuth() error {
|
||||
|
||||
return b.WebsocketConn.WriteJSON(sendAuth)
|
||||
}
|
||||
|
||||
// Shutdown to monitor and shut down routines package specific
|
||||
type Shutdown struct {
|
||||
c chan int
|
||||
routineCount int
|
||||
finishC chan int
|
||||
}
|
||||
|
||||
// NewRoutineManagement returns an new initial routine management system
|
||||
func (b *Bitmex) NewRoutineManagement() *Shutdown {
|
||||
return &Shutdown{
|
||||
c: make(chan int, 1),
|
||||
finishC: make(chan int, 1),
|
||||
}
|
||||
}
|
||||
|
||||
// AddRoutine adds a routine to the monitor and returns a channel
|
||||
func (r *Shutdown) addRoutine() chan int {
|
||||
log.Println("Bitmex Websocket: Routine added to monitor")
|
||||
r.routineCount++
|
||||
return r.c
|
||||
}
|
||||
|
||||
// RoutineShutdown sends a message to the finisher channel
|
||||
func (r *Shutdown) routineShutdown() {
|
||||
log.Println("Bitmex Websocket: Routine is shutting down")
|
||||
r.finishC <- 1
|
||||
}
|
||||
|
||||
// SignalShutdown signals a shutdown across routines
|
||||
func (r *Shutdown) SignalShutdown() {
|
||||
log.Println("Bitmex Websocket: Shutdown signal sending..")
|
||||
for i := 0; i < r.routineCount; i++ {
|
||||
log.Printf("Bitmex Websocket: Shutdown signal sent to routine %d", i+1)
|
||||
r.c <- 1
|
||||
}
|
||||
|
||||
for {
|
||||
<-r.finishC
|
||||
r.routineCount--
|
||||
if r.routineCount <= 0 {
|
||||
close(r.c)
|
||||
close(r.finishC)
|
||||
log.Println("Bitmex Websocket: All routines stopped")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ func (b *Bitmex) Start(wg *sync.WaitGroup) {
|
||||
// Run implements the Bitmex wrapper
|
||||
func (b *Bitmex) Run() {
|
||||
if b.Verbose {
|
||||
log.Printf("%s Websocket: %s. (url: %s).\n", b.GetName(), common.IsEnabled(b.Websocket), b.WebsocketURL)
|
||||
log.Printf("%s Websocket: %s. (url: %s).\n", b.GetName(), common.IsEnabled(b.Websocket.IsEnabled()), b.WebsocketURL)
|
||||
log.Printf("%s polling delay: %ds.\n", b.GetName(), b.RESTPollingDelay)
|
||||
log.Printf("%s %d currencies enabled: %s.\n", b.GetName(), len(b.EnabledPairs), b.EnabledPairs)
|
||||
}
|
||||
@@ -33,10 +33,9 @@ func (b *Bitmex) Run() {
|
||||
marketInfo, err := b.GetActiveInstruments(GenericRequestParams{})
|
||||
if err != nil {
|
||||
log.Printf("%s Failed to get available symbols.\n", b.GetName())
|
||||
|
||||
} else {
|
||||
|
||||
var exchangeProducts []string
|
||||
|
||||
for _, info := range marketInfo {
|
||||
exchangeProducts = append(exchangeProducts, info.Symbol)
|
||||
}
|
||||
@@ -193,3 +192,8 @@ func (b *Bitmex) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount fl
|
||||
func (b *Bitmex) WithdrawExchangeFiatFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) {
|
||||
return "", errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// GetWebsocket returns a pointer to the exchange websocket
|
||||
func (b *Bitmex) GetWebsocket() (*exchange.Websocket, error) {
|
||||
return b.Websocket, nil
|
||||
}
|
||||
|
||||
@@ -56,7 +56,8 @@ const (
|
||||
// Bitstamp is the overarching type across the bitstamp package
|
||||
type Bitstamp struct {
|
||||
exchange.Base
|
||||
Balance Balances
|
||||
Balance Balances
|
||||
WebsocketConn WebsocketConn
|
||||
}
|
||||
|
||||
// SetDefaults sets default for Bitstamp
|
||||
@@ -64,7 +65,6 @@ func (b *Bitstamp) SetDefaults() {
|
||||
b.Name = "Bitstamp"
|
||||
b.Enabled = false
|
||||
b.Verbose = false
|
||||
b.Websocket = false
|
||||
b.RESTPollingDelay = 10
|
||||
b.RequestCurrencyPairFormat.Delimiter = ""
|
||||
b.RequestCurrencyPairFormat.Uppercase = true
|
||||
@@ -79,6 +79,7 @@ func (b *Bitstamp) SetDefaults() {
|
||||
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
|
||||
b.APIUrlDefault = bitstampAPIURL
|
||||
b.APIUrl = b.APIUrlDefault
|
||||
b.WebsocketInit()
|
||||
}
|
||||
|
||||
// Setup sets configuration values to bitstamp
|
||||
@@ -93,7 +94,7 @@ func (b *Bitstamp) Setup(exch config.ExchangeConfig) {
|
||||
b.SetHTTPClientUserAgent(exch.HTTPUserAgent)
|
||||
b.RESTPollingDelay = exch.RESTPollingDelay
|
||||
b.Verbose = exch.Verbose
|
||||
b.Websocket = exch.Websocket
|
||||
b.Websocket.SetEnabled(exch.Websocket)
|
||||
b.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",")
|
||||
b.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",")
|
||||
b.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",")
|
||||
@@ -113,6 +114,18 @@ func (b *Bitstamp) Setup(exch config.ExchangeConfig) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = b.SetClientProxyAddress(exch.ProxyAddress)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = b.WebsocketSetup(b.WsConnect,
|
||||
exch.Name,
|
||||
exch.Websocket,
|
||||
BitstampPusherKey,
|
||||
exch.WebsocketURL)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ func TestSetDefaults(t *testing.T) {
|
||||
if b.Verbose != false {
|
||||
t.Error("Test Failed - SetDefaults() error")
|
||||
}
|
||||
if b.Websocket != false {
|
||||
if b.Websocket.IsEnabled() != false {
|
||||
t.Error("Test Failed - SetDefaults() error")
|
||||
}
|
||||
if b.RESTPollingDelay != 10 {
|
||||
@@ -47,7 +47,7 @@ func TestSetup(t *testing.T) {
|
||||
b.Setup(bConfig)
|
||||
|
||||
if !b.IsEnabled() || b.AuthenticatedAPISupport || b.RESTPollingDelay != time.Duration(10) ||
|
||||
b.Verbose || b.Websocket || len(b.BaseCurrencies) < 1 ||
|
||||
b.Verbose || b.Websocket.IsEnabled() || len(b.BaseCurrencies) < 1 ||
|
||||
len(b.AvailablePairs) < 1 || len(b.EnabledPairs) < 1 {
|
||||
t.Error("Test Failed - Bitstamp Setup values not set correctly")
|
||||
}
|
||||
|
||||
@@ -4,23 +4,47 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/thrasher-/gocryptotrader/common"
|
||||
"github.com/thrasher-/gocryptotrader/currency/pair"
|
||||
"github.com/thrasher-/gocryptotrader/exchanges"
|
||||
"github.com/thrasher-/gocryptotrader/exchanges/orderbook"
|
||||
"github.com/toorop/go-pusher"
|
||||
)
|
||||
|
||||
// WebsocketConn defins a pusher websocket connection
|
||||
type WebsocketConn struct {
|
||||
Client *pusher.Client
|
||||
Data chan *pusher.Event
|
||||
Trade chan *pusher.Event
|
||||
}
|
||||
|
||||
// PusherOrderbook holds order book information to be pushed
|
||||
type PusherOrderbook struct {
|
||||
Asks [][]string `json:"asks"`
|
||||
Bids [][]string `json:"bids"`
|
||||
Asks [][]string `json:"asks"`
|
||||
Bids [][]string `json:"bids"`
|
||||
Timestamp int64 `json:"timestamp,string"`
|
||||
}
|
||||
|
||||
// PusherTrade holds trade information to be pushed
|
||||
type PusherTrade struct {
|
||||
Price float64 `json:"price"`
|
||||
Amount float64 `json:"amount"`
|
||||
Price float64 `json:"price"`
|
||||
Amount float64 `json:"amount"`
|
||||
ID int64 `json:"id"`
|
||||
Type int64 `json:"type"`
|
||||
Timestamp int64 `json:"timestamp,string"`
|
||||
BuyOrderID int64 `json:"buy_order_id"`
|
||||
SellOrderID int64 `json:"sell_order_id"`
|
||||
}
|
||||
|
||||
// PusherOrders defines order information
|
||||
type PusherOrders struct {
|
||||
ID int64 `json:"id"`
|
||||
Amount float64 `json:"amount"`
|
||||
Price float64 `json:""`
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -28,6 +52,8 @@ const (
|
||||
BitstampPusherKey = "de504dc5763aeef9ff52"
|
||||
)
|
||||
|
||||
var tradingPairs map[string]string
|
||||
|
||||
// findPairFromChannel extracts the capitalized trading pair from the channel and returns it only if enabled in the config
|
||||
func (b *Bitstamp) findPairFromChannel(channelName string) (string, error) {
|
||||
split := strings.Split(channelName, "_")
|
||||
@@ -39,92 +65,214 @@ func (b *Bitstamp) findPairFromChannel(channelName string) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("Could not find trading pair")
|
||||
return "", errors.New("bistamp_websocket.go error - could not find trading pair")
|
||||
}
|
||||
|
||||
// PusherClient starts the push mechanism
|
||||
func (b *Bitstamp) PusherClient() {
|
||||
for b.Enabled && b.Websocket {
|
||||
// hold the mapping of channel:tradingPair in order not to always compute it
|
||||
seenTradingPairs := map[string]string{}
|
||||
// WsConnect connects to a websocket feed
|
||||
func (b *Bitstamp) WsConnect() error {
|
||||
if !b.Websocket.IsEnabled() || !b.IsEnabled() {
|
||||
return errors.New(exchange.WebsocketNotEnabled)
|
||||
}
|
||||
|
||||
pusherClient, err := pusher.NewClient(BitstampPusherKey)
|
||||
tradingPairs = make(map[string]string)
|
||||
var err error
|
||||
|
||||
if b.Websocket.GetProxyAddress() != "" {
|
||||
log.Println("bistamp_websocket.go warning - set proxy address error: proxy not supported")
|
||||
}
|
||||
|
||||
b.WebsocketConn.Client, err = pusher.NewClient(BitstampPusherKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s Unable to connect to Websocket. Error: %s",
|
||||
b.GetName(),
|
||||
err)
|
||||
}
|
||||
|
||||
b.WebsocketConn.Data, err = b.WebsocketConn.Client.Bind("data")
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s Websocket Bind error: %s", b.GetName(), err)
|
||||
|
||||
}
|
||||
|
||||
b.WebsocketConn.Trade, err = b.WebsocketConn.Client.Bind("trade")
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s Websocket Bind error: %s", b.GetName(), err)
|
||||
}
|
||||
|
||||
go b.WsReadData()
|
||||
|
||||
for _, p := range b.GetEnabledCurrencies() {
|
||||
orderbookSeed, err := b.GetOrderbook(p.Pair().String())
|
||||
if err != nil {
|
||||
log.Printf("%s Unable to connect to Websocket. Error: %s\n", b.GetName(), err)
|
||||
continue
|
||||
return err
|
||||
}
|
||||
|
||||
for _, pair := range b.EnabledPairs {
|
||||
err = pusherClient.Subscribe(fmt.Sprintf("live_trades_%s", strings.ToLower(pair)))
|
||||
var newOrderbook orderbook.Base
|
||||
|
||||
var asks []orderbook.Item
|
||||
for _, ask := range orderbookSeed.Asks {
|
||||
var item orderbook.Item
|
||||
item.Amount = ask.Amount
|
||||
item.Price = ask.Price
|
||||
asks = append(asks, item)
|
||||
}
|
||||
|
||||
var bids []orderbook.Item
|
||||
for _, bid := range orderbookSeed.Bids {
|
||||
var item orderbook.Item
|
||||
item.Amount = bid.Amount
|
||||
item.Price = bid.Price
|
||||
bids = append(bids, item)
|
||||
}
|
||||
|
||||
newOrderbook.Asks = asks
|
||||
newOrderbook.Bids = bids
|
||||
newOrderbook.CurrencyPair = p.Pair().String()
|
||||
newOrderbook.Pair = p
|
||||
newOrderbook.LastUpdated = time.Unix(0, orderbookSeed.Timestamp)
|
||||
newOrderbook.AssetType = "SPOT"
|
||||
|
||||
err = b.Websocket.Orderbook.LoadSnapshot(newOrderbook, b.GetName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{
|
||||
Pair: p,
|
||||
Asset: "SPOT",
|
||||
Exchange: b.GetName(),
|
||||
}
|
||||
|
||||
err = b.WebsocketConn.Client.Subscribe(fmt.Sprintf("live_trades_%s",
|
||||
strings.ToLower(p.Pair().String())))
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return fmt.Errorf("%s Websocket Trade subscription error: %s",
|
||||
b.GetName(),
|
||||
err)
|
||||
}
|
||||
|
||||
err = b.WebsocketConn.Client.Subscribe(fmt.Sprintf("diff_order_book_%s",
|
||||
strings.ToLower(p.Pair().String())))
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return fmt.Errorf("%s Websocket Trade subscription error: %s",
|
||||
b.GetName(),
|
||||
err)
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WsReadData reads data coming from bitstamp websocket connection
|
||||
func (b *Bitstamp) WsReadData() {
|
||||
b.Websocket.Wg.Add(1)
|
||||
|
||||
defer func() {
|
||||
err := b.WebsocketConn.Client.Close()
|
||||
if err != nil {
|
||||
b.Websocket.DataHandler <- fmt.Errorf("bitstamp_websocket.go - Unable to to close Websocket connection. Error: %s",
|
||||
err)
|
||||
}
|
||||
b.Websocket.Wg.Done()
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-b.Websocket.ShutdownC:
|
||||
return
|
||||
|
||||
case data := <-b.WebsocketConn.Data:
|
||||
b.Websocket.TrafficAlert <- struct{}{}
|
||||
|
||||
result := PusherOrderbook{}
|
||||
err := common.JSONDecode([]byte(data.Data), &result)
|
||||
if err != nil {
|
||||
log.Printf("%s Websocket Trade subscription error: %s\n", b.GetName(), err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = pusherClient.Subscribe(fmt.Sprintf("order_book_%s", strings.ToLower(pair)))
|
||||
currencyPair := common.SplitStrings(data.Channel, "_")
|
||||
p := pair.NewCurrencyPairFromString(common.StringToUpper(currencyPair[3]))
|
||||
|
||||
err = b.WsUpdateOrderbook(result, p, "SPOT")
|
||||
if err != nil {
|
||||
log.Printf("%s Websocket Trade subscription error: %s\n", b.GetName(), err)
|
||||
b.Websocket.DataHandler <- err
|
||||
}
|
||||
}
|
||||
|
||||
dataChannelTrade, err := pusherClient.Bind("data")
|
||||
if err != nil {
|
||||
log.Printf("%s Websocket Bind error: %s\n", b.GetName(), err)
|
||||
continue
|
||||
}
|
||||
case trade := <-b.WebsocketConn.Trade:
|
||||
b.Websocket.TrafficAlert <- struct{}{}
|
||||
|
||||
tradeChannelTrade, err := pusherClient.Bind("trade")
|
||||
if err != nil {
|
||||
log.Printf("%s Websocket Bind error: %s\n", b.GetName(), err)
|
||||
continue
|
||||
}
|
||||
result := PusherTrade{}
|
||||
err := common.JSONDecode([]byte(trade.Data), &result)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Printf("%s Pusher client connected.\n", b.GetName())
|
||||
currencyPair := common.SplitStrings(trade.Channel, "_")
|
||||
|
||||
for b.Websocket {
|
||||
select {
|
||||
case data := <-dataChannelTrade:
|
||||
result := PusherOrderbook{}
|
||||
err := common.JSONDecode([]byte(data.Data), &result)
|
||||
var channelTradingPair string
|
||||
var ok bool
|
||||
|
||||
if channelTradingPair, ok = seenTradingPairs[data.Channel]; !ok {
|
||||
if foundTradingPair, noPair := b.findPairFromChannel(data.Channel); noPair == nil {
|
||||
seenTradingPairs[data.Channel] = foundTradingPair
|
||||
} else {
|
||||
log.Printf("%s Pair from Channel: %s does not seem to be enabled or found", b.GetName(), data.Channel)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("%s Pusher: received ticker for Pair: %s\n", b.GetName(), channelTradingPair)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
case trade := <-tradeChannelTrade:
|
||||
result := PusherTrade{}
|
||||
err := common.JSONDecode([]byte(trade.Data), &result)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
var channelTradingPair string
|
||||
var ok bool
|
||||
|
||||
if channelTradingPair, ok = seenTradingPairs[trade.Channel]; !ok {
|
||||
if foundTradingPair, noPair := b.findPairFromChannel(trade.Channel); noPair == nil {
|
||||
seenTradingPairs[trade.Channel] = foundTradingPair
|
||||
} else {
|
||||
log.Printf("%s LiveTrade Pair from Channel: %s does not seem to be enabled or found", b.GetName(), trade.Channel)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
log.Println(trade.Channel)
|
||||
log.Printf("%s Pusher trade: Pair: %s Price: %f Amount: %f\n", b.GetName(), channelTradingPair, result.Price, result.Amount)
|
||||
b.Websocket.DataHandler <- exchange.TradeData{
|
||||
Price: result.Price,
|
||||
Amount: result.Amount,
|
||||
CurrencyPair: pair.NewCurrencyPairFromString(currencyPair[2]),
|
||||
Exchange: b.GetName(),
|
||||
AssetType: "SPOT",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WsUpdateOrderbook updates local cache of orderbook information
|
||||
func (b *Bitstamp) WsUpdateOrderbook(ob PusherOrderbook, p pair.CurrencyPair, assetType string) error {
|
||||
if len(ob.Asks) == 0 && len(ob.Bids) == 0 {
|
||||
return errors.New("bitstamp_websocket.go error - no orderbook data")
|
||||
}
|
||||
|
||||
var asks, bids []orderbook.Item
|
||||
if len(ob.Asks) > 0 {
|
||||
for _, ask := range ob.Asks {
|
||||
target, err := strconv.ParseFloat(ask[0], 64)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
amount, err := strconv.ParseFloat(ask[1], 64)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
asks = append(asks, orderbook.Item{Price: target, Amount: amount})
|
||||
}
|
||||
}
|
||||
|
||||
if len(ob.Bids) > 0 {
|
||||
for _, bid := range ob.Bids {
|
||||
target, err := strconv.ParseFloat(bid[0], 64)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
amount, err := strconv.ParseFloat(bid[1], 64)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
bids = append(bids, orderbook.Item{Price: target, Amount: amount})
|
||||
}
|
||||
}
|
||||
|
||||
err := b.Websocket.Orderbook.Update(bids, asks, p, time.Now(), b.GetName(), assetType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{
|
||||
Pair: p,
|
||||
Asset: assetType,
|
||||
Exchange: b.GetName(),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -25,15 +25,11 @@ func (b *Bitstamp) Start(wg *sync.WaitGroup) {
|
||||
// Run implements the Bitstamp wrapper
|
||||
func (b *Bitstamp) Run() {
|
||||
if b.Verbose {
|
||||
log.Printf("%s Websocket: %s.", b.GetName(), common.IsEnabled(b.Websocket))
|
||||
log.Printf("%s Websocket: %s.", b.GetName(), common.IsEnabled(b.Websocket.IsEnabled()))
|
||||
log.Printf("%s polling delay: %ds.\n", b.GetName(), b.RESTPollingDelay)
|
||||
log.Printf("%s %d currencies enabled: %s.\n", b.GetName(), len(b.EnabledPairs), b.EnabledPairs)
|
||||
}
|
||||
|
||||
if b.Websocket {
|
||||
go b.PusherClient()
|
||||
}
|
||||
|
||||
pairs, err := b.GetTradingPairs()
|
||||
if err != nil {
|
||||
log.Printf("%s failed to get trading pairs. Err: %s", b.Name, err)
|
||||
@@ -211,3 +207,8 @@ func (b *Bitstamp) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount
|
||||
func (b *Bitstamp) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) {
|
||||
return "", errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// GetWebsocket returns a pointer to the exchange websocket
|
||||
func (b *Bitstamp) GetWebsocket() (*exchange.Websocket, error) {
|
||||
return b.Websocket, nil
|
||||
}
|
||||
|
||||
@@ -67,7 +67,6 @@ func (b *Bittrex) SetDefaults() {
|
||||
b.Name = "Bittrex"
|
||||
b.Enabled = false
|
||||
b.Verbose = false
|
||||
b.Websocket = false
|
||||
b.RESTPollingDelay = 10
|
||||
b.RequestCurrencyPairFormat.Delimiter = "-"
|
||||
b.RequestCurrencyPairFormat.Uppercase = true
|
||||
@@ -82,6 +81,7 @@ func (b *Bittrex) SetDefaults() {
|
||||
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
|
||||
b.APIUrlDefault = bittrexAPIURL
|
||||
b.APIUrl = b.APIUrlDefault
|
||||
b.WebsocketInit()
|
||||
}
|
||||
|
||||
// Setup method sets current configuration details if enabled
|
||||
@@ -96,7 +96,6 @@ func (b *Bittrex) Setup(exch config.ExchangeConfig) {
|
||||
b.SetHTTPClientUserAgent(exch.HTTPUserAgent)
|
||||
b.RESTPollingDelay = exch.RESTPollingDelay
|
||||
b.Verbose = exch.Verbose
|
||||
b.Websocket = exch.Websocket
|
||||
b.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",")
|
||||
b.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",")
|
||||
b.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",")
|
||||
@@ -116,6 +115,10 @@ func (b *Bittrex) Setup(exch config.ExchangeConfig) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = b.SetClientProxyAddress(exch.ProxyAddress)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,8 +32,9 @@ func TestSetup(t *testing.T) {
|
||||
|
||||
b.Setup(bConfig)
|
||||
|
||||
if !b.IsEnabled() || b.AuthenticatedAPISupport || b.RESTPollingDelay != time.Duration(10) ||
|
||||
b.Verbose || b.Websocket || len(b.BaseCurrencies) < 1 ||
|
||||
if !b.IsEnabled() || b.AuthenticatedAPISupport ||
|
||||
b.RESTPollingDelay != time.Duration(10) || b.Verbose ||
|
||||
b.Websocket.IsEnabled() || len(b.BaseCurrencies) < 1 ||
|
||||
len(b.AvailablePairs) < 1 || len(b.EnabledPairs) < 1 {
|
||||
t.Error("Test Failed - Bittrex Setup values not set correctly")
|
||||
}
|
||||
|
||||
@@ -217,3 +217,8 @@ func (b *Bittrex) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount f
|
||||
func (b *Bittrex) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) {
|
||||
return "", errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// GetWebsocket returns a pointer to the exchange websocket
|
||||
func (b *Bittrex) GetWebsocket() (*exchange.Websocket, error) {
|
||||
return nil, errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
package btcc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/thrasher-/gocryptotrader/common"
|
||||
"github.com/thrasher-/gocryptotrader/config"
|
||||
"github.com/thrasher-/gocryptotrader/exchanges"
|
||||
@@ -17,39 +13,16 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
btccAPIUrl = "https://spotusd-data.btcc.com"
|
||||
btccAPIAuthenticatedMethod = "api_trade_v1.php"
|
||||
btccAPIVersion = "2.0.1.3"
|
||||
btccOrderBuy = "buyOrder2"
|
||||
btccOrderSell = "sellOrder2"
|
||||
btccOrderCancel = "cancelOrder"
|
||||
btccIcebergBuy = "buyIcebergOrder"
|
||||
btccIcebergSell = "sellIcebergOrder"
|
||||
btccIcebergOrder = "getIcebergOrder"
|
||||
btccIcebergOrders = "getIcebergOrders"
|
||||
btccIcebergCancel = "cancelIcebergOrder"
|
||||
btccAccountInfo = "getAccountInfo"
|
||||
btccDeposits = "getDeposits"
|
||||
btccMarketdepth = "getMarketDepth2"
|
||||
btccOrder = "getOrder"
|
||||
btccOrders = "getOrders"
|
||||
btccTransactions = "getTransactions"
|
||||
btccWithdrawal = "getWithdrawal"
|
||||
btccWithdrawals = "getWithdrawals"
|
||||
btccWithdrawalRequest = "requestWithdrawal"
|
||||
btccStoporderBuy = "buyStopOrder"
|
||||
btccStoporderSell = "sellStopOrder"
|
||||
btccStoporderCancel = "cancelStopOrder"
|
||||
btccStoporder = "getStopOrder"
|
||||
btccStoporders = "getStopOrders"
|
||||
|
||||
btccAuthRate = 0
|
||||
btccUnauthRate = 0
|
||||
)
|
||||
|
||||
// BTCC is the main overaching type across the BTCC package
|
||||
// NOTE this package is websocket connection dependant, the REST endpoints have
|
||||
// been dropped
|
||||
type BTCC struct {
|
||||
exchange.Base
|
||||
Conn *websocket.Conn
|
||||
}
|
||||
|
||||
// SetDefaults sets default values for the exchange
|
||||
@@ -58,10 +31,9 @@ func (b *BTCC) SetDefaults() {
|
||||
b.Enabled = false
|
||||
b.Fee = 0
|
||||
b.Verbose = false
|
||||
b.Websocket = false
|
||||
b.RESTPollingDelay = 10
|
||||
b.RequestCurrencyPairFormat.Delimiter = ""
|
||||
b.RequestCurrencyPairFormat.Uppercase = false
|
||||
b.RequestCurrencyPairFormat.Uppercase = true
|
||||
b.ConfigCurrencyPairFormat.Delimiter = ""
|
||||
b.ConfigCurrencyPairFormat.Uppercase = true
|
||||
b.AssetTypes = []string{ticker.Spot}
|
||||
@@ -71,8 +43,7 @@ func (b *BTCC) SetDefaults() {
|
||||
request.NewRateLimit(time.Second, btccAuthRate),
|
||||
request.NewRateLimit(time.Second, btccUnauthRate),
|
||||
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
|
||||
b.APIUrlDefault = btccAPIUrl
|
||||
b.APIUrl = b.APIUrlDefault
|
||||
b.WebsocketInit()
|
||||
}
|
||||
|
||||
// Setup is run on startup to setup exchange with config values
|
||||
@@ -87,7 +58,7 @@ func (b *BTCC) Setup(exch config.ExchangeConfig) {
|
||||
b.SetHTTPClientUserAgent(exch.HTTPUserAgent)
|
||||
b.RESTPollingDelay = exch.RESTPollingDelay
|
||||
b.Verbose = exch.Verbose
|
||||
b.Websocket = exch.Websocket
|
||||
b.Websocket.SetEnabled(exch.Websocket)
|
||||
b.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",")
|
||||
b.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",")
|
||||
b.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",")
|
||||
@@ -107,6 +78,18 @@ func (b *BTCC) Setup(exch config.ExchangeConfig) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = b.SetClientProxyAddress(exch.ProxyAddress)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = b.WebsocketSetup(b.WsConnect,
|
||||
exch.Name,
|
||||
exch.Websocket,
|
||||
btccSocketioAddress,
|
||||
exch.WebsocketURL)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,528 +97,3 @@ func (b *BTCC) Setup(exch config.ExchangeConfig) {
|
||||
func (b *BTCC) GetFee() float64 {
|
||||
return b.Fee
|
||||
}
|
||||
|
||||
// GetTicker returns ticker information
|
||||
// currencyPair - Example "btccny", "ltccny" or "ltcbtc"
|
||||
func (b *BTCC) GetTicker(currencyPair string) (Ticker, error) {
|
||||
resp := Response{}
|
||||
path := fmt.Sprintf("%s/data/pro/ticker?symbol=%s", b.APIUrl, currencyPair)
|
||||
return resp.Ticker, b.SendHTTPRequest(path, &resp)
|
||||
}
|
||||
|
||||
// GetTradeHistory returns trade history data
|
||||
// currencyPair - Example "btccny", "ltccny" or "ltcbtc"
|
||||
// limit - limits the returned trades example "10"
|
||||
// sinceTid - returns trade records starting from id supplied example "5000"
|
||||
// time - returns trade records starting from unix time 1406794449
|
||||
func (b *BTCC) GetTradeHistory(currencyPair string, limit, sinceTid int64, time time.Time) ([]Trade, error) {
|
||||
trades := []Trade{}
|
||||
path := fmt.Sprintf("%s/data/pro/historydata?symbol=%s", b.APIUrl, currencyPair)
|
||||
v := url.Values{}
|
||||
|
||||
if limit > 0 {
|
||||
v.Set("limit", strconv.FormatInt(limit, 10))
|
||||
}
|
||||
if sinceTid > 0 {
|
||||
v.Set("since", strconv.FormatInt(sinceTid, 10))
|
||||
}
|
||||
if !time.IsZero() {
|
||||
v.Set("sincetype", strconv.FormatInt(time.Unix(), 10))
|
||||
}
|
||||
|
||||
path = common.EncodeURLValues(path, v)
|
||||
return trades, b.SendHTTPRequest(path, &trades)
|
||||
}
|
||||
|
||||
// GetOrderBook returns current symbol order book
|
||||
// currencyPair - Example "btccny", "ltccny" or "ltcbtc"
|
||||
// limit - limits the returned trades example "10" if 0 will return full
|
||||
// orderbook
|
||||
func (b *BTCC) GetOrderBook(currencyPair string, limit int) (Orderbook, error) {
|
||||
result := Orderbook{}
|
||||
path := fmt.Sprintf("%s/data/pro/orderbook?symbol=%s&limit=%d", b.APIUrl, currencyPair, limit)
|
||||
if limit == 0 {
|
||||
path = fmt.Sprintf("%s/data/pro/orderbook?symbol=%s", b.APIUrl, currencyPair)
|
||||
}
|
||||
|
||||
return result, b.SendHTTPRequest(path, &result)
|
||||
}
|
||||
|
||||
// GetAccountInfo returns account information
|
||||
func (b *BTCC) GetAccountInfo(infoType string) error {
|
||||
params := make([]interface{}, 0)
|
||||
|
||||
if len(infoType) > 0 {
|
||||
params = append(params, infoType)
|
||||
}
|
||||
|
||||
return b.SendAuthenticatedHTTPRequest(btccAccountInfo, params)
|
||||
}
|
||||
|
||||
// PlaceOrder places a new order
|
||||
func (b *BTCC) PlaceOrder(buyOrder bool, price, amount float64, symbol string) {
|
||||
params := make([]interface{}, 0)
|
||||
params = append(params, strconv.FormatFloat(price, 'f', -1, 64))
|
||||
params = append(params, strconv.FormatFloat(amount, 'f', -1, 64))
|
||||
|
||||
if len(symbol) > 0 {
|
||||
params = append(params, symbol)
|
||||
}
|
||||
|
||||
req := btccOrderBuy
|
||||
if !buyOrder {
|
||||
req = btccOrderSell
|
||||
}
|
||||
|
||||
err := b.SendAuthenticatedHTTPRequest(req, params)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
// CancelOrder cancels an order
|
||||
func (b *BTCC) CancelOrder(orderID int64, symbol string) {
|
||||
params := make([]interface{}, 0)
|
||||
params = append(params, orderID)
|
||||
|
||||
if len(symbol) > 0 {
|
||||
params = append(params, symbol)
|
||||
}
|
||||
|
||||
err := b.SendAuthenticatedHTTPRequest(btccOrderCancel, params)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
// GetDeposits returns deposit information
|
||||
func (b *BTCC) GetDeposits(currency string, pending bool) {
|
||||
params := make([]interface{}, 0)
|
||||
params = append(params, currency)
|
||||
|
||||
if pending {
|
||||
params = append(params, pending)
|
||||
}
|
||||
|
||||
err := b.SendAuthenticatedHTTPRequest(btccDeposits, params)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
// GetMarketDepth returns market depth at limit
|
||||
func (b *BTCC) GetMarketDepth(symbol string, limit int64) {
|
||||
params := make([]interface{}, 0)
|
||||
|
||||
if limit > 0 {
|
||||
params = append(params, limit)
|
||||
}
|
||||
|
||||
if len(symbol) > 0 {
|
||||
params = append(params, symbol)
|
||||
}
|
||||
|
||||
err := b.SendAuthenticatedHTTPRequest(btccMarketdepth, params)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
// GetOrder returns information about a specific order
|
||||
func (b *BTCC) GetOrder(orderID int64, symbol string, detailed bool) {
|
||||
params := make([]interface{}, 0)
|
||||
params = append(params, orderID)
|
||||
|
||||
if len(symbol) > 0 {
|
||||
params = append(params, symbol)
|
||||
}
|
||||
|
||||
if detailed {
|
||||
params = append(params, detailed)
|
||||
}
|
||||
|
||||
err := b.SendAuthenticatedHTTPRequest(btccOrder, params)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
// GetOrders returns information of a range of orders
|
||||
func (b *BTCC) GetOrders(openonly bool, symbol string, limit, offset, since int64, detailed bool) {
|
||||
params := make([]interface{}, 0)
|
||||
|
||||
if openonly {
|
||||
params = append(params, openonly)
|
||||
}
|
||||
|
||||
if len(symbol) > 0 {
|
||||
params = append(params, symbol)
|
||||
}
|
||||
|
||||
if limit > 0 {
|
||||
params = append(params, limit)
|
||||
}
|
||||
|
||||
if offset > 0 {
|
||||
params = append(params, offset)
|
||||
}
|
||||
|
||||
if since > 0 {
|
||||
params = append(params, since)
|
||||
}
|
||||
|
||||
if detailed {
|
||||
params = append(params, detailed)
|
||||
}
|
||||
|
||||
err := b.SendAuthenticatedHTTPRequest(btccOrders, params)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
// GetTransactions returns transaction lists
|
||||
func (b *BTCC) GetTransactions(transType string, limit, offset, since int64, sinceType string) {
|
||||
params := make([]interface{}, 0)
|
||||
|
||||
if len(transType) > 0 {
|
||||
params = append(params, transType)
|
||||
}
|
||||
|
||||
if limit > 0 {
|
||||
params = append(params, limit)
|
||||
}
|
||||
|
||||
if offset > 0 {
|
||||
params = append(params, offset)
|
||||
}
|
||||
|
||||
if since > 0 {
|
||||
params = append(params, since)
|
||||
}
|
||||
|
||||
if len(sinceType) > 0 {
|
||||
params = append(params, sinceType)
|
||||
}
|
||||
|
||||
err := b.SendAuthenticatedHTTPRequest(btccTransactions, params)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
// GetWithdrawal returns information about a withdrawal process
|
||||
func (b *BTCC) GetWithdrawal(withdrawalID int64, currency string) {
|
||||
params := make([]interface{}, 0)
|
||||
params = append(params, withdrawalID)
|
||||
|
||||
if len(currency) > 0 {
|
||||
params = append(params, currency)
|
||||
}
|
||||
|
||||
err := b.SendAuthenticatedHTTPRequest(btccWithdrawal, params)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
// GetWithdrawals gets information about all withdrawals
|
||||
func (b *BTCC) GetWithdrawals(currency string, pending bool) {
|
||||
params := make([]interface{}, 0)
|
||||
params = append(params, currency)
|
||||
|
||||
if pending {
|
||||
params = append(params, pending)
|
||||
}
|
||||
|
||||
err := b.SendAuthenticatedHTTPRequest(btccWithdrawals, params)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
// RequestWithdrawal requests a new withdrawal
|
||||
func (b *BTCC) RequestWithdrawal(currency string, amount float64) {
|
||||
params := make([]interface{}, 0)
|
||||
params = append(params, currency)
|
||||
params = append(params, amount)
|
||||
|
||||
err := b.SendAuthenticatedHTTPRequest(btccWithdrawalRequest, params)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
// IcebergOrder intiates a large order but at intervals to preserve orderbook
|
||||
// integrity
|
||||
func (b *BTCC) IcebergOrder(buyOrder bool, price, amount, discAmount, variance float64, symbol string) {
|
||||
params := make([]interface{}, 0)
|
||||
params = append(params, strconv.FormatFloat(price, 'f', -1, 64))
|
||||
params = append(params, strconv.FormatFloat(amount, 'f', -1, 64))
|
||||
params = append(params, strconv.FormatFloat(discAmount, 'f', -1, 64))
|
||||
params = append(params, strconv.FormatFloat(variance, 'f', -1, 64))
|
||||
|
||||
if len(symbol) > 0 {
|
||||
params = append(params, symbol)
|
||||
}
|
||||
|
||||
req := btccIcebergBuy
|
||||
if !buyOrder {
|
||||
req = btccIcebergSell
|
||||
}
|
||||
|
||||
err := b.SendAuthenticatedHTTPRequest(req, params)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
// GetIcebergOrder returns information on your iceberg order
|
||||
func (b *BTCC) GetIcebergOrder(orderID int64, symbol string) {
|
||||
params := make([]interface{}, 0)
|
||||
params = append(params, orderID)
|
||||
|
||||
if len(symbol) > 0 {
|
||||
params = append(params, symbol)
|
||||
}
|
||||
|
||||
err := b.SendAuthenticatedHTTPRequest(btccIcebergOrder, params)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
// GetIcebergOrders returns information on all iceberg orders
|
||||
func (b *BTCC) GetIcebergOrders(limit, offset int64, symbol string) {
|
||||
params := make([]interface{}, 0)
|
||||
|
||||
if limit > 0 {
|
||||
params = append(params, limit)
|
||||
}
|
||||
|
||||
if offset > 0 {
|
||||
params = append(params, offset)
|
||||
}
|
||||
|
||||
if len(symbol) > 0 {
|
||||
params = append(params, symbol)
|
||||
}
|
||||
|
||||
err := b.SendAuthenticatedHTTPRequest(btccIcebergOrders, params)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
// CancelIcebergOrder cancels iceberg order
|
||||
func (b *BTCC) CancelIcebergOrder(orderID int64, symbol string) {
|
||||
params := make([]interface{}, 0)
|
||||
params = append(params, orderID)
|
||||
|
||||
if len(symbol) > 0 {
|
||||
params = append(params, symbol)
|
||||
}
|
||||
|
||||
err := b.SendAuthenticatedHTTPRequest(btccIcebergCancel, params)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
// PlaceStopOrder inserts a stop loss order
|
||||
func (b *BTCC) PlaceStopOrder(buyOder bool, stopPrice, price, amount, trailingAmt, trailingPct float64, symbol string) {
|
||||
params := make([]interface{}, 0)
|
||||
|
||||
if stopPrice > 0 {
|
||||
params = append(params, stopPrice)
|
||||
}
|
||||
|
||||
params = append(params, strconv.FormatFloat(price, 'f', -1, 64))
|
||||
params = append(params, strconv.FormatFloat(amount, 'f', -1, 64))
|
||||
|
||||
if trailingAmt > 0 {
|
||||
params = append(params, strconv.FormatFloat(trailingAmt, 'f', -1, 64))
|
||||
}
|
||||
|
||||
if trailingPct > 0 {
|
||||
params = append(params, strconv.FormatFloat(trailingPct, 'f', -1, 64))
|
||||
}
|
||||
|
||||
if len(symbol) > 0 {
|
||||
params = append(params, symbol)
|
||||
}
|
||||
|
||||
req := btccStoporderBuy
|
||||
if !buyOder {
|
||||
req = btccStoporderSell
|
||||
}
|
||||
|
||||
err := b.SendAuthenticatedHTTPRequest(req, params)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
// GetStopOrder returns a stop order
|
||||
func (b *BTCC) GetStopOrder(orderID int64, symbol string) {
|
||||
params := make([]interface{}, 0)
|
||||
params = append(params, orderID)
|
||||
|
||||
if len(symbol) > 0 {
|
||||
params = append(params, symbol)
|
||||
}
|
||||
|
||||
err := b.SendAuthenticatedHTTPRequest(btccStoporder, params)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
// GetStopOrders returns all stop orders
|
||||
func (b *BTCC) GetStopOrders(status, orderType string, stopPrice float64, limit, offset int64, symbol string) {
|
||||
params := make([]interface{}, 0)
|
||||
|
||||
if len(status) > 0 {
|
||||
params = append(params, status)
|
||||
}
|
||||
|
||||
if len(orderType) > 0 {
|
||||
params = append(params, orderType)
|
||||
}
|
||||
|
||||
if stopPrice > 0 {
|
||||
params = append(params, stopPrice)
|
||||
}
|
||||
|
||||
if limit > 0 {
|
||||
params = append(params, limit)
|
||||
}
|
||||
|
||||
if offset > 0 {
|
||||
params = append(params, limit)
|
||||
}
|
||||
|
||||
if len(symbol) > 0 {
|
||||
params = append(params, symbol)
|
||||
}
|
||||
|
||||
err := b.SendAuthenticatedHTTPRequest(btccStoporders, params)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
// CancelStopOrder cancels a stop order
|
||||
func (b *BTCC) CancelStopOrder(orderID int64, symbol string) {
|
||||
params := make([]interface{}, 0)
|
||||
params = append(params, orderID)
|
||||
|
||||
if len(symbol) > 0 {
|
||||
params = append(params, symbol)
|
||||
}
|
||||
|
||||
err := b.SendAuthenticatedHTTPRequest(btccStoporderCancel, params)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
// SendHTTPRequest sends an unauthenticated HTTP request
|
||||
func (b *BTCC) SendHTTPRequest(path string, result interface{}) error {
|
||||
return b.SendPayload("GET", path, nil, nil, result, false, b.Verbose)
|
||||
}
|
||||
|
||||
// SendAuthenticatedHTTPRequest sends a valid authenticated HTTP request
|
||||
func (b *BTCC) SendAuthenticatedHTTPRequest(method string, params []interface{}) (err error) {
|
||||
if !b.AuthenticatedAPISupport {
|
||||
return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, b.Name)
|
||||
}
|
||||
|
||||
if b.Nonce.Get() == 0 {
|
||||
b.Nonce.Set(time.Now().UnixNano())
|
||||
} else {
|
||||
b.Nonce.Inc()
|
||||
}
|
||||
encoded := fmt.Sprintf("tonce=%s&accesskey=%s&requestmethod=post&id=%d&method=%s¶ms=", b.Nonce.String()[0:16], b.APIKey, 1, method)
|
||||
|
||||
if len(params) == 0 {
|
||||
params = make([]interface{}, 0)
|
||||
} else {
|
||||
items := make([]string, 0)
|
||||
for _, x := range params {
|
||||
xType := fmt.Sprintf("%T", x)
|
||||
switch xType {
|
||||
case "int64", "int":
|
||||
{
|
||||
items = append(items, fmt.Sprintf("%d", x))
|
||||
}
|
||||
case "string":
|
||||
{
|
||||
items = append(items, fmt.Sprintf("%s", x))
|
||||
}
|
||||
case "float64":
|
||||
{
|
||||
items = append(items, fmt.Sprintf("%f", x))
|
||||
}
|
||||
case "bool":
|
||||
{
|
||||
if x == true {
|
||||
items = append(items, "1")
|
||||
} else {
|
||||
items = append(items, "")
|
||||
}
|
||||
}
|
||||
default:
|
||||
{
|
||||
items = append(items, fmt.Sprintf("%v", x))
|
||||
}
|
||||
}
|
||||
}
|
||||
encoded += common.JoinStrings(items, ",")
|
||||
}
|
||||
if b.Verbose {
|
||||
log.Println(encoded)
|
||||
}
|
||||
|
||||
hmac := common.GetHMAC(common.HashSHA1, []byte(encoded), []byte(b.APISecret))
|
||||
postData := make(map[string]interface{})
|
||||
postData["method"] = method
|
||||
postData["params"] = params
|
||||
postData["id"] = 1
|
||||
|
||||
apiURL := fmt.Sprintf("%s/%s", b.APIUrl, btccAPIAuthenticatedMethod)
|
||||
|
||||
data, err := common.JSONEncode(postData)
|
||||
if err != nil {
|
||||
return errors.New("Unable to JSON Marshal POST data")
|
||||
}
|
||||
|
||||
if b.Verbose {
|
||||
log.Printf("Sending POST request to %s calling method %s with params %s\n", apiURL, method, data)
|
||||
}
|
||||
|
||||
headers := make(map[string]string)
|
||||
headers["Content-type"] = "application/json-rpc"
|
||||
headers["Authorization"] = "Basic " + common.Base64Encode([]byte(b.APIKey+":"+common.HexEncodeToString(hmac)))
|
||||
headers["Json-Rpc-Tonce"] = b.Nonce.String()
|
||||
|
||||
return b.SendPayload("POST", apiURL, headers, strings.NewReader(string(data)), nil, true, b.Verbose)
|
||||
}
|
||||
|
||||
@@ -28,8 +28,9 @@ func TestSetup(t *testing.T) {
|
||||
}
|
||||
b.Setup(bConfig)
|
||||
|
||||
if !b.IsEnabled() || b.AuthenticatedAPISupport || b.RESTPollingDelay != time.Duration(10) ||
|
||||
b.Verbose || b.Websocket || len(b.BaseCurrencies) < 1 ||
|
||||
if !b.IsEnabled() || b.AuthenticatedAPISupport ||
|
||||
b.RESTPollingDelay != time.Duration(10) || b.Verbose ||
|
||||
b.Websocket.IsEnabled() || len(b.BaseCurrencies) < 1 ||
|
||||
len(b.AvailablePairs) < 1 || len(b.EnabledPairs) < 1 {
|
||||
t.Error("Test Failed - BTCC Setup values not set correctly")
|
||||
}
|
||||
@@ -41,38 +42,38 @@ func TestGetFee(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTicker(t *testing.T) {
|
||||
t.Skip()
|
||||
_, err := b.GetTicker("BTCUSD")
|
||||
if err != nil {
|
||||
t.Error("Test failed - GetTicker() error", err)
|
||||
}
|
||||
}
|
||||
// func TestGetTicker(t *testing.T) {
|
||||
// t.Skip()
|
||||
// _, err := b.GetTicker("BTCUSD")
|
||||
// if err != nil {
|
||||
// t.Error("Test failed - GetTicker() error", err)
|
||||
// }
|
||||
// }
|
||||
|
||||
func TestGetTradeHistory(t *testing.T) {
|
||||
t.Skip()
|
||||
_, err := b.GetTradeHistory("BTCUSD", 0, 0, time.Time{})
|
||||
if err != nil {
|
||||
t.Error("Test failed - GetTradeHistory() error", err)
|
||||
}
|
||||
}
|
||||
// func TestGetTradeHistory(t *testing.T) {
|
||||
// t.Skip()
|
||||
// _, err := b.GetTradeHistory("BTCUSD", 0, 0, time.Time{})
|
||||
// if err != nil {
|
||||
// t.Error("Test failed - GetTradeHistory() error", err)
|
||||
// }
|
||||
// }
|
||||
|
||||
func TestGetOrderBook(t *testing.T) {
|
||||
t.Skip()
|
||||
_, err := b.GetOrderBook("BTCUSD", 100)
|
||||
if err != nil {
|
||||
t.Error("Test failed - GetOrderBook() error", err)
|
||||
}
|
||||
_, err = b.GetOrderBook("BTCUSD", 0)
|
||||
if err != nil {
|
||||
t.Error("Test failed - GetOrderBook() error", err)
|
||||
}
|
||||
}
|
||||
// func TestGetOrderBook(t *testing.T) {
|
||||
// t.Skip()
|
||||
// _, err := b.GetOrderBook("BTCUSD", 100)
|
||||
// if err != nil {
|
||||
// t.Error("Test failed - GetOrderBook() error", err)
|
||||
// }
|
||||
// _, err = b.GetOrderBook("BTCUSD", 0)
|
||||
// if err != nil {
|
||||
// t.Error("Test failed - GetOrderBook() error", err)
|
||||
// }
|
||||
// }
|
||||
|
||||
func TestGetAccountInfo(t *testing.T) {
|
||||
t.Skip()
|
||||
err := b.GetAccountInfo("")
|
||||
if err == nil {
|
||||
t.Error("Test failed - GetAccountInfo() error", err)
|
||||
}
|
||||
}
|
||||
// func TestGetAccountInfo(t *testing.T) {
|
||||
// t.Skip()
|
||||
// err := b.GetAccountInfo("")
|
||||
// if err == nil {
|
||||
// t.Error("Test failed - GetAccountInfo() error", err)
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,12 +1,70 @@
|
||||
package btcc
|
||||
|
||||
// Response is the generalized response type
|
||||
type Response struct {
|
||||
Ticker Ticker `json:"ticker"`
|
||||
import "encoding/json"
|
||||
|
||||
// WsAllTickerData defines multiple ticker data
|
||||
type WsAllTickerData []WsTicker
|
||||
|
||||
// WsOutgoing defines outgoing JSON
|
||||
type WsOutgoing struct {
|
||||
Action string `json:"action"`
|
||||
Symbol string `json:"symbol,omitempty"`
|
||||
Count int `json:"count,omitempty"`
|
||||
Len int `json:"len,omitempty"`
|
||||
}
|
||||
|
||||
// Ticker holds basic ticker information
|
||||
type Ticker struct {
|
||||
// WsResponseMain defines the main websocket response
|
||||
type WsResponseMain struct {
|
||||
MsgType string `json:"MsgType"`
|
||||
CRID string `json:"CRID"`
|
||||
RC interface{} `json:"RC"`
|
||||
Reason string `json:"Reason"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
// WsOrderbookSnapshot defines an orderbook from the websocket
|
||||
type WsOrderbookSnapshot struct {
|
||||
Timestamp int64 `json:"Timestamp"`
|
||||
Symbol string `json:"Symbol"`
|
||||
Version int64 `json:"Version"`
|
||||
Type string `json:"Type"`
|
||||
Content string `json:"Content"`
|
||||
List []struct {
|
||||
Side string `json:"Side"`
|
||||
Size interface{} `json:"Size"`
|
||||
Price float64 `json:"Price"`
|
||||
} `json:"List"`
|
||||
MsgType string `json:"MsgType"`
|
||||
}
|
||||
|
||||
// WsOrderbookSnapshotOld defines an old orderbook from the websocket connection
|
||||
type WsOrderbookSnapshotOld struct {
|
||||
MsgType string `json:"MsgType"`
|
||||
Symbol string `json:"Symbol"`
|
||||
Data map[string][]interface{} `json:"Data"`
|
||||
Timestamp int64 `json:"Timestamp"`
|
||||
}
|
||||
|
||||
// WsTrades defines trading data from the websocket
|
||||
type WsTrades struct {
|
||||
Trades []struct {
|
||||
TID int64 `json:"TID"`
|
||||
Timestamp int64 `json:"Timestamp"`
|
||||
Symbol string `json:"Symbol"`
|
||||
Side string `json:"Side"`
|
||||
Size float64 `json:"Size"`
|
||||
Price float64 `json:"Price"`
|
||||
MsgType string `json:"MsgType"`
|
||||
} `json:"Trades"`
|
||||
RC int64 `json:"RC"`
|
||||
CRID string `json:"CRID"`
|
||||
Reason string `json:"Reason"`
|
||||
MsgType string `json:"MsgType"`
|
||||
}
|
||||
|
||||
// WsTicker defines ticker data from the websocket
|
||||
type WsTicker struct {
|
||||
Symbol string `json:"Symbol"`
|
||||
BidPrice float64 `json:"BidPrice"`
|
||||
AskPrice float64 `json:"AskPrice"`
|
||||
Open float64 `json:"Open"`
|
||||
@@ -20,177 +78,5 @@ type Ticker struct {
|
||||
Timestamp int64 `json:"Timestamp"`
|
||||
ExecutionLimitDown float64 `json:"ExecutionLimitDown"`
|
||||
ExecutionLimitUp float64 `json:"ExecutionLimitUp"`
|
||||
}
|
||||
|
||||
// Trade holds executed trade data
|
||||
type Trade struct {
|
||||
ID int64 `json:"Id"`
|
||||
Timestamp int64 `json:"Timestamp"`
|
||||
Price float64 `json:"Price"`
|
||||
Quantity float64 `json:"Quantity"`
|
||||
Side string `json:"Side"`
|
||||
}
|
||||
|
||||
// Orderbook holds orderbook data
|
||||
type Orderbook struct {
|
||||
Bids [][]float64 `json:"bids"`
|
||||
Asks [][]float64 `json:"asks"`
|
||||
Date int64 `json:"date"`
|
||||
}
|
||||
|
||||
// Profile holds profile information
|
||||
type Profile struct {
|
||||
Username string
|
||||
TradePasswordEnabled bool `json:"trade_password_enabled,bool"`
|
||||
OTPEnabled bool `json:"otp_enabled,bool"`
|
||||
TradeFee float64 `json:"trade_fee"`
|
||||
TradeFeeCNYLTC float64 `json:"trade_fee_cnyltc"`
|
||||
TradeFeeBTCLTC float64 `json:"trade_fee_btcltc"`
|
||||
DailyBTCLimit float64 `json:"daily_btc_limit"`
|
||||
DailyLTCLimit float64 `json:"daily_ltc_limit"`
|
||||
BTCDespoitAddress string `json:"btc_despoit_address"`
|
||||
BTCWithdrawalAddress string `json:"btc_withdrawal_address"`
|
||||
LTCDepositAddress string `json:"ltc_deposit_address"`
|
||||
LTCWithdrawalAddress string `json:"ltc_withdrawal_request"`
|
||||
APIKeyPermission int64 `json:"api_key_permission"`
|
||||
}
|
||||
|
||||
// CurrencyGeneric holds currency information
|
||||
type CurrencyGeneric struct {
|
||||
Currency string
|
||||
Symbol string
|
||||
Amount string
|
||||
AmountInt int64 `json:"amount_integer"`
|
||||
AmountDecimal float64 `json:"amount_decimal"`
|
||||
}
|
||||
|
||||
// Order holds order information
|
||||
type Order struct {
|
||||
ID int64
|
||||
Type string
|
||||
Price float64
|
||||
Currency string
|
||||
Amount float64
|
||||
AmountOrig float64 `json:"amount_original"`
|
||||
Date int64
|
||||
Status string
|
||||
Detail OrderDetail
|
||||
}
|
||||
|
||||
// OrderDetail holds order detail information
|
||||
type OrderDetail struct {
|
||||
Dateline int64
|
||||
Price float64
|
||||
Amount float64
|
||||
}
|
||||
|
||||
// Withdrawal holds withdrawal transaction information
|
||||
type Withdrawal struct {
|
||||
ID int64
|
||||
Address string
|
||||
Currency string
|
||||
Amount float64
|
||||
Date int64
|
||||
Transaction string
|
||||
Status string
|
||||
}
|
||||
|
||||
// Deposit holds deposit address information
|
||||
type Deposit struct {
|
||||
ID int64
|
||||
Address string
|
||||
Currency string
|
||||
Amount float64
|
||||
Date int64
|
||||
Status string
|
||||
}
|
||||
|
||||
// BidAsk holds bid and ask information
|
||||
type BidAsk struct {
|
||||
Price float64
|
||||
Amount float64
|
||||
}
|
||||
|
||||
// Depth holds order book depth
|
||||
type Depth struct {
|
||||
Bid []BidAsk
|
||||
Ask []BidAsk
|
||||
}
|
||||
|
||||
// Transaction holds transaction information
|
||||
type Transaction struct {
|
||||
ID int64
|
||||
Type string
|
||||
BTCAmount float64 `json:"btc_amount"`
|
||||
LTCAmount float64 `json:"ltc_amount"`
|
||||
CNYAmount float64 `json:"cny_amount"`
|
||||
Date int64
|
||||
}
|
||||
|
||||
// IcebergOrder holds iceberg lettuce
|
||||
type IcebergOrder struct {
|
||||
ID int64
|
||||
Type string
|
||||
Price float64
|
||||
Market string
|
||||
Amount float64
|
||||
AmountOrig float64 `json:"amount_original"`
|
||||
DisclosedAmount float64 `json:"disclosed_amount"`
|
||||
Variance float64
|
||||
Date int64
|
||||
Status string
|
||||
}
|
||||
|
||||
// StopOrder holds stop order information
|
||||
type StopOrder struct {
|
||||
ID int64
|
||||
Type string
|
||||
StopPrice float64 `json:"stop_price"`
|
||||
TrailingAmt float64 `json:"trailing_amount"`
|
||||
TrailingPct float64 `json:"trailing_percentage"`
|
||||
Price float64
|
||||
Market string
|
||||
Amount float64
|
||||
Date int64
|
||||
Status string
|
||||
OrderID int64 `json:"order_id"`
|
||||
}
|
||||
|
||||
// WebsocketOrder holds websocket order information
|
||||
type WebsocketOrder struct {
|
||||
Price float64 `json:"price"`
|
||||
TotalAmount float64 `json:"totalamount"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// WebsocketGroupOrder holds websocket group order book information
|
||||
type WebsocketGroupOrder struct {
|
||||
Asks []WebsocketOrder `json:"ask"`
|
||||
Bids []WebsocketOrder `json:"bid"`
|
||||
Market string `json:"market"`
|
||||
}
|
||||
|
||||
// WebsocketTrade holds websocket trade information
|
||||
type WebsocketTrade struct {
|
||||
Amount float64 `json:"amount"`
|
||||
Date float64 `json:"date"`
|
||||
Market string `json:"market"`
|
||||
Price float64 `json:"price"`
|
||||
TradeID float64 `json:"trade_id"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// WebsocketTicker holds websocket ticker information
|
||||
type WebsocketTicker struct {
|
||||
Buy float64 `json:"buy"`
|
||||
Date float64 `json:"date"`
|
||||
High float64 `json:"high"`
|
||||
Last float64 `json:"last"`
|
||||
Low float64 `json:"low"`
|
||||
Market string `json:"market"`
|
||||
Open float64 `json:"open"`
|
||||
PrevClose float64 `json:"prev_close"`
|
||||
Sell float64 `json:"sell"`
|
||||
Volume float64 `json:"vol"`
|
||||
Vwap float64 `json:"vwap"`
|
||||
MsgType string `json:"MsgType"`
|
||||
}
|
||||
|
||||
@@ -1,125 +1,569 @@
|
||||
package btcc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/thrasher-/gocryptotrader/common"
|
||||
"github.com/thrasher-/socketio"
|
||||
"github.com/thrasher-/gocryptotrader/currency/pair"
|
||||
exchange "github.com/thrasher-/gocryptotrader/exchanges"
|
||||
"github.com/thrasher-/gocryptotrader/exchanges/orderbook"
|
||||
)
|
||||
|
||||
const (
|
||||
btccSocketioAddress = "https://websocket.btcc.com"
|
||||
btccSocketioAddress = "wss://ws.btcc.com"
|
||||
|
||||
msgTypeHeartBeat = "Heartbeat"
|
||||
msgTypeGetActiveContracts = "GetActiveContractsResponse"
|
||||
msgTypeQuote = "QuoteResponse"
|
||||
msgTypeLogin = "LoginResponse"
|
||||
msgTypeAccountInfo = "AccountInfo"
|
||||
msgTypeExecReport = "ExecReport"
|
||||
msgTypePlaceOrder = "PlaceOrderResponse"
|
||||
msgTypeCancelAllOrders = "CancelAllOrdersResponse"
|
||||
msgTypeCancelOrder = "CancelOrderResponse"
|
||||
msgTypeCancelReplaceOrder = "CancelReplaceOrderResponse"
|
||||
msgTypeGetAccountInfo = "GetAccountInfoResponse"
|
||||
msgTypeRetrieveOrder = "RetrieveOrderResponse"
|
||||
msgTypeGetTrades = "GetTradesResponse"
|
||||
|
||||
msgTypeAllTickers = "AllTickersResponse"
|
||||
)
|
||||
|
||||
// BTCCSocket is a pointer to a IO socket
|
||||
var BTCCSocket *socketio.SocketIO
|
||||
var (
|
||||
mtx sync.Mutex
|
||||
)
|
||||
|
||||
// OnConnect gets information from the server when its connected
|
||||
func (b *BTCC) OnConnect(output chan socketio.Message) {
|
||||
if b.Verbose {
|
||||
log.Printf("%s Connected to Websocket.", b.GetName())
|
||||
// WsConnect initiates a websocket client connection
|
||||
func (b *BTCC) WsConnect() error {
|
||||
if !b.Websocket.IsEnabled() || !b.IsEnabled() {
|
||||
return errors.New(exchange.WebsocketNotEnabled)
|
||||
}
|
||||
|
||||
currencies := []string{}
|
||||
for _, x := range b.EnabledPairs {
|
||||
currency := common.StringToLower(x[3:] + x[0:3])
|
||||
currencies = append(currencies, currency)
|
||||
}
|
||||
endpoints := []string{"marketdata", "grouporder"}
|
||||
var dialer websocket.Dialer
|
||||
var err error
|
||||
|
||||
for _, x := range endpoints {
|
||||
for _, y := range currencies {
|
||||
channel := fmt.Sprintf(`"%s_%s"`, x, y)
|
||||
if b.Verbose {
|
||||
log.Printf("%s Websocket subscribing to channel: %s.", b.GetName(), channel)
|
||||
if b.Websocket.GetProxyAddress() != "" {
|
||||
prxy, err := url.Parse(b.Websocket.GetProxyAddress())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dialer.Proxy = http.ProxyURL(prxy)
|
||||
}
|
||||
|
||||
b.Conn, _, err = dialer.Dial(b.Websocket.GetWebsocketURL(), http.Header{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = b.WsUpdateCurrencyPairs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go b.WsReadData()
|
||||
go b.WsHandleData()
|
||||
|
||||
err = b.WsSubscribeToOrderbook()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = b.WsSubcribeToTicker()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.WsSubcribeToTrades()
|
||||
}
|
||||
|
||||
// WsReadData reads data from the websocket connection
|
||||
func (b *BTCC) WsReadData() {
|
||||
b.Websocket.Wg.Add(1)
|
||||
defer b.Websocket.Wg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-b.Websocket.ShutdownC:
|
||||
return
|
||||
|
||||
default:
|
||||
mtx.Lock()
|
||||
_, resp, err := b.Conn.ReadMessage()
|
||||
mtx.Unlock()
|
||||
if err != nil {
|
||||
b.Websocket.DataHandler <- err
|
||||
}
|
||||
|
||||
b.Websocket.TrafficAlert <- struct{}{}
|
||||
|
||||
b.Websocket.Intercomm <- exchange.WebsocketResponse{
|
||||
Raw: resp,
|
||||
}
|
||||
output <- socketio.CreateMessageEvent("subscribe", channel, b.OnMessage, BTCCSocket.Version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OnDisconnect alerts when disconnection occurs
|
||||
func (b *BTCC) OnDisconnect(output chan socketio.Message) {
|
||||
log.Printf("%s Disconnected from websocket server.. Reconnecting.\n", b.GetName())
|
||||
b.WebsocketClient()
|
||||
}
|
||||
// WsHandleData handles read data
|
||||
func (b *BTCC) WsHandleData() {
|
||||
b.Websocket.Wg.Add(1)
|
||||
defer b.Websocket.Wg.Done()
|
||||
|
||||
// OnError alerts when error occurs
|
||||
func (b *BTCC) OnError() {
|
||||
log.Printf("%s Error with Websocket connection.. Reconnecting.\n", b.GetName())
|
||||
b.WebsocketClient()
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-b.Websocket.ShutdownC:
|
||||
return
|
||||
|
||||
// OnMessage if message received and verbose it is printed out
|
||||
func (b *BTCC) OnMessage(message []byte, output chan socketio.Message) {
|
||||
if b.Verbose {
|
||||
log.Printf("%s Websocket message received which isn't handled by default.\n", b.GetName())
|
||||
log.Println(string(message))
|
||||
case resp := <-b.Websocket.Intercomm:
|
||||
var Result WsResponseMain
|
||||
err := common.JSONDecode(resp.Raw, &Result)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
switch Result.MsgType {
|
||||
case msgTypeHeartBeat:
|
||||
|
||||
case msgTypeGetActiveContracts:
|
||||
log.Println("Active Contracts")
|
||||
log.Fatal(string(resp.Raw))
|
||||
|
||||
case msgTypeQuote:
|
||||
log.Println("Quotes")
|
||||
log.Fatal(string(resp.Raw))
|
||||
|
||||
case msgTypeLogin:
|
||||
log.Println("Login")
|
||||
log.Fatal(string(resp.Raw))
|
||||
|
||||
case msgTypeAccountInfo:
|
||||
log.Println("Account info")
|
||||
log.Fatal(string(resp.Raw))
|
||||
|
||||
case msgTypeExecReport:
|
||||
log.Println("Exec Report")
|
||||
log.Fatal(string(resp.Raw))
|
||||
|
||||
case msgTypePlaceOrder:
|
||||
log.Println("Place order")
|
||||
log.Fatal(string(resp.Raw))
|
||||
|
||||
case msgTypeCancelAllOrders:
|
||||
log.Println("Cancel All orders")
|
||||
log.Fatal(string(resp.Raw))
|
||||
|
||||
case msgTypeCancelOrder:
|
||||
log.Println("Cancel order")
|
||||
log.Fatal(string(resp.Raw))
|
||||
|
||||
case msgTypeCancelReplaceOrder:
|
||||
log.Println("Replace order")
|
||||
log.Fatal(string(resp.Raw))
|
||||
|
||||
case msgTypeGetAccountInfo:
|
||||
log.Println("Account info")
|
||||
log.Fatal(string(resp.Raw))
|
||||
|
||||
case msgTypeRetrieveOrder:
|
||||
log.Println("Retrieve order")
|
||||
log.Fatal(string(resp.Raw))
|
||||
|
||||
case msgTypeGetTrades:
|
||||
var trades WsTrades
|
||||
|
||||
err := common.JSONDecode(resp.Raw, &trades)
|
||||
if err != nil {
|
||||
b.Websocket.DataHandler <- err
|
||||
continue
|
||||
}
|
||||
|
||||
case "OrderBook":
|
||||
// NOTE: This seems to be a websocket update not reflected in
|
||||
// current API docs, this comes in conjunction with the other
|
||||
// orderbook feeds
|
||||
var orderbook WsOrderbookSnapshot
|
||||
|
||||
err := common.JSONDecode(resp.Raw, &orderbook)
|
||||
if err != nil {
|
||||
b.Websocket.DataHandler <- err
|
||||
continue
|
||||
}
|
||||
|
||||
switch orderbook.Type {
|
||||
case "F":
|
||||
err = b.WsProcessOrderbookSnapshot(orderbook)
|
||||
if err != nil {
|
||||
b.Websocket.DataHandler <- err
|
||||
}
|
||||
|
||||
case "I":
|
||||
err = b.WsProcessOrderbookUpdate(orderbook)
|
||||
if err != nil {
|
||||
b.Websocket.DataHandler <- err
|
||||
}
|
||||
}
|
||||
|
||||
case "SubOrderBookResponse":
|
||||
|
||||
case "Ticker":
|
||||
var ticker WsTicker
|
||||
|
||||
err = common.JSONDecode(resp.Raw, &ticker)
|
||||
if err != nil {
|
||||
b.Websocket.DataHandler <- err
|
||||
continue
|
||||
}
|
||||
|
||||
tick := exchange.TickerData{}
|
||||
tick.AssetType = "SPOT"
|
||||
tick.ClosePrice = ticker.PrevCls
|
||||
tick.Exchange = b.GetName()
|
||||
tick.HighPrice = ticker.High
|
||||
tick.LowPrice = ticker.Low
|
||||
tick.OpenPrice = ticker.Open
|
||||
tick.Pair = pair.NewCurrencyPairFromString(ticker.Symbol)
|
||||
tick.Quantity = ticker.Volume
|
||||
timestamp := time.Unix(ticker.Timestamp, 0)
|
||||
tick.Timestamp = timestamp
|
||||
|
||||
b.Websocket.DataHandler <- tick
|
||||
|
||||
default:
|
||||
|
||||
if common.StringContains(Result.MsgType, "OrderBook") {
|
||||
var oldOrderbookType WsOrderbookSnapshotOld
|
||||
err = common.JSONDecode(resp.Raw, &oldOrderbookType)
|
||||
if err != nil {
|
||||
b.Websocket.DataHandler <- err
|
||||
continue
|
||||
}
|
||||
|
||||
symbol := common.SplitStrings(Result.MsgType, ".")
|
||||
err = b.WsProcessOldOrderbookSnapshot(oldOrderbookType, symbol[1])
|
||||
if err != nil {
|
||||
b.Websocket.DataHandler <- err
|
||||
continue
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OnTicker handles ticker information
|
||||
func (b *BTCC) OnTicker(message []byte, output chan socketio.Message) {
|
||||
type Response struct {
|
||||
Ticker WebsocketTicker `json:"ticker"`
|
||||
}
|
||||
var resp Response
|
||||
err := common.JSONDecode(message, &resp)
|
||||
// WsSubscribeAllTickers subscribes to a ticker channel
|
||||
func (b *BTCC) WsSubscribeAllTickers() error {
|
||||
mtx.Lock()
|
||||
defer mtx.Unlock()
|
||||
|
||||
return b.Conn.WriteJSON(WsOutgoing{
|
||||
Action: "SubscribeAllTickers",
|
||||
})
|
||||
}
|
||||
|
||||
// WsUnSubscribeAllTickers unsubscribes from a ticker channel
|
||||
func (b *BTCC) WsUnSubscribeAllTickers() error {
|
||||
mtx.Lock()
|
||||
defer mtx.Unlock()
|
||||
|
||||
return b.Conn.WriteJSON(WsOutgoing{
|
||||
Action: "UnSubscribeAllTickers",
|
||||
})
|
||||
}
|
||||
|
||||
// WsUpdateCurrencyPairs updates currency pairs from the websocket connection
|
||||
func (b *BTCC) WsUpdateCurrencyPairs() error {
|
||||
err := b.WsSubscribeAllTickers()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// OnGroupOrder handles group order information
|
||||
func (b *BTCC) OnGroupOrder(message []byte, output chan socketio.Message) {
|
||||
type Response struct {
|
||||
GroupOrder WebsocketGroupOrder `json:"grouporder"`
|
||||
}
|
||||
var resp Response
|
||||
err := common.JSONDecode(message, &resp)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// OnTrade handles group trade information
|
||||
func (b *BTCC) OnTrade(message []byte, output chan socketio.Message) {
|
||||
trade := WebsocketTrade{}
|
||||
err := common.JSONDecode(message, &trade)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// WebsocketClient initiates a websocket client
|
||||
func (b *BTCC) WebsocketClient() {
|
||||
events := make(map[string]func(message []byte, output chan socketio.Message))
|
||||
events["grouporder"] = b.OnGroupOrder
|
||||
events["ticker"] = b.OnTicker
|
||||
events["trade"] = b.OnTrade
|
||||
|
||||
BTCCSocket = &socketio.SocketIO{
|
||||
Version: 1,
|
||||
OnConnect: b.OnConnect,
|
||||
OnEvent: events,
|
||||
OnError: b.OnError,
|
||||
OnMessage: b.OnMessage,
|
||||
OnDisconnect: b.OnDisconnect,
|
||||
return err
|
||||
}
|
||||
|
||||
for b.Enabled && b.Websocket {
|
||||
err := socketio.ConnectToSocket(btccSocketioAddress, BTCCSocket)
|
||||
var currencyResponse WsResponseMain
|
||||
for {
|
||||
_, resp, err := b.Conn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Printf("%s Unable to connect to Websocket. Err: %s\n", b.GetName(), err)
|
||||
return err
|
||||
}
|
||||
|
||||
b.Websocket.TrafficAlert <- struct{}{}
|
||||
|
||||
err = common.JSONDecode(resp, ¤cyResponse)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch currencyResponse.MsgType {
|
||||
case msgTypeAllTickers:
|
||||
var tickers WsAllTickerData
|
||||
err := common.JSONDecode(currencyResponse.Data, &tickers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var availableTickers []string
|
||||
for _, tickerData := range tickers {
|
||||
availableTickers = append(availableTickers, tickerData.Symbol)
|
||||
}
|
||||
|
||||
err = b.UpdateCurrencies(availableTickers, false, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s failed to update available currencies. %s",
|
||||
b.Name,
|
||||
err)
|
||||
}
|
||||
|
||||
return b.WsUnSubscribeAllTickers()
|
||||
|
||||
case "Heartbeat":
|
||||
|
||||
default:
|
||||
return fmt.Errorf("btcc_websocket.go error - Updating currency pairs resp incorrect: %s",
|
||||
string(resp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WsSubscribeToOrderbook subscribes to an orderbook channel
|
||||
func (b *BTCC) WsSubscribeToOrderbook() error {
|
||||
mtx.Lock()
|
||||
defer mtx.Unlock()
|
||||
|
||||
for _, pair := range b.GetEnabledCurrencies() {
|
||||
formattedPair := exchange.FormatExchangeCurrency(b.GetName(), pair)
|
||||
err := b.Conn.WriteJSON(WsOutgoing{
|
||||
Action: "SubOrderBook",
|
||||
Symbol: formattedPair.String(),
|
||||
Len: 100})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WsSubcribeToTicker subscribes to a ticker channel
|
||||
func (b *BTCC) WsSubcribeToTicker() error {
|
||||
mtx.Lock()
|
||||
defer mtx.Unlock()
|
||||
|
||||
for _, pair := range b.GetEnabledCurrencies() {
|
||||
formattedPair := exchange.FormatExchangeCurrency(b.GetName(), pair)
|
||||
err := b.Conn.WriteJSON(WsOutgoing{
|
||||
Action: "Subscribe",
|
||||
Symbol: formattedPair.String(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WsSubcribeToTrades subscribes to a trade channel
|
||||
func (b *BTCC) WsSubcribeToTrades() error {
|
||||
mtx.Lock()
|
||||
defer mtx.Unlock()
|
||||
|
||||
for _, pair := range b.GetEnabledCurrencies() {
|
||||
formattedPair := exchange.FormatExchangeCurrency(b.GetName(), pair)
|
||||
err := b.Conn.WriteJSON(WsOutgoing{
|
||||
Action: "GetTrades",
|
||||
Symbol: formattedPair.String(),
|
||||
Count: 100,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WsProcessOrderbookSnapshot processes a new orderbook snapshot
|
||||
func (b *BTCC) WsProcessOrderbookSnapshot(ob WsOrderbookSnapshot) error {
|
||||
var asks, bids []orderbook.Item
|
||||
for _, data := range ob.List {
|
||||
var newSize float64
|
||||
switch data.Size.(type) {
|
||||
case float64:
|
||||
newSize = data.Size.(float64)
|
||||
case string:
|
||||
var err error
|
||||
newSize, err = strconv.ParseFloat(data.Size.(string), 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if data.Side == "1" {
|
||||
asks = append(asks, orderbook.Item{Price: data.Price, Amount: newSize})
|
||||
continue
|
||||
}
|
||||
log.Printf("%s Disconnected from Websocket.\n", b.GetName())
|
||||
|
||||
bids = append(bids, orderbook.Item{Price: data.Price, Amount: newSize})
|
||||
}
|
||||
|
||||
var newOrderbook orderbook.Base
|
||||
|
||||
newOrderbook.Asks = asks
|
||||
newOrderbook.AssetType = "SPOT"
|
||||
newOrderbook.Bids = bids
|
||||
newOrderbook.CurrencyPair = ob.Symbol
|
||||
newOrderbook.LastUpdated = time.Now()
|
||||
newOrderbook.Pair = pair.NewCurrencyPairFromString(ob.Symbol)
|
||||
|
||||
err := b.Websocket.Orderbook.LoadSnapshot(newOrderbook, b.GetName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{
|
||||
Exchange: b.GetName(),
|
||||
Asset: "SPOT",
|
||||
Pair: pair.NewCurrencyPairFromString(ob.Symbol),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WsProcessOrderbookUpdate processes an orderbook update
|
||||
func (b *BTCC) WsProcessOrderbookUpdate(ob WsOrderbookSnapshot) error {
|
||||
var asks, bids []orderbook.Item
|
||||
for _, data := range ob.List {
|
||||
var newSize float64
|
||||
switch data.Size.(type) {
|
||||
case float64:
|
||||
newSize = data.Size.(float64)
|
||||
case string:
|
||||
var err error
|
||||
newSize, err = strconv.ParseFloat(data.Size.(string), 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if data.Side == "1" {
|
||||
if newSize < 0 {
|
||||
asks = append(asks, orderbook.Item{Price: data.Price, Amount: 0})
|
||||
continue
|
||||
}
|
||||
asks = append(asks, orderbook.Item{Price: data.Price, Amount: newSize})
|
||||
continue
|
||||
}
|
||||
|
||||
if newSize < 0 {
|
||||
bids = append(bids, orderbook.Item{Price: data.Price, Amount: 0})
|
||||
continue
|
||||
}
|
||||
|
||||
bids = append(bids, orderbook.Item{Price: data.Price, Amount: newSize})
|
||||
}
|
||||
|
||||
p := pair.NewCurrencyPairFromString(ob.Symbol)
|
||||
|
||||
err := b.Websocket.Orderbook.Update(bids, asks, p, time.Now(), b.GetName(), "SPOT")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{
|
||||
Exchange: b.GetName(),
|
||||
Asset: "SPOT",
|
||||
Pair: pair.NewCurrencyPairFromString(ob.Symbol),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WsProcessOldOrderbookSnapshot processes an old orderbook snapshot
|
||||
func (b *BTCC) WsProcessOldOrderbookSnapshot(ob WsOrderbookSnapshotOld, symbol string) error {
|
||||
var asks, bids []orderbook.Item
|
||||
|
||||
askData, _ := ob.Data["Asks"]
|
||||
bidData, _ := ob.Data["Bids"]
|
||||
|
||||
for _, ask := range askData {
|
||||
data := ask.([]interface{})
|
||||
var price, amount float64
|
||||
|
||||
switch data[0].(type) {
|
||||
case string:
|
||||
var err error
|
||||
price, err = strconv.ParseFloat(data[0].(string), 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case float64:
|
||||
price = data[0].(float64)
|
||||
}
|
||||
|
||||
switch data[0].(type) {
|
||||
case string:
|
||||
var err error
|
||||
amount, err = strconv.ParseFloat(data[0].(string), 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case float64:
|
||||
amount = data[0].(float64)
|
||||
}
|
||||
|
||||
asks = append(asks, orderbook.Item{
|
||||
Price: price,
|
||||
Amount: amount,
|
||||
})
|
||||
}
|
||||
|
||||
for _, bid := range bidData {
|
||||
data := bid.([]interface{})
|
||||
var price, amount float64
|
||||
|
||||
switch data[1].(type) {
|
||||
case string:
|
||||
var err error
|
||||
price, err = strconv.ParseFloat(data[1].(string), 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case float64:
|
||||
price = data[1].(float64)
|
||||
}
|
||||
|
||||
switch data[1].(type) {
|
||||
case string:
|
||||
var err error
|
||||
amount, err = strconv.ParseFloat(data[1].(string), 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case float64:
|
||||
amount = data[1].(float64)
|
||||
}
|
||||
|
||||
bids = append(bids, orderbook.Item{
|
||||
Price: price,
|
||||
Amount: amount,
|
||||
})
|
||||
}
|
||||
|
||||
p := pair.NewCurrencyPairFromString(symbol)
|
||||
|
||||
err := b.Websocket.Orderbook.Update(bids, asks, p, time.Now(), b.GetName(), "SPOT")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{
|
||||
Exchange: b.GetName(),
|
||||
Pair: p,
|
||||
Asset: "SPOT",
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -25,15 +25,11 @@ func (b *BTCC) Start(wg *sync.WaitGroup) {
|
||||
// Run implements the BTCC wrapper
|
||||
func (b *BTCC) Run() {
|
||||
if b.Verbose {
|
||||
log.Printf("%s Websocket: %s.", b.GetName(), common.IsEnabled(b.Websocket))
|
||||
log.Printf("%s Websocket: %s.", b.GetName(), common.IsEnabled(b.Websocket.IsEnabled()))
|
||||
log.Printf("%s polling delay: %ds.\n", b.GetName(), b.RESTPollingDelay)
|
||||
log.Printf("%s %d currencies enabled: %s.\n", b.GetName(), len(b.EnabledPairs), b.EnabledPairs)
|
||||
}
|
||||
|
||||
if b.Websocket {
|
||||
go b.WebsocketClient()
|
||||
}
|
||||
|
||||
if common.StringDataContains(b.EnabledPairs, "CNY") || common.StringDataContains(b.AvailablePairs, "CNY") || common.StringDataContains(b.BaseCurrencies, "CNY") {
|
||||
log.Println("WARNING: BTCC only supports BTCUSD now, upgrading available, enabled and base currencies to BTCUSD/USD")
|
||||
pairs := []string{"BTCUSD"}
|
||||
@@ -69,82 +65,89 @@ func (b *BTCC) Run() {
|
||||
|
||||
// UpdateTicker updates and returns the ticker for a currency pair
|
||||
func (b *BTCC) UpdateTicker(p pair.CurrencyPair, assetType string) (ticker.Price, error) {
|
||||
var tickerPrice ticker.Price
|
||||
tick, err := b.GetTicker(exchange.FormatExchangeCurrency(b.GetName(), p).String())
|
||||
if err != nil {
|
||||
return tickerPrice, err
|
||||
}
|
||||
tickerPrice.Pair = p
|
||||
tickerPrice.Ask = tick.AskPrice
|
||||
tickerPrice.Bid = tick.BidPrice
|
||||
tickerPrice.Low = tick.Low
|
||||
tickerPrice.Last = tick.Last
|
||||
tickerPrice.Volume = tick.Volume24H
|
||||
tickerPrice.High = tick.High
|
||||
ticker.ProcessTicker(b.GetName(), p, tickerPrice, assetType)
|
||||
return ticker.GetTicker(b.Name, p, assetType)
|
||||
// var tickerPrice ticker.Price
|
||||
// tick, err := b.GetTicker(exchange.FormatExchangeCurrency(b.GetName(), p).String())
|
||||
// if err != nil {
|
||||
// return tickerPrice, err
|
||||
// }
|
||||
// tickerPrice.Pair = p
|
||||
// tickerPrice.Ask = tick.AskPrice
|
||||
// tickerPrice.Bid = tick.BidPrice
|
||||
// tickerPrice.Low = tick.Low
|
||||
// tickerPrice.Last = tick.Last
|
||||
// tickerPrice.Volume = tick.Volume24H
|
||||
// tickerPrice.High = tick.High
|
||||
// ticker.ProcessTicker(b.GetName(), p, tickerPrice, assetType)
|
||||
// return ticker.GetTicker(b.Name, p, assetType)
|
||||
return ticker.Price{}, errors.New("REST NOT SUPPORTED")
|
||||
}
|
||||
|
||||
// GetTickerPrice returns the ticker for a currency pair
|
||||
func (b *BTCC) GetTickerPrice(p pair.CurrencyPair, assetType string) (ticker.Price, error) {
|
||||
tickerNew, err := ticker.GetTicker(b.GetName(), p, assetType)
|
||||
if err != nil {
|
||||
return b.UpdateTicker(p, assetType)
|
||||
}
|
||||
return tickerNew, nil
|
||||
// tickerNew, err := ticker.GetTicker(b.GetName(), p, assetType)
|
||||
// if err != nil {
|
||||
// return b.UpdateTicker(p, assetType)
|
||||
// }
|
||||
// return tickerNew, nil
|
||||
return ticker.Price{}, errors.New("REST NOT SUPPORTED")
|
||||
}
|
||||
|
||||
// GetOrderbookEx returns the orderbook for a currency pair
|
||||
func (b *BTCC) GetOrderbookEx(p pair.CurrencyPair, assetType string) (orderbook.Base, error) {
|
||||
ob, err := orderbook.GetOrderbook(b.GetName(), p, assetType)
|
||||
if err != nil {
|
||||
return b.UpdateOrderbook(p, assetType)
|
||||
}
|
||||
return ob, nil
|
||||
// ob, err := orderbook.GetOrderbook(b.GetName(), p, assetType)
|
||||
// if err != nil {
|
||||
// return b.UpdateOrderbook(p, assetType)
|
||||
// }
|
||||
// return ob, nil
|
||||
return orderbook.Base{}, errors.New("REST NOT SUPPORTED")
|
||||
}
|
||||
|
||||
// UpdateOrderbook updates and returns the orderbook for a currency pair
|
||||
func (b *BTCC) UpdateOrderbook(p pair.CurrencyPair, assetType string) (orderbook.Base, error) {
|
||||
var orderBook orderbook.Base
|
||||
orderbookNew, err := b.GetOrderBook(exchange.FormatExchangeCurrency(b.GetName(), p).String(), 100)
|
||||
if err != nil {
|
||||
return orderBook, err
|
||||
}
|
||||
// var orderBook orderbook.Base
|
||||
// orderbookNew, err := b.GetOrderBook(exchange.FormatExchangeCurrency(b.GetName(), p).String(), 100)
|
||||
// if err != nil {
|
||||
// return orderBook, err
|
||||
// }
|
||||
|
||||
for x := range orderbookNew.Bids {
|
||||
data := orderbookNew.Bids[x]
|
||||
orderBook.Bids = append(orderBook.Bids, orderbook.Item{Price: data[0], Amount: data[1]})
|
||||
}
|
||||
// for x := range orderbookNew.Bids {
|
||||
// data := orderbookNew.Bids[x]
|
||||
// orderBook.Bids = append(orderBook.Bids, orderbook.Item{Price: data[0], Amount: data[1]})
|
||||
// }
|
||||
|
||||
for x := range orderbookNew.Asks {
|
||||
data := orderbookNew.Asks[x]
|
||||
orderBook.Asks = append(orderBook.Asks, orderbook.Item{Price: data[0], Amount: data[1]})
|
||||
}
|
||||
// for x := range orderbookNew.Asks {
|
||||
// data := orderbookNew.Asks[x]
|
||||
// orderBook.Asks = append(orderBook.Asks, orderbook.Item{Price: data[0], Amount: data[1]})
|
||||
// }
|
||||
|
||||
orderbook.ProcessOrderbook(b.GetName(), p, orderBook, assetType)
|
||||
return orderbook.GetOrderbook(b.Name, p, assetType)
|
||||
// orderbook.ProcessOrderbook(b.GetName(), p, orderBook, assetType)
|
||||
// return orderbook.GetOrderbook(b.Name, p, assetType)
|
||||
return orderbook.Base{}, errors.New("REST NOT SUPPORTED")
|
||||
}
|
||||
|
||||
// GetExchangeAccountInfo : Retrieves balances for all enabled currencies for
|
||||
// the Kraken exchange - TODO
|
||||
func (b *BTCC) GetExchangeAccountInfo() (exchange.AccountInfo, error) {
|
||||
var response exchange.AccountInfo
|
||||
response.ExchangeName = b.GetName()
|
||||
return response, nil
|
||||
// var response exchange.AccountInfo
|
||||
// response.ExchangeName = b.GetName()
|
||||
// return response, nil
|
||||
return exchange.AccountInfo{}, errors.New("REST NOT SUPPORTED")
|
||||
}
|
||||
|
||||
// GetExchangeFundTransferHistory returns funding history, deposits and
|
||||
// withdrawals
|
||||
func (b *BTCC) GetExchangeFundTransferHistory() ([]exchange.FundHistory, error) {
|
||||
var fundHistory []exchange.FundHistory
|
||||
return fundHistory, errors.New("not supported on exchange")
|
||||
// var fundHistory []exchange.FundHistory
|
||||
// return fundHistory, errors.New("not supported on exchange")
|
||||
return nil, errors.New("REST NOT SUPPORTED")
|
||||
}
|
||||
|
||||
// GetExchangeHistory returns historic trade data since exchange opening.
|
||||
func (b *BTCC) GetExchangeHistory(p pair.CurrencyPair, assetType string) ([]exchange.TradeHistory, error) {
|
||||
var resp []exchange.TradeHistory
|
||||
// var resp []exchange.TradeHistory
|
||||
|
||||
return resp, errors.New("trade history not yet implemented")
|
||||
// return resp, errors.New("trade history not yet implemented")
|
||||
return nil, errors.New("REST NOT SUPPORTED")
|
||||
}
|
||||
|
||||
// SubmitExchangeOrder submits a new order
|
||||
@@ -196,3 +199,8 @@ func (b *BTCC) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount floa
|
||||
func (b *BTCC) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) {
|
||||
return "", errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// GetWebsocket returns a pointer to the exchange websocket
|
||||
func (b *BTCC) GetWebsocket() (*exchange.Websocket, error) {
|
||||
return b.Websocket, nil
|
||||
}
|
||||
|
||||
@@ -54,7 +54,6 @@ func (b *BTCMarkets) SetDefaults() {
|
||||
b.Enabled = false
|
||||
b.Fee = 0.85
|
||||
b.Verbose = false
|
||||
b.Websocket = false
|
||||
b.RESTPollingDelay = 10
|
||||
b.Ticker = make(map[string]Ticker)
|
||||
b.RequestCurrencyPairFormat.Delimiter = ""
|
||||
@@ -70,6 +69,7 @@ func (b *BTCMarkets) SetDefaults() {
|
||||
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
|
||||
b.APIUrlDefault = btcMarketsAPIURL
|
||||
b.APIUrl = b.APIUrlDefault
|
||||
b.WebsocketInit()
|
||||
}
|
||||
|
||||
// Setup takes in an exchange configuration and sets all parameters
|
||||
@@ -84,7 +84,6 @@ func (b *BTCMarkets) Setup(exch config.ExchangeConfig) {
|
||||
b.SetHTTPClientUserAgent(exch.HTTPUserAgent)
|
||||
b.RESTPollingDelay = exch.RESTPollingDelay
|
||||
b.Verbose = exch.Verbose
|
||||
b.Websocket = exch.Websocket
|
||||
b.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",")
|
||||
b.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",")
|
||||
b.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",")
|
||||
@@ -104,6 +103,10 @@ func (b *BTCMarkets) Setup(exch config.ExchangeConfig) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = b.SetClientProxyAddress(exch.ProxyAddress)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -250,3 +250,8 @@ func (b *BTCMarkets) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amoun
|
||||
func (b *BTCMarkets) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) {
|
||||
return "", errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// GetWebsocket returns a pointer to the exchange websocket
|
||||
func (b *BTCMarkets) GetWebsocket() (*exchange.Websocket, error) {
|
||||
return nil, errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/thrasher-/gocryptotrader/common"
|
||||
"github.com/thrasher-/gocryptotrader/config"
|
||||
"github.com/thrasher-/gocryptotrader/exchanges"
|
||||
@@ -56,6 +57,7 @@ const (
|
||||
// CoinbasePro is the overarching type across the coinbasepro package
|
||||
type CoinbasePro struct {
|
||||
exchange.Base
|
||||
WebsocketConn *websocket.Conn
|
||||
}
|
||||
|
||||
// SetDefaults sets default values for the exchange
|
||||
@@ -65,7 +67,6 @@ func (c *CoinbasePro) SetDefaults() {
|
||||
c.Verbose = false
|
||||
c.TakerFee = 0.25
|
||||
c.MakerFee = 0
|
||||
c.Websocket = false
|
||||
c.RESTPollingDelay = 10
|
||||
c.RequestCurrencyPairFormat.Delimiter = "-"
|
||||
c.RequestCurrencyPairFormat.Uppercase = true
|
||||
@@ -80,6 +81,7 @@ func (c *CoinbasePro) SetDefaults() {
|
||||
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
|
||||
c.APIUrlDefault = coinbaseproAPIURL
|
||||
c.APIUrl = c.APIUrlDefault
|
||||
c.WebsocketInit()
|
||||
}
|
||||
|
||||
// Setup initialises the exchange parameters with the current configuration
|
||||
@@ -94,7 +96,7 @@ func (c *CoinbasePro) Setup(exch config.ExchangeConfig) {
|
||||
c.SetHTTPClientUserAgent(exch.HTTPUserAgent)
|
||||
c.RESTPollingDelay = exch.RESTPollingDelay
|
||||
c.Verbose = exch.Verbose
|
||||
c.Websocket = exch.Websocket
|
||||
c.Websocket.SetEnabled(exch.Websocket)
|
||||
c.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",")
|
||||
c.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",")
|
||||
c.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",")
|
||||
@@ -117,6 +119,18 @@ func (c *CoinbasePro) Setup(exch config.ExchangeConfig) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = c.SetClientProxyAddress(exch.ProxyAddress)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = c.WebsocketSetup(c.WsConnect,
|
||||
exch.Name,
|
||||
exch.Websocket,
|
||||
coinbaseproWebsocketURL,
|
||||
exch.WebsocketURL)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -341,55 +341,68 @@ type FillResponse struct {
|
||||
|
||||
// WebsocketSubscribe takes in subscription information
|
||||
type WebsocketSubscribe struct {
|
||||
Type string `json:"type"`
|
||||
ProductID string `json:"product_id"`
|
||||
Type string `json:"type"`
|
||||
ProductID string `json:"product_id,omitempty"`
|
||||
Channels []WsChannels `json:"channels,omitempty"`
|
||||
}
|
||||
|
||||
// WsChannels defines outgoing channels for subscription purposes
|
||||
type WsChannels struct {
|
||||
Name string `json:"name"`
|
||||
ProductIDs []string `json:"product_ids"`
|
||||
}
|
||||
|
||||
// WebsocketReceived holds websocket received values
|
||||
type WebsocketReceived struct {
|
||||
Type string `json:"type"`
|
||||
Time string `json:"time"`
|
||||
Sequence int `json:"sequence"`
|
||||
OrderID string `json:"order_id"`
|
||||
Size float64 `json:"size,string"`
|
||||
Price float64 `json:"price,string"`
|
||||
Side string `json:"side"`
|
||||
Type string `json:"type"`
|
||||
OrderID string `json:"order_id"`
|
||||
OrderType string `json:"order_type"`
|
||||
Size float64 `json:"size,string"`
|
||||
Price float64 `json:"price,string"`
|
||||
Side string `json:"side"`
|
||||
ClientOID string `json:"client_oid"`
|
||||
ProductID string `json:"product_id"`
|
||||
Sequence int64 `json:"sequence"`
|
||||
Time string `json:"time"`
|
||||
}
|
||||
|
||||
// WebsocketOpen collates open orders
|
||||
type WebsocketOpen struct {
|
||||
Type string `json:"type"`
|
||||
Time string `json:"time"`
|
||||
Sequence int `json:"sequence"`
|
||||
OrderID string `json:"order_id"`
|
||||
Price float64 `json:"price,string"`
|
||||
RemainingSize float64 `json:"remaining_size,string"`
|
||||
Side string `json:"side"`
|
||||
Price float64 `json:"price,string"`
|
||||
OrderID string `json:"order_id"`
|
||||
RemainingSize float64 `json:"remaining_size,string"`
|
||||
ProductID string `json:"product_id"`
|
||||
Sequence int64 `json:"sequence"`
|
||||
Time string `json:"time"`
|
||||
}
|
||||
|
||||
// WebsocketDone holds finished order information
|
||||
type WebsocketDone struct {
|
||||
Type string `json:"type"`
|
||||
Time string `json:"time"`
|
||||
Sequence int `json:"sequence"`
|
||||
Price float64 `json:"price,string"`
|
||||
Side string `json:"side"`
|
||||
OrderID string `json:"order_id"`
|
||||
Reason string `json:"reason"`
|
||||
Side string `json:"side"`
|
||||
ProductID string `json:"product_id"`
|
||||
Price float64 `json:"price,string"`
|
||||
RemainingSize float64 `json:"remaining_size,string"`
|
||||
Sequence int64 `json:"sequence"`
|
||||
Time string `json:"time"`
|
||||
}
|
||||
|
||||
// WebsocketMatch holds match information
|
||||
type WebsocketMatch struct {
|
||||
Type string `json:"type"`
|
||||
TradeID int `json:"trade_id"`
|
||||
Sequence int `json:"sequence"`
|
||||
MakerOrderID string `json:"maker_order_id"`
|
||||
TakerOrderID string `json:"taker_order_id"`
|
||||
Time string `json:"time"`
|
||||
Side string `json:"side"`
|
||||
Size float64 `json:"size,string"`
|
||||
Price float64 `json:"price,string"`
|
||||
Side string `json:"side"`
|
||||
ProductID string `json:"product_id"`
|
||||
Sequence int64 `json:"sequence"`
|
||||
Time string `json:"time"`
|
||||
}
|
||||
|
||||
// WebsocketChange holds change information
|
||||
@@ -403,3 +416,43 @@ type WebsocketChange struct {
|
||||
Price float64 `json:"price,string"`
|
||||
Side string `json:"side"`
|
||||
}
|
||||
|
||||
// WebsocketHeartBeat defines JSON response for a heart beat message
|
||||
type WebsocketHeartBeat struct {
|
||||
Type string `json:"type"`
|
||||
Sequence int64 `json:"sequence"`
|
||||
LastTradeID int64 `json:"last_trade_id"`
|
||||
ProductID string `json:"product_id"`
|
||||
Time string `json:"time"`
|
||||
}
|
||||
|
||||
// WebsocketTicker defines ticker websocket response
|
||||
type WebsocketTicker struct {
|
||||
Type string `json:"type"`
|
||||
Sequence int64 `json:"sequence"`
|
||||
ProductID string `json:"product_id"`
|
||||
Price float64 `json:"price,string"`
|
||||
Open24H float64 `json:"open_24h,string"`
|
||||
Volume24H float64 `json:"volumen_24h,string"`
|
||||
Low24H float64 `json:"low_24h,string"`
|
||||
High24H float64 `json:"high_24h,string"`
|
||||
Volume30D float64 `json:"volume_30d,string"`
|
||||
BestBid float64 `json:"best_bid,string"`
|
||||
BestAsk float64 `json:"best_ask,string"`
|
||||
}
|
||||
|
||||
// WebsocketOrderbookSnapshot defines a snapshot reponse
|
||||
type WebsocketOrderbookSnapshot struct {
|
||||
ProductID string `json:"product_id"`
|
||||
Type string `json:"type"`
|
||||
Bids [][]interface{} `json:"bids"`
|
||||
Asks [][]interface{} `json:"asks"`
|
||||
}
|
||||
|
||||
// WebsocketL2Update defines an update on the L2 orderbooks
|
||||
type WebsocketL2Update struct {
|
||||
Type string `json:"type"`
|
||||
ProductID string `json:"product_id"`
|
||||
Time string `json:"time"`
|
||||
Changes [][]interface{} `json:"changes"`
|
||||
}
|
||||
|
||||
@@ -1,127 +1,293 @@
|
||||
package coinbasepro
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/thrasher-/gocryptotrader/common"
|
||||
"github.com/thrasher-/gocryptotrader/currency/pair"
|
||||
"github.com/thrasher-/gocryptotrader/exchanges"
|
||||
"github.com/thrasher-/gocryptotrader/exchanges/orderbook"
|
||||
)
|
||||
|
||||
const (
|
||||
coinbaseproWebsocketURL = "wss://ws-feed.pro.coinbase.com"
|
||||
)
|
||||
|
||||
// WebsocketSubscribe subscribes to a websocket connection
|
||||
func (c *CoinbasePro) WebsocketSubscribe(product string, conn *websocket.Conn) error {
|
||||
subscribe := WebsocketSubscribe{"subscribe", product}
|
||||
// WebsocketSubscriber subscribes to websocket channels with respect to enabled
|
||||
// currencies
|
||||
func (c *CoinbasePro) WebsocketSubscriber() error {
|
||||
currencies := []string{}
|
||||
for _, x := range c.EnabledPairs {
|
||||
currency := x[0:3] + "-" + x[3:]
|
||||
currencies = append(currencies, currency)
|
||||
}
|
||||
|
||||
var channels []WsChannels
|
||||
channels = append(channels, WsChannels{
|
||||
Name: "heartbeat",
|
||||
ProductIDs: currencies,
|
||||
})
|
||||
|
||||
channels = append(channels, WsChannels{
|
||||
Name: "ticker",
|
||||
ProductIDs: currencies,
|
||||
})
|
||||
|
||||
channels = append(channels, WsChannels{
|
||||
Name: "level2",
|
||||
ProductIDs: currencies,
|
||||
})
|
||||
|
||||
subscribe := WebsocketSubscribe{Type: "subscribe", Channels: channels}
|
||||
|
||||
json, err := common.JSONEncode(subscribe)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = conn.WriteMessage(websocket.TextMessage, json)
|
||||
return c.WebsocketConn.WriteMessage(websocket.TextMessage, json)
|
||||
}
|
||||
|
||||
// WsConnect initiates a websocket connection
|
||||
func (c *CoinbasePro) WsConnect() error {
|
||||
if !c.Websocket.IsEnabled() || !c.IsEnabled() {
|
||||
return errors.New(exchange.WebsocketNotEnabled)
|
||||
}
|
||||
|
||||
var dialer websocket.Dialer
|
||||
|
||||
if c.Websocket.GetProxyAddress() != "" {
|
||||
proxy, err := url.Parse(c.Websocket.GetProxyAddress())
|
||||
if err != nil {
|
||||
return fmt.Errorf("coinbasepro_websocket.go error - proxy address %s",
|
||||
err)
|
||||
}
|
||||
|
||||
dialer.Proxy = http.ProxyURL(proxy)
|
||||
}
|
||||
|
||||
var err error
|
||||
c.WebsocketConn, _, err = dialer.Dial(c.Websocket.GetWebsocketURL(),
|
||||
http.Header{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("coinbasepro_websocket.go error - unable to connect to websocket %s",
|
||||
err)
|
||||
}
|
||||
|
||||
err = c.WebsocketSubscriber()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go c.WsReadData()
|
||||
go c.WsHandleData()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WebsocketClient initiates a websocket client
|
||||
func (c *CoinbasePro) WebsocketClient() {
|
||||
for c.Enabled && c.Websocket {
|
||||
var Dialer websocket.Dialer
|
||||
conn, _, err := Dialer.Dial(coinbaseproWebsocketURL, http.Header{})
|
||||
// WsReadData reads data from the websocket connection
|
||||
func (c *CoinbasePro) WsReadData() {
|
||||
c.Websocket.Wg.Add(1)
|
||||
|
||||
defer func() {
|
||||
err := c.WebsocketConn.Close()
|
||||
if err != nil {
|
||||
log.Printf("%s Unable to connect to Websocket. Error: %s\n", c.GetName(), err)
|
||||
continue
|
||||
c.Websocket.DataHandler <- fmt.Errorf("coinbasepro_websocket.go - Unable to to close Websocket connection. Error: %s",
|
||||
err)
|
||||
}
|
||||
c.Websocket.Wg.Done()
|
||||
}()
|
||||
|
||||
log.Printf("%s Connected to Websocket.\n", c.GetName())
|
||||
for {
|
||||
select {
|
||||
case <-c.Websocket.ShutdownC:
|
||||
return
|
||||
|
||||
currencies := []string{}
|
||||
for _, x := range c.EnabledPairs {
|
||||
currency := x[0:3] + "-" + x[3:]
|
||||
currencies = append(currencies, currency)
|
||||
}
|
||||
|
||||
for _, x := range currencies {
|
||||
err = c.WebsocketSubscribe(x, conn)
|
||||
default:
|
||||
_, resp, err := c.WebsocketConn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Printf("%s Websocket subscription error: %s\n", c.GetName(), err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if c.Verbose {
|
||||
log.Printf("%s Subscribed to product messages.", c.GetName())
|
||||
}
|
||||
|
||||
for c.Enabled && c.Websocket {
|
||||
msgType, resp, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
break
|
||||
c.Websocket.DataHandler <- err
|
||||
return
|
||||
}
|
||||
|
||||
switch msgType {
|
||||
case websocket.TextMessage:
|
||||
type MsgType struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
msgType := MsgType{}
|
||||
err := common.JSONDecode(resp, &msgType)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
|
||||
switch msgType.Type {
|
||||
case "error":
|
||||
log.Println(string(resp))
|
||||
break
|
||||
case "received":
|
||||
received := WebsocketReceived{}
|
||||
err := common.JSONDecode(resp, &received)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
case "open":
|
||||
open := WebsocketOpen{}
|
||||
err := common.JSONDecode(resp, &open)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
case "done":
|
||||
done := WebsocketDone{}
|
||||
err := common.JSONDecode(resp, &done)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
case "match":
|
||||
match := WebsocketMatch{}
|
||||
err := common.JSONDecode(resp, &match)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
case "change":
|
||||
change := WebsocketChange{}
|
||||
err := common.JSONDecode(resp, &change)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
c.Websocket.TrafficAlert <- struct{}{}
|
||||
c.Websocket.Intercomm <- exchange.WebsocketResponse{Raw: resp}
|
||||
}
|
||||
conn.Close()
|
||||
log.Printf("%s Websocket client disconnected.", c.GetName())
|
||||
}
|
||||
}
|
||||
|
||||
// WsHandleData handles read data from websocket connection
|
||||
func (c *CoinbasePro) WsHandleData() {
|
||||
c.Websocket.Wg.Add(1)
|
||||
defer c.Websocket.Wg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.Websocket.ShutdownC:
|
||||
return
|
||||
|
||||
case resp := <-c.Websocket.Intercomm:
|
||||
type MsgType struct {
|
||||
Type string `json:"type"`
|
||||
Sequence int64 `json:"sequence"`
|
||||
ProductID string `json:"product_id"`
|
||||
}
|
||||
|
||||
msgType := MsgType{}
|
||||
err := common.JSONDecode(resp.Raw, &msgType)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if msgType.Type == "subscriptions" || msgType.Type == "heartbeat" {
|
||||
continue
|
||||
}
|
||||
|
||||
switch msgType.Type {
|
||||
case "error":
|
||||
c.Websocket.DataHandler <- errors.New(string(resp.Raw))
|
||||
|
||||
case "ticker":
|
||||
ticker := WebsocketTicker{}
|
||||
err := common.JSONDecode(resp.Raw, &ticker)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
c.Websocket.DataHandler <- exchange.TickerData{
|
||||
Timestamp: time.Now(),
|
||||
Pair: pair.NewCurrencyPairFromString(ticker.ProductID),
|
||||
AssetType: "SPOT",
|
||||
Exchange: c.GetName(),
|
||||
OpenPrice: ticker.Price,
|
||||
HighPrice: ticker.High24H,
|
||||
LowPrice: ticker.Low24H,
|
||||
Quantity: ticker.Volume24H,
|
||||
}
|
||||
|
||||
case "snapshot":
|
||||
snapshot := WebsocketOrderbookSnapshot{}
|
||||
err := common.JSONDecode(resp.Raw, &snapshot)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = c.ProcessSnapshot(snapshot)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
case "l2update":
|
||||
update := WebsocketL2Update{}
|
||||
err := common.JSONDecode(resp.Raw, &update)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = c.ProcessUpdate(update)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
default:
|
||||
log.Fatal("Edge test", string(resp.Raw))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessSnapshot processes the intial orderbook snap shot
|
||||
func (c *CoinbasePro) ProcessSnapshot(snapshot WebsocketOrderbookSnapshot) error {
|
||||
var base orderbook.Base
|
||||
for _, bid := range snapshot.Bids {
|
||||
price, err := strconv.ParseFloat(bid[0].(string), 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
amount, err := strconv.ParseFloat(bid[1].(string), 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
base.Bids = append(base.Bids,
|
||||
orderbook.Item{Price: price, Amount: amount})
|
||||
}
|
||||
|
||||
for _, ask := range snapshot.Asks {
|
||||
price, err := strconv.ParseFloat(ask[0].(string), 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
amount, err := strconv.ParseFloat(ask[1].(string), 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
base.Asks = append(base.Asks,
|
||||
orderbook.Item{Price: price, Amount: amount})
|
||||
}
|
||||
|
||||
p := pair.NewCurrencyPairFromString(snapshot.ProductID)
|
||||
|
||||
base.AssetType = "SPOT"
|
||||
base.Pair = p
|
||||
base.CurrencyPair = snapshot.ProductID
|
||||
base.LastUpdated = time.Now()
|
||||
|
||||
err := c.Websocket.Orderbook.LoadSnapshot(base, c.GetName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{
|
||||
Pair: p,
|
||||
Asset: "SPOT",
|
||||
Exchange: c.GetName(),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProcessUpdate updates the orderbook local cache
|
||||
func (c *CoinbasePro) ProcessUpdate(update WebsocketL2Update) error {
|
||||
var Asks, Bids []orderbook.Item
|
||||
|
||||
for _, data := range update.Changes {
|
||||
price, _ := strconv.ParseFloat(data[1].(string), 64)
|
||||
volume, _ := strconv.ParseFloat(data[2].(string), 64)
|
||||
|
||||
if data[0].(string) == "buy" {
|
||||
Bids = append(Bids, orderbook.Item{Price: price, Amount: volume})
|
||||
} else {
|
||||
Asks = append(Asks, orderbook.Item{Price: price, Amount: volume})
|
||||
}
|
||||
}
|
||||
|
||||
if len(Asks) == 0 && len(Bids) == 0 {
|
||||
return errors.New("coibasepro_websocket.go error - no data in websocket update")
|
||||
}
|
||||
|
||||
p := pair.NewCurrencyPairFromString(update.ProductID)
|
||||
|
||||
err := c.Websocket.Orderbook.Update(Bids, Asks, p, time.Now(), c.GetName(), "SPOT")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{
|
||||
Pair: p,
|
||||
Asset: "SPOT",
|
||||
Exchange: c.GetName(),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -24,15 +24,11 @@ func (c *CoinbasePro) Start(wg *sync.WaitGroup) {
|
||||
// Run implements the coinbasepro wrapper
|
||||
func (c *CoinbasePro) Run() {
|
||||
if c.Verbose {
|
||||
log.Printf("%s Websocket: %s. (url: %s).\n", c.GetName(), common.IsEnabled(c.Websocket), coinbaseproWebsocketURL)
|
||||
log.Printf("%s Websocket: %s. (url: %s).\n", c.GetName(), common.IsEnabled(c.Websocket.IsEnabled()), coinbaseproWebsocketURL)
|
||||
log.Printf("%s polling delay: %ds.\n", c.GetName(), c.RESTPollingDelay)
|
||||
log.Printf("%s %d currencies enabled: %s.\n", c.GetName(), len(c.EnabledPairs), c.EnabledPairs)
|
||||
}
|
||||
|
||||
if c.Websocket {
|
||||
go c.WebsocketClient()
|
||||
}
|
||||
|
||||
exchangeProducts, err := c.GetProducts()
|
||||
if err != nil {
|
||||
log.Printf("%s Failed to get available products.\n", c.GetName())
|
||||
@@ -190,3 +186,8 @@ func (c *CoinbasePro) WithdrawCryptoExchangeFunds(address string, cryptocurrency
|
||||
func (c *CoinbasePro) WithdrawFiatExchangeFunds(cryptocurrency pair.CurrencyItem, amount float64) (string, error) {
|
||||
return "", errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// GetWebsocket returns a pointer to the exchange websocket
|
||||
func (c *CoinbasePro) GetWebsocket() (*exchange.Websocket, error) {
|
||||
return c.Websocket, nil
|
||||
}
|
||||
|
||||
@@ -53,7 +53,6 @@ func (c *COINUT) SetDefaults() {
|
||||
c.TakerFee = 0.1 //spot
|
||||
c.MakerFee = 0
|
||||
c.Verbose = false
|
||||
c.Websocket = false
|
||||
c.RESTPollingDelay = 10
|
||||
c.RequestCurrencyPairFormat.Delimiter = ""
|
||||
c.RequestCurrencyPairFormat.Uppercase = true
|
||||
@@ -68,6 +67,7 @@ func (c *COINUT) SetDefaults() {
|
||||
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
|
||||
c.APIUrlDefault = coinutAPIURL
|
||||
c.APIUrl = c.APIUrlDefault
|
||||
c.WebsocketInit()
|
||||
}
|
||||
|
||||
// Setup sets the current exchange configuration
|
||||
@@ -82,7 +82,7 @@ func (c *COINUT) Setup(exch config.ExchangeConfig) {
|
||||
c.SetHTTPClientUserAgent(exch.HTTPUserAgent)
|
||||
c.RESTPollingDelay = exch.RESTPollingDelay
|
||||
c.Verbose = exch.Verbose
|
||||
c.Websocket = exch.Websocket
|
||||
c.Websocket.SetEnabled(exch.Websocket)
|
||||
c.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",")
|
||||
c.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",")
|
||||
c.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",")
|
||||
@@ -102,6 +102,18 @@ func (c *COINUT) Setup(exch config.ExchangeConfig) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = c.SetClientProxyAddress(exch.ProxyAddress)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = c.WebsocketSetup(c.WsConnect,
|
||||
exch.Name,
|
||||
exch.Websocket,
|
||||
coinutWebsocketURL,
|
||||
exch.WebsocketURL)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,8 +28,9 @@ func TestSetup(t *testing.T) {
|
||||
}
|
||||
c.Setup(bConfig)
|
||||
|
||||
if !c.IsEnabled() || c.AuthenticatedAPISupport || c.RESTPollingDelay != time.Duration(10) ||
|
||||
c.Verbose || c.Websocket || len(c.BaseCurrencies) < 1 ||
|
||||
if !c.IsEnabled() || c.AuthenticatedAPISupport ||
|
||||
c.RESTPollingDelay != time.Duration(10) || c.Verbose ||
|
||||
c.Websocket.IsEnabled() || len(c.BaseCurrencies) < 1 ||
|
||||
len(c.AvailablePairs) < 1 || len(c.EnabledPairs) < 1 {
|
||||
t.Error("Test Failed - Coinut Setup values not set correctly")
|
||||
}
|
||||
|
||||
@@ -236,3 +236,113 @@ type OpenPosition struct {
|
||||
OpenTimestamp int64 `json:"open_timestamp"`
|
||||
InstrumentID int `json:"inst_id"`
|
||||
}
|
||||
|
||||
type wsRequest struct {
|
||||
Request string `json:"request"`
|
||||
SecType string `json:"sec_type,omitempty"`
|
||||
InstID int64 `json:"inst_id,omitempty"`
|
||||
TopN int64 `json:"top_n,omitempty"`
|
||||
Subscribe bool `json:"subscribe"`
|
||||
Nonce int64 `json:"nonce"`
|
||||
}
|
||||
|
||||
type wsResponse struct {
|
||||
Reply string `json:"reply"`
|
||||
}
|
||||
|
||||
type wsHeartbeatResp struct {
|
||||
Nonce int64 `json:"nonce"`
|
||||
Reply string `json:"reply"`
|
||||
Status []interface{} `json:"status"`
|
||||
}
|
||||
|
||||
// WsTicker defines the resp for ticker updates from the websocket connection
|
||||
type WsTicker struct {
|
||||
HighestBuy float64 `json:"highest_buy,string"`
|
||||
InstID int64 `json:"inst_id"`
|
||||
Last float64 `json:"last,string"`
|
||||
LowestSell float64 `json:"lowest_sell,string"`
|
||||
OpenInterest float64 `json:"open_interest,string"`
|
||||
Reply string `json:"reply"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
TransID int64 `json:"trans_id"`
|
||||
Volume float64 `json:"volume,string"`
|
||||
Volume24H float64 `json:"volume24,string"`
|
||||
}
|
||||
|
||||
// WsOrderbookSnapshot defines the resp for orderbook snapshot updates from
|
||||
// the websocket connection
|
||||
type WsOrderbookSnapshot struct {
|
||||
Buy []WsOrderbookData `json:"buy"`
|
||||
Sell []WsOrderbookData `json:"sell"`
|
||||
InstID int64 `json:"inst_id"`
|
||||
Nonce int64 `json:"nonce"`
|
||||
TotalBuy float64 `json:"total_buy,string"`
|
||||
TotalSell float64 `json:"total_sell,string"`
|
||||
Reply string `json:"reply"`
|
||||
Status []interface{} `json:"status"`
|
||||
}
|
||||
|
||||
// WsOrderbookData defines singular orderbook data
|
||||
type WsOrderbookData struct {
|
||||
Count int64 `json:"count"`
|
||||
Price float64 `json:"price,string"`
|
||||
Volume float64 `json:"qty,string"`
|
||||
}
|
||||
|
||||
// WsOrderbookUpdate defines orderbook update response from the websocket
|
||||
// connection
|
||||
type WsOrderbookUpdate struct {
|
||||
Count int64 `json:"count"`
|
||||
InstID int64 `json:"inst_id"`
|
||||
Price float64 `json:"price,string"`
|
||||
Volume float64 `json:"qty,string"`
|
||||
TotalBuy float64 `json:"total_buy,string"`
|
||||
Reply string `json:"reply"`
|
||||
Side string `json:"side"`
|
||||
TransID int64 `json:"trans_id"`
|
||||
}
|
||||
|
||||
// WsTradeSnapshot defines Market trade response from the websocket
|
||||
// connection
|
||||
type WsTradeSnapshot struct {
|
||||
Nonce int64 `json:"nonce"`
|
||||
Reply string `json:"reply"`
|
||||
Status []interface{} `json:"status"`
|
||||
Trades []WsTradeData `json:"trades"`
|
||||
}
|
||||
|
||||
// WsTradeData defines market trade data
|
||||
type WsTradeData struct {
|
||||
Price float64 `json:"price,string"`
|
||||
Volume float64 `json:"qty,string"`
|
||||
Side string `json:"side"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
TransID int64 `json:"trans_id"`
|
||||
}
|
||||
|
||||
// WsTradeUpdate defines trade update response from the websocket connection
|
||||
type WsTradeUpdate struct {
|
||||
InstID int64 `json:"inst_id"`
|
||||
Price float64 `json:"price,string"`
|
||||
Reply string `json:"reply"`
|
||||
Side string `json:"side"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
TransID int64 `json:"trans_id"`
|
||||
}
|
||||
|
||||
// WsInstrumentList defines instrument list
|
||||
type WsInstrumentList struct {
|
||||
Spot map[string][]WsSupportedCurrency `json:"SPOT"`
|
||||
Nonce int64 `json:"nonce"`
|
||||
Reply string `json:"inst_list"`
|
||||
Status []interface{} `json:"status"`
|
||||
}
|
||||
|
||||
// WsSupportedCurrency defines supported currency on the exchange
|
||||
type WsSupportedCurrency struct {
|
||||
Base string `json:"base"`
|
||||
InstID int64 `json:"inst_id"`
|
||||
DecimalPlaces int64 `json:"decimal_places"`
|
||||
Quote string `json:"quote"`
|
||||
}
|
||||
|
||||
@@ -1,61 +1,365 @@
|
||||
package coinut
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/thrasher-/gocryptotrader/common"
|
||||
"github.com/thrasher-/gocryptotrader/currency/pair"
|
||||
"github.com/thrasher-/gocryptotrader/exchanges"
|
||||
"github.com/thrasher-/gocryptotrader/exchanges/orderbook"
|
||||
)
|
||||
|
||||
const coinutWebsocketURL = "wss://wsapi.coinut.com"
|
||||
|
||||
// WebsocketClient initiates a websocket client
|
||||
func (c *COINUT) WebsocketClient() {
|
||||
for c.Enabled && c.Websocket {
|
||||
var Dialer websocket.Dialer
|
||||
var err error
|
||||
c.WebsocketConn, _, err = Dialer.Dial(c.WebsocketURL, http.Header{})
|
||||
var nNonce map[int64]string
|
||||
var channels map[string]chan []byte
|
||||
var instrumentListByString map[string]int64
|
||||
var instrumentListByCode map[int64]string
|
||||
var populatedList bool
|
||||
|
||||
// NOTE for speed considerations
|
||||
// wss://wsapi-as.coinut.com
|
||||
// wss://wsapi-na.coinut.com
|
||||
// wss://wsapi-eu.coinut.com
|
||||
|
||||
// WsReadData reads data from the websocket conection
|
||||
func (c *COINUT) WsReadData() {
|
||||
c.Websocket.Wg.Add(1)
|
||||
|
||||
defer func() {
|
||||
err := c.WebsocketConn.Close()
|
||||
if err != nil {
|
||||
log.Printf("%s Unable to connect to Websocket. Error: %s\n", c.Name, err)
|
||||
continue
|
||||
c.Websocket.DataHandler <- fmt.Errorf("coinut_websocket.go - Unable to to close Websocket connection. Error: %s",
|
||||
err)
|
||||
}
|
||||
c.Websocket.Wg.Done()
|
||||
}()
|
||||
|
||||
if c.Verbose {
|
||||
log.Printf("%s Connected to Websocket.\n", c.Name)
|
||||
}
|
||||
|
||||
err = c.WebsocketConn.WriteMessage(websocket.TextMessage, []byte(`{"messageType": "hello_world"}`))
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
for {
|
||||
select {
|
||||
case <-c.Websocket.ShutdownC:
|
||||
return
|
||||
}
|
||||
|
||||
for c.Enabled && c.Websocket {
|
||||
msgType, resp, err := c.WebsocketConn.ReadMessage()
|
||||
default:
|
||||
_, resp, err := c.WebsocketConn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
break
|
||||
c.Websocket.DataHandler <- err
|
||||
return
|
||||
}
|
||||
|
||||
switch msgType {
|
||||
case websocket.TextMessage:
|
||||
type MsgType struct {
|
||||
MessageType string `json:"messageType"`
|
||||
}
|
||||
|
||||
msgType := MsgType{}
|
||||
err := common.JSONDecode(resp, &msgType)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
log.Println(string(resp))
|
||||
}
|
||||
c.Websocket.TrafficAlert <- struct{}{}
|
||||
c.Websocket.Intercomm <- exchange.WebsocketResponse{Raw: resp}
|
||||
}
|
||||
c.WebsocketConn.Close()
|
||||
log.Printf("%s Websocket client disconnected.", c.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// WsHandleData handles read data
|
||||
func (c *COINUT) WsHandleData() {
|
||||
c.Websocket.Wg.Add(1)
|
||||
defer c.Websocket.Wg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.Websocket.ShutdownC:
|
||||
return
|
||||
|
||||
case resp := <-c.Websocket.Intercomm:
|
||||
var incoming wsResponse
|
||||
err := common.JSONDecode(resp.Raw, &incoming)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
switch incoming.Reply {
|
||||
case "hb":
|
||||
channels["hb"] <- resp.Raw
|
||||
|
||||
case "inst_tick":
|
||||
var ticker WsTicker
|
||||
err := common.JSONDecode(resp.Raw, &ticker)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
c.Websocket.DataHandler <- exchange.TickerData{
|
||||
Timestamp: time.Unix(0, ticker.Timestamp),
|
||||
Exchange: c.GetName(),
|
||||
AssetType: "SPOT",
|
||||
HighPrice: ticker.HighestBuy,
|
||||
LowPrice: ticker.LowestSell,
|
||||
ClosePrice: ticker.Last,
|
||||
Quantity: ticker.Volume,
|
||||
}
|
||||
|
||||
case "inst_order_book":
|
||||
var orderbooksnapshot WsOrderbookSnapshot
|
||||
err := common.JSONDecode(resp.Raw, &orderbooksnapshot)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = c.WsProcessOrderbookSnapshot(orderbooksnapshot)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
currencyPair := instrumentListByCode[orderbooksnapshot.InstID]
|
||||
|
||||
c.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{
|
||||
Exchange: c.GetName(),
|
||||
Asset: "SPOT",
|
||||
Pair: pair.NewCurrencyPairFromString(currencyPair),
|
||||
}
|
||||
|
||||
case "inst_order_book_update":
|
||||
var orderbookUpdate WsOrderbookUpdate
|
||||
err := common.JSONDecode(resp.Raw, &orderbookUpdate)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = c.WsProcessOrderbookUpdate(orderbookUpdate)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
currencyPair := instrumentListByCode[orderbookUpdate.InstID]
|
||||
|
||||
c.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{
|
||||
Exchange: c.GetName(),
|
||||
Asset: "SPOT",
|
||||
Pair: pair.NewCurrencyPairFromString(currencyPair),
|
||||
}
|
||||
|
||||
case "inst_trade":
|
||||
var tradeSnap WsTradeSnapshot
|
||||
err := common.JSONDecode(resp.Raw, &tradeSnap)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
case "inst_trade_update":
|
||||
var tradeUpdate WsTradeUpdate
|
||||
err := common.JSONDecode(resp.Raw, &tradeUpdate)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
currencyPair := instrumentListByCode[tradeUpdate.InstID]
|
||||
|
||||
c.Websocket.DataHandler <- exchange.TradeData{
|
||||
Timestamp: time.Unix(tradeUpdate.Timestamp, 0),
|
||||
CurrencyPair: pair.NewCurrencyPairFromString(currencyPair),
|
||||
AssetType: "SPOT",
|
||||
Exchange: c.GetName(),
|
||||
Price: tradeUpdate.Price,
|
||||
Side: tradeUpdate.Side,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WsConnect intiates a websocket connection
|
||||
func (c *COINUT) WsConnect() error {
|
||||
if !c.Websocket.IsEnabled() || !c.IsEnabled() {
|
||||
return errors.New(exchange.WebsocketNotEnabled)
|
||||
}
|
||||
|
||||
var Dialer websocket.Dialer
|
||||
|
||||
if c.Websocket.GetProxyAddress() != "" {
|
||||
proxy, err := url.Parse(c.Websocket.GetProxyAddress())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Dialer.Proxy = http.ProxyURL(proxy)
|
||||
}
|
||||
|
||||
var err error
|
||||
c.WebsocketConn, _, err = Dialer.Dial(c.Websocket.GetWebsocketURL(),
|
||||
http.Header{})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !populatedList {
|
||||
instrumentListByString = make(map[string]int64)
|
||||
instrumentListByCode = make(map[int64]string)
|
||||
|
||||
err = c.WsSetInstrumentList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
populatedList = true
|
||||
}
|
||||
|
||||
err = c.WsSubscribe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// define bi-directional communication
|
||||
channels = make(map[string]chan []byte)
|
||||
channels["hb"] = make(chan []byte, 1)
|
||||
|
||||
go c.WsReadData()
|
||||
go c.WsHandleData()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetNonce returns a nonce for a required request
|
||||
func (c *COINUT) GetNonce() int64 {
|
||||
if c.Nonce.Get() == 0 {
|
||||
c.Nonce.Set(time.Now().Unix())
|
||||
} else {
|
||||
c.Nonce.Inc()
|
||||
}
|
||||
|
||||
return c.Nonce.Get()
|
||||
}
|
||||
|
||||
// WsSetInstrumentList fetches instrument list and propagates a local cache
|
||||
func (c *COINUT) WsSetInstrumentList() error {
|
||||
request, err := common.JSONEncode(wsRequest{
|
||||
Request: "inst_list",
|
||||
SecType: "SPOT",
|
||||
Nonce: c.GetNonce(),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = c.WebsocketConn.WriteMessage(websocket.TextMessage, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, resp, err := c.WebsocketConn.ReadMessage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Websocket.TrafficAlert <- struct{}{}
|
||||
|
||||
var list WsInstrumentList
|
||||
err = common.JSONDecode(resp, &list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for currency, data := range list.Spot {
|
||||
instrumentListByString[currency] = data[0].InstID
|
||||
instrumentListByCode[data[0].InstID] = currency
|
||||
}
|
||||
|
||||
if len(instrumentListByString) == 0 || len(instrumentListByCode) == 0 {
|
||||
return errors.New("instrument lists failed to populate")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WsSubscribe subscribes to websocket streams
|
||||
func (c *COINUT) WsSubscribe() error {
|
||||
pairs := c.GetEnabledCurrencies()
|
||||
|
||||
for _, p := range pairs {
|
||||
ticker := wsRequest{
|
||||
Request: "inst_tick",
|
||||
InstID: instrumentListByString[p.Pair().String()],
|
||||
Subscribe: true,
|
||||
Nonce: c.GetNonce(),
|
||||
}
|
||||
|
||||
tickjson, err := common.JSONEncode(ticker)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = c.WebsocketConn.WriteMessage(websocket.TextMessage, tickjson)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
orderbook := wsRequest{
|
||||
Request: "inst_order_book",
|
||||
InstID: instrumentListByString[p.Pair().String()],
|
||||
Subscribe: true,
|
||||
Nonce: c.GetNonce(),
|
||||
}
|
||||
|
||||
objson, err := common.JSONEncode(orderbook)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = c.WebsocketConn.WriteMessage(websocket.TextMessage, objson)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WsProcessOrderbookSnapshot processes the orderbook snapshot
|
||||
func (c *COINUT) WsProcessOrderbookSnapshot(ob WsOrderbookSnapshot) error {
|
||||
var bids []orderbook.Item
|
||||
for _, bid := range ob.Buy {
|
||||
bids = append(bids, orderbook.Item{
|
||||
Amount: bid.Volume,
|
||||
Price: bid.Price,
|
||||
})
|
||||
}
|
||||
|
||||
var asks []orderbook.Item
|
||||
for _, ask := range ob.Sell {
|
||||
asks = append(asks, orderbook.Item{
|
||||
Amount: ask.Volume,
|
||||
Price: ask.Price,
|
||||
})
|
||||
}
|
||||
|
||||
var newOrderbook orderbook.Base
|
||||
newOrderbook.Asks = asks
|
||||
newOrderbook.Bids = bids
|
||||
newOrderbook.CurrencyPair = instrumentListByCode[ob.InstID]
|
||||
newOrderbook.Pair = pair.NewCurrencyPairFromString(instrumentListByCode[ob.InstID])
|
||||
newOrderbook.AssetType = "SPOT"
|
||||
newOrderbook.LastUpdated = time.Now()
|
||||
|
||||
return c.Websocket.Orderbook.LoadSnapshot(newOrderbook, c.GetName())
|
||||
}
|
||||
|
||||
// WsProcessOrderbookUpdate process an orderbook update
|
||||
func (c *COINUT) WsProcessOrderbookUpdate(ob WsOrderbookUpdate) error {
|
||||
p := pair.NewCurrencyPairFromString(instrumentListByCode[ob.InstID])
|
||||
|
||||
if ob.Side == "buy" {
|
||||
return c.Websocket.Orderbook.Update([]orderbook.Item{
|
||||
orderbook.Item{Price: ob.Price, Amount: ob.Volume}},
|
||||
nil,
|
||||
p,
|
||||
time.Now(),
|
||||
c.GetName(),
|
||||
"SPOT")
|
||||
}
|
||||
|
||||
return c.Websocket.Orderbook.Update([]orderbook.Item{
|
||||
orderbook.Item{Price: ob.Price, Amount: ob.Volume}},
|
||||
nil,
|
||||
p,
|
||||
time.Now(),
|
||||
c.GetName(),
|
||||
"SPOT")
|
||||
}
|
||||
|
||||
@@ -24,15 +24,11 @@ func (c *COINUT) Start(wg *sync.WaitGroup) {
|
||||
// Run implements the COINUT wrapper
|
||||
func (c *COINUT) Run() {
|
||||
if c.Verbose {
|
||||
log.Printf("%s Websocket: %s. (url: %s).\n", c.GetName(), common.IsEnabled(c.Websocket), coinutWebsocketURL)
|
||||
log.Printf("%s Websocket: %s. (url: %s).\n", c.GetName(), common.IsEnabled(c.Websocket.IsEnabled()), coinutWebsocketURL)
|
||||
log.Printf("%s polling delay: %ds.\n", c.GetName(), c.RESTPollingDelay)
|
||||
log.Printf("%s %d currencies enabled: %s.\n", c.GetName(), len(c.EnabledPairs), c.EnabledPairs)
|
||||
}
|
||||
|
||||
if c.Websocket {
|
||||
go c.WebsocketClient()
|
||||
}
|
||||
|
||||
exchangeProducts, err := c.GetInstruments()
|
||||
if err != nil {
|
||||
log.Printf("%s Failed to get available products.\n", c.GetName())
|
||||
@@ -193,3 +189,8 @@ func (c *COINUT) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount fl
|
||||
func (c *COINUT) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) {
|
||||
return "", errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// GetWebsocket returns a pointer to the exchange websocket
|
||||
func (c *COINUT) GetWebsocket() (*exchange.Websocket, error) {
|
||||
return c.Websocket, nil
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -90,7 +91,6 @@ type Base struct {
|
||||
Name string
|
||||
Enabled bool
|
||||
Verbose bool
|
||||
Websocket bool
|
||||
RESTPollingDelay time.Duration
|
||||
AuthenticatedAPISupport bool
|
||||
APIAuthPEMKeySupport bool
|
||||
@@ -113,6 +113,7 @@ type Base struct {
|
||||
APIUrlSecondaryDefault string
|
||||
RequestCurrencyPairFormat config.CurrencyPairFormatConfig
|
||||
ConfigCurrencyPairFormat config.CurrencyPairFormatConfig
|
||||
Websocket *Websocket
|
||||
*request.Requester
|
||||
}
|
||||
|
||||
@@ -149,6 +150,8 @@ type IBotExchange interface {
|
||||
|
||||
WithdrawCryptoExchangeFunds(address string, cryptocurrency pair.CurrencyItem, amount float64) (string, error)
|
||||
WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount float64) (string, error)
|
||||
|
||||
GetWebsocket() (*Websocket, error)
|
||||
}
|
||||
|
||||
// SupportsRESTTickerBatchUpdates returns whether or not the
|
||||
@@ -161,7 +164,10 @@ func (e *Base) SupportsRESTTickerBatchUpdates() bool {
|
||||
// HTTP Client
|
||||
func (e *Base) SetHTTPClientTimeout(t time.Duration) {
|
||||
if e.Requester == nil {
|
||||
e.Requester = request.New(e.Name, request.NewRateLimit(time.Second, 0), request.NewRateLimit(time.Second, 0), new(http.Client))
|
||||
e.Requester = request.New(e.Name,
|
||||
request.NewRateLimit(time.Second, 0),
|
||||
request.NewRateLimit(time.Second, 0),
|
||||
new(http.Client))
|
||||
}
|
||||
e.Requester.HTTPClient.Timeout = t
|
||||
}
|
||||
@@ -169,7 +175,10 @@ func (e *Base) SetHTTPClientTimeout(t time.Duration) {
|
||||
// SetHTTPClient sets exchanges HTTP client
|
||||
func (e *Base) SetHTTPClient(h *http.Client) {
|
||||
if e.Requester == nil {
|
||||
e.Requester = request.New(e.Name, request.NewRateLimit(time.Second, 0), request.NewRateLimit(time.Second, 0), new(http.Client))
|
||||
e.Requester = request.New(e.Name,
|
||||
request.NewRateLimit(time.Second, 0),
|
||||
request.NewRateLimit(time.Second, 0),
|
||||
new(http.Client))
|
||||
}
|
||||
e.Requester.HTTPClient = h
|
||||
}
|
||||
@@ -177,7 +186,10 @@ func (e *Base) SetHTTPClient(h *http.Client) {
|
||||
// GetHTTPClient gets the exchanges HTTP client
|
||||
func (e *Base) GetHTTPClient() *http.Client {
|
||||
if e.Requester == nil {
|
||||
e.Requester = request.New(e.Name, request.NewRateLimit(time.Second, 0), request.NewRateLimit(time.Second, 0), new(http.Client))
|
||||
e.Requester = request.New(e.Name,
|
||||
request.NewRateLimit(time.Second, 0),
|
||||
request.NewRateLimit(time.Second, 0),
|
||||
new(http.Client))
|
||||
}
|
||||
return e.Requester.HTTPClient
|
||||
}
|
||||
@@ -185,7 +197,10 @@ func (e *Base) GetHTTPClient() *http.Client {
|
||||
// SetHTTPClientUserAgent sets the exchanges HTTP user agent
|
||||
func (e *Base) SetHTTPClientUserAgent(ua string) {
|
||||
if e.Requester == nil {
|
||||
e.Requester = request.New(e.Name, request.NewRateLimit(time.Second, 0), request.NewRateLimit(time.Second, 0), new(http.Client))
|
||||
e.Requester = request.New(e.Name,
|
||||
request.NewRateLimit(time.Second, 0),
|
||||
request.NewRateLimit(time.Second, 0),
|
||||
new(http.Client))
|
||||
}
|
||||
e.Requester.UserAgent = ua
|
||||
e.HTTPUserAgent = ua
|
||||
@@ -196,6 +211,31 @@ func (e *Base) GetHTTPClientUserAgent() string {
|
||||
return e.HTTPUserAgent
|
||||
}
|
||||
|
||||
// SetClientProxyAddress sets a proxy address for REST and websocket requests
|
||||
func (e *Base) SetClientProxyAddress(addr string) error {
|
||||
if addr != "" {
|
||||
proxy, err := url.Parse(addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("exchange.go - setting proxy address error %s",
|
||||
err)
|
||||
}
|
||||
|
||||
err = e.Requester.SetProxy(proxy)
|
||||
if err != nil {
|
||||
return fmt.Errorf("exchange.go - setting proxy address error %s",
|
||||
err)
|
||||
}
|
||||
|
||||
if e.Websocket != nil {
|
||||
err = e.Websocket.SetProxyAddress(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetAutoPairDefaults sets the default values for whether or not the exchange
|
||||
// supports auto pair updating or not
|
||||
func (e *Base) SetAutoPairDefaults() error {
|
||||
@@ -645,10 +685,10 @@ func (e *Base) SetAPIURL(ec config.ExchangeConfig) error {
|
||||
if ec.APIURL == "" || ec.APIURLSecondary == "" {
|
||||
return errors.New("SetAPIURL error variable zero value")
|
||||
}
|
||||
if ec.APIURL != config.APIURLDefaultMessage {
|
||||
if ec.APIURL != config.APIURLNonDefaultMessage {
|
||||
e.APIUrl = ec.APIURL
|
||||
}
|
||||
if ec.APIURLSecondary != config.APIURLDefaultMessage {
|
||||
if ec.APIURLSecondary != config.APIURLNonDefaultMessage {
|
||||
e.APIUrlSecondary = ec.APIURLSecondary
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -46,7 +46,10 @@ func TestHTTPClient(t *testing.T) {
|
||||
}
|
||||
|
||||
b := Base{Name: "RAWR"}
|
||||
b.Requester = request.New(b.Name, request.NewRateLimit(time.Second, 1), request.NewRateLimit(time.Second, 1), new(http.Client))
|
||||
b.Requester = request.New(b.Name,
|
||||
request.NewRateLimit(time.Second, 1),
|
||||
request.NewRateLimit(time.Second, 1),
|
||||
new(http.Client))
|
||||
|
||||
b.SetHTTPClientTimeout(time.Second * 5)
|
||||
if b.GetHTTPClient().Timeout != time.Second*5 {
|
||||
@@ -61,6 +64,36 @@ func TestHTTPClient(t *testing.T) {
|
||||
t.Fatalf("Test failed. TestHTTPClient unexpected value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetClientProxyAddress(t *testing.T) {
|
||||
requester := request.New("testicles",
|
||||
&request.RateLimit{},
|
||||
&request.RateLimit{},
|
||||
&http.Client{})
|
||||
|
||||
newBase := Base{Name: "Testicles", Requester: requester}
|
||||
|
||||
newBase.WebsocketInit()
|
||||
|
||||
err := newBase.SetClientProxyAddress(":invalid")
|
||||
if err == nil {
|
||||
t.Error("Test failed. SetClientProxyAddress parsed invalid URL")
|
||||
}
|
||||
|
||||
if newBase.Websocket.GetProxyAddress() != "" {
|
||||
t.Error("Test failed. SetClientProxyAddress error", err)
|
||||
}
|
||||
|
||||
err = newBase.SetClientProxyAddress("www.valid.com")
|
||||
if err != nil {
|
||||
t.Error("Test failed. SetClientProxyAddress error", err)
|
||||
}
|
||||
|
||||
if newBase.Websocket.GetProxyAddress() != "www.valid.com" {
|
||||
t.Error("Test failed. SetClientProxyAddress error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetAutoPairDefaults(t *testing.T) {
|
||||
cfg := config.GetConfig()
|
||||
err := cfg.LoadConfig(config.ConfigTestFile)
|
||||
|
||||
618
exchanges/exchange_websocket.go
Normal file
618
exchanges/exchange_websocket.go
Normal file
@@ -0,0 +1,618 @@
|
||||
package exchange
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/thrasher-/gocryptotrader/config"
|
||||
"github.com/thrasher-/gocryptotrader/currency/pair"
|
||||
"github.com/thrasher-/gocryptotrader/exchanges/orderbook"
|
||||
)
|
||||
|
||||
const (
|
||||
// WebsocketNotEnabled alerts of a disabled websocket
|
||||
WebsocketNotEnabled = "exchange_websocket_not_enabled"
|
||||
// WebsocketTrafficLimitTime defines a standard time for no traffic from the
|
||||
// websocket connection
|
||||
WebsocketTrafficLimitTime = 5 * time.Second
|
||||
// WebsocketStateTimeout defines a const for when a websocket connection
|
||||
// times out, will be handled by the routine management system
|
||||
WebsocketStateTimeout = "TIMEOUT"
|
||||
|
||||
websocketRestablishConnection = 1 * time.Second
|
||||
)
|
||||
|
||||
// WebsocketInit initialises the websocket struct
|
||||
func (e *Base) WebsocketInit() {
|
||||
e.Websocket = &Websocket{
|
||||
defaultURL: "",
|
||||
enabled: false,
|
||||
proxyAddr: "",
|
||||
runningURL: "",
|
||||
init: true,
|
||||
}
|
||||
}
|
||||
|
||||
// WebsocketSetup sets main variables for websocket connection
|
||||
func (e *Base) WebsocketSetup(connector func() error,
|
||||
exchangeName string,
|
||||
wsEnabled bool,
|
||||
defaultURL,
|
||||
runningURL string) error {
|
||||
|
||||
e.Websocket.DataHandler = make(chan interface{}, 1)
|
||||
e.Websocket.Connected = make(chan struct{}, 1)
|
||||
e.Websocket.Disconnected = make(chan struct{}, 1)
|
||||
e.Websocket.Intercomm = make(chan WebsocketResponse, 1)
|
||||
e.Websocket.TrafficAlert = make(chan struct{}, 1)
|
||||
|
||||
err := e.Websocket.SetEnabled(wsEnabled)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e.Websocket.SetDefaultURL(defaultURL)
|
||||
e.Websocket.SetConnector(connector)
|
||||
e.Websocket.SetWebsocketURL(runningURL)
|
||||
e.Websocket.SetExchangeName(exchangeName)
|
||||
|
||||
e.Websocket.init = false
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Websocket defines a return type for websocket connections via the interface
|
||||
// wrapper for routine processing in routines.go
|
||||
type Websocket struct {
|
||||
proxyAddr string
|
||||
defaultURL string
|
||||
runningURL string
|
||||
exchangeName string
|
||||
enabled bool
|
||||
init bool
|
||||
connected bool
|
||||
connector func() error
|
||||
m sync.Mutex
|
||||
|
||||
// Connected denotes a channel switch for diversion of request flow
|
||||
Connected chan struct{}
|
||||
|
||||
// Disconnected denotes a channel switch for diversion of request flow
|
||||
Disconnected chan struct{}
|
||||
|
||||
// Intercomm denotes a channel from read data routine to handle data routine
|
||||
Intercomm chan WebsocketResponse
|
||||
|
||||
// DataHandler pipes websocket data to an exchange websocket data handler
|
||||
DataHandler chan interface{}
|
||||
|
||||
// ShutdownC is the main shutdown channel used within an exchange package
|
||||
// called by its own defined Shutdown function
|
||||
ShutdownC chan struct{}
|
||||
|
||||
// Orderbook is a local cache of orderbooks
|
||||
Orderbook WebsocketOrderbookLocal
|
||||
|
||||
// Wg defines a wait group for websocket routines for cleanly shutting down
|
||||
// routines
|
||||
Wg sync.WaitGroup
|
||||
|
||||
// TrafficAlert monitors if there is a halt in traffic throughput
|
||||
TrafficAlert chan struct{}
|
||||
}
|
||||
|
||||
// trafficMonitor monitors traffic and switches connection modes for websocket
|
||||
func (w *Websocket) trafficMonitor(wg *sync.WaitGroup) {
|
||||
w.Wg.Add(1)
|
||||
wg.Done() // Makes sure we are unlocking after we add to waitgroup
|
||||
|
||||
defer func() {
|
||||
if w.connected {
|
||||
w.Disconnected <- struct{}{}
|
||||
}
|
||||
w.Wg.Done()
|
||||
}()
|
||||
|
||||
// Define an initial traffic timer which will be a delay then fall over to
|
||||
// WebsocketTrafficLimitTime after first response
|
||||
trafficTimer := time.NewTimer(5 * time.Second)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-w.ShutdownC: // Returns on shutdown channel close
|
||||
return
|
||||
|
||||
case <-w.TrafficAlert: // Resets timer on traffic
|
||||
if !w.connected {
|
||||
w.Connected <- struct{}{}
|
||||
w.connected = true
|
||||
}
|
||||
|
||||
trafficTimer.Reset(WebsocketTrafficLimitTime)
|
||||
|
||||
case <-trafficTimer.C: // Falls through when timer runs out
|
||||
newtimer := time.NewTimer(10 * time.Second) // New secondary timer set
|
||||
if w.connected {
|
||||
// If connected divert traffic to rest
|
||||
w.Disconnected <- struct{}{}
|
||||
w.connected = false
|
||||
}
|
||||
|
||||
select {
|
||||
case <-w.ShutdownC: // Returns on shutdown channel close
|
||||
return
|
||||
|
||||
case <-newtimer.C: // If secondary timer runs state timeout is sent to the data handler
|
||||
w.DataHandler <- WebsocketStateTimeout
|
||||
return
|
||||
|
||||
case <-w.TrafficAlert: // If in this time response traffic comes through
|
||||
trafficTimer.Reset(WebsocketTrafficLimitTime)
|
||||
if !w.connected {
|
||||
// If not connected divert traffic from REST to websocket
|
||||
w.Connected <- struct{}{}
|
||||
w.connected = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Connect intiates a websocket connection by using a package defined connection
|
||||
// function
|
||||
func (w *Websocket) Connect() error {
|
||||
w.m.Lock()
|
||||
defer w.m.Unlock()
|
||||
|
||||
if !w.IsEnabled() {
|
||||
return fmt.Errorf("exchange_websocket.go %s error - websocket disabled",
|
||||
w.GetName())
|
||||
}
|
||||
|
||||
if w.connected {
|
||||
return errors.New("exchange_websocket.go error - already connected, cannot connect again")
|
||||
}
|
||||
|
||||
w.ShutdownC = make(chan struct{}, 1)
|
||||
|
||||
var anotherWG sync.WaitGroup
|
||||
anotherWG.Add(1)
|
||||
go w.trafficMonitor(&anotherWG)
|
||||
anotherWG.Wait()
|
||||
|
||||
err := w.connector()
|
||||
if err != nil {
|
||||
return fmt.Errorf("exchange_websocket.go connection error %s",
|
||||
err)
|
||||
}
|
||||
|
||||
// Divert for incoming websocket traffic
|
||||
w.Connected <- struct{}{}
|
||||
w.connected = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown attempts to shut down a websocket connection and associated routines
|
||||
// by using a package defined shutdown function
|
||||
func (w *Websocket) Shutdown() error {
|
||||
w.m.Lock()
|
||||
|
||||
defer func() {
|
||||
w.Orderbook.FlushCache()
|
||||
w.m.Unlock()
|
||||
}()
|
||||
|
||||
if !w.connected {
|
||||
return errors.New("exchange_websocket.go error - System not connected to shut down")
|
||||
}
|
||||
|
||||
timer := time.NewTimer(5 * time.Second)
|
||||
c := make(chan struct{}, 1)
|
||||
|
||||
go func(c chan struct{}) {
|
||||
close(w.ShutdownC)
|
||||
w.Wg.Wait()
|
||||
c <- struct{}{}
|
||||
}(c)
|
||||
|
||||
select {
|
||||
case <-c:
|
||||
w.connected = false
|
||||
return nil
|
||||
case <-timer.C:
|
||||
return fmt.Errorf("%s - Websocket routines failed to shutdown",
|
||||
w.GetName())
|
||||
}
|
||||
}
|
||||
|
||||
// SetWebsocketURL sets websocket URL
|
||||
func (w *Websocket) SetWebsocketURL(URL string) {
|
||||
if URL == "" || URL == config.WebsocketURLNonDefaultMessage {
|
||||
w.runningURL = w.defaultURL
|
||||
return
|
||||
}
|
||||
w.runningURL = URL
|
||||
}
|
||||
|
||||
// GetWebsocketURL returns the running websocket URL
|
||||
func (w *Websocket) GetWebsocketURL() string {
|
||||
return w.runningURL
|
||||
}
|
||||
|
||||
// SetEnabled sets if websocket is enabled
|
||||
func (w *Websocket) SetEnabled(enabled bool) error {
|
||||
if w.enabled == enabled {
|
||||
if w.init {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("exchange_websocket.go error - already set as %t",
|
||||
enabled)
|
||||
}
|
||||
|
||||
w.enabled = enabled
|
||||
|
||||
if !w.init {
|
||||
if enabled {
|
||||
if w.connected {
|
||||
return nil
|
||||
}
|
||||
return w.Connect()
|
||||
}
|
||||
|
||||
if !w.connected {
|
||||
return nil
|
||||
}
|
||||
return w.Shutdown()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsEnabled returns bool
|
||||
func (w *Websocket) IsEnabled() bool {
|
||||
return w.enabled
|
||||
}
|
||||
|
||||
// SetProxyAddress sets websocket proxy address
|
||||
func (w *Websocket) SetProxyAddress(URL string) error {
|
||||
if w.proxyAddr == URL {
|
||||
return errors.New("exchange_websocket.go error - Setting proxy address - same address")
|
||||
}
|
||||
|
||||
w.proxyAddr = URL
|
||||
|
||||
if !w.init && w.enabled {
|
||||
if w.connected {
|
||||
err := w.Shutdown()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return w.Connect()
|
||||
}
|
||||
return w.Connect()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetProxyAddress returns the current websocket proxy
|
||||
func (w *Websocket) GetProxyAddress() string {
|
||||
return w.proxyAddr
|
||||
}
|
||||
|
||||
// SetDefaultURL sets default websocket URL
|
||||
func (w *Websocket) SetDefaultURL(defaultURL string) {
|
||||
w.defaultURL = defaultURL
|
||||
}
|
||||
|
||||
// GetDefaultURL returns the default websocket URL
|
||||
func (w *Websocket) GetDefaultURL() string {
|
||||
return w.defaultURL
|
||||
}
|
||||
|
||||
// SetConnector sets connection function
|
||||
func (w *Websocket) SetConnector(connector func() error) {
|
||||
w.connector = connector
|
||||
}
|
||||
|
||||
// SetExchangeName sets exchange name
|
||||
func (w *Websocket) SetExchangeName(exchName string) {
|
||||
w.exchangeName = exchName
|
||||
}
|
||||
|
||||
// GetName returns exchange name
|
||||
func (w *Websocket) GetName() string {
|
||||
return w.exchangeName
|
||||
}
|
||||
|
||||
// WebsocketOrderbookLocal defines a local cache of orderbooks for ammending,
|
||||
// appending and deleting changes and updates the main store in orderbook.go
|
||||
type WebsocketOrderbookLocal struct {
|
||||
ob []orderbook.Base
|
||||
lastUpdated time.Time
|
||||
m sync.Mutex
|
||||
}
|
||||
|
||||
// Update updates a local cache using bid targets and ask targets then updates
|
||||
// main cache in orderbook.go
|
||||
// Volume == 0; deletion at price target
|
||||
// Price target not found; append of price target
|
||||
// Price target found; ammend volume of price target
|
||||
func (w *WebsocketOrderbookLocal) Update(bidTargets, askTargets []orderbook.Item,
|
||||
p pair.CurrencyPair,
|
||||
updated time.Time,
|
||||
exchName, assetType string) error {
|
||||
if bidTargets == nil && askTargets == nil {
|
||||
return errors.New("exchange.go websocket orderbook cache Update() error - cannot have bids and ask targets both nil")
|
||||
}
|
||||
|
||||
if w.lastUpdated.After(updated) {
|
||||
return errors.New("exchange.go WebsocketOrderbookLocal Update() - update is before last update time")
|
||||
}
|
||||
|
||||
w.m.Lock()
|
||||
defer w.m.Unlock()
|
||||
|
||||
var orderbookAddress *orderbook.Base
|
||||
for i := range w.ob {
|
||||
if w.ob[i].Pair == p && w.ob[i].AssetType == assetType {
|
||||
orderbookAddress = &w.ob[i]
|
||||
}
|
||||
}
|
||||
|
||||
if orderbookAddress == nil {
|
||||
return fmt.Errorf("exchange.go WebsocketOrderbookLocal Update() - orderbook.Base could not be found for Exchange %s CurrencyPair: %s AssetType: %s",
|
||||
exchName,
|
||||
p.Pair().String(),
|
||||
assetType)
|
||||
}
|
||||
|
||||
if len(orderbookAddress.Asks) == 0 || len(orderbookAddress.Bids) == 0 {
|
||||
return errors.New("exchange.go websocket orderbook cache Update() error - snapshot incorrectly loaded")
|
||||
}
|
||||
|
||||
if orderbookAddress.Pair == (pair.CurrencyPair{}) {
|
||||
return fmt.Errorf("exchange.go websocket orderbook cache Update() error - snapshot not found %v",
|
||||
p)
|
||||
}
|
||||
|
||||
for x := range bidTargets {
|
||||
// bid targets
|
||||
func() {
|
||||
for y := range orderbookAddress.Bids {
|
||||
if orderbookAddress.Bids[y].Price == bidTargets[x].Price {
|
||||
if bidTargets[x].Amount == 0 {
|
||||
// Delete
|
||||
orderbookAddress.Asks = append(orderbookAddress.Bids[:y],
|
||||
orderbookAddress.Bids[y+1:]...)
|
||||
return
|
||||
}
|
||||
// Ammend
|
||||
orderbookAddress.Bids[y].Amount = bidTargets[x].Amount
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if bidTargets[x].Amount == 0 {
|
||||
// Makes sure we dont append things we missed
|
||||
return
|
||||
}
|
||||
|
||||
// Append
|
||||
orderbookAddress.Bids = append(orderbookAddress.Bids, orderbook.Item{
|
||||
Price: bidTargets[x].Price,
|
||||
Amount: bidTargets[x].Amount,
|
||||
})
|
||||
}()
|
||||
// bid targets
|
||||
}
|
||||
|
||||
for x := range askTargets {
|
||||
func() {
|
||||
for y := range orderbookAddress.Asks {
|
||||
if orderbookAddress.Asks[y].Price == askTargets[x].Price {
|
||||
if askTargets[x].Amount == 0 {
|
||||
// Delete
|
||||
orderbookAddress.Asks = append(orderbookAddress.Asks[:y],
|
||||
orderbookAddress.Asks[y+1:]...)
|
||||
return
|
||||
}
|
||||
// Ammend
|
||||
orderbookAddress.Asks[y].Amount = askTargets[x].Amount
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if askTargets[x].Amount == 0 {
|
||||
// Makes sure we dont append things we missed
|
||||
return
|
||||
}
|
||||
|
||||
// Append
|
||||
orderbookAddress.Asks = append(orderbookAddress.Asks, orderbook.Item{
|
||||
Price: askTargets[x].Price,
|
||||
Amount: askTargets[x].Amount,
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
orderbook.ProcessOrderbook(exchName, p, *orderbookAddress, assetType)
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadSnapshot loads initial snapshot of orderbook data
|
||||
func (w *WebsocketOrderbookLocal) LoadSnapshot(newOrderbook orderbook.Base, exchName string) error {
|
||||
if len(newOrderbook.Asks) == 0 || len(newOrderbook.Bids) == 0 {
|
||||
return errors.New("exchange.go websocket orderbook cache LoadSnapshot() error - snapshot ask and bids are nil")
|
||||
}
|
||||
|
||||
w.m.Lock()
|
||||
defer w.m.Unlock()
|
||||
|
||||
for i := range w.ob {
|
||||
if w.ob[i].Pair == newOrderbook.Pair && w.ob[i].AssetType == newOrderbook.AssetType {
|
||||
return errors.New("exchange.go websocket orderbook cache LoadSnapshot() error - Snapshot instance already found")
|
||||
}
|
||||
}
|
||||
|
||||
w.ob = append(w.ob, newOrderbook)
|
||||
w.lastUpdated = newOrderbook.LastUpdated
|
||||
|
||||
orderbook.ProcessOrderbook(exchName,
|
||||
newOrderbook.Pair,
|
||||
newOrderbook,
|
||||
newOrderbook.AssetType)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateUsingID updates orderbooks using specified ID
|
||||
func (w *WebsocketOrderbookLocal) UpdateUsingID(bidTargets, askTargets []orderbook.Item,
|
||||
p pair.CurrencyPair,
|
||||
updated time.Time,
|
||||
exchName, assetType, action string) error {
|
||||
w.m.Lock()
|
||||
defer w.m.Unlock()
|
||||
|
||||
var orderbookAddress *orderbook.Base
|
||||
for i := range w.ob {
|
||||
if w.ob[i].Pair == p && w.ob[i].AssetType == assetType {
|
||||
orderbookAddress = &w.ob[i]
|
||||
}
|
||||
}
|
||||
|
||||
if orderbookAddress == nil {
|
||||
return fmt.Errorf("exchange.go WebsocketOrderbookLocal Update() - orderbook.Base could not be found for Exchange %s CurrencyPair: %s AssetType: %s",
|
||||
exchName,
|
||||
assetType,
|
||||
p.Pair().String())
|
||||
}
|
||||
|
||||
switch action {
|
||||
case "update":
|
||||
for _, target := range bidTargets {
|
||||
for i := range orderbookAddress.Bids {
|
||||
if orderbookAddress.Bids[i].ID == target.ID {
|
||||
orderbookAddress.Bids[i].Amount = target.Amount
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, target := range askTargets {
|
||||
for i := range orderbookAddress.Asks {
|
||||
if orderbookAddress.Asks[i].ID == target.ID {
|
||||
orderbookAddress.Asks[i].Amount = target.Amount
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case "delete":
|
||||
for _, target := range bidTargets {
|
||||
for i := range orderbookAddress.Bids {
|
||||
if orderbookAddress.Bids[i].ID == target.ID {
|
||||
orderbookAddress.Bids = append(orderbookAddress.Bids[:i],
|
||||
orderbookAddress.Bids[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, target := range askTargets {
|
||||
for i := range orderbookAddress.Asks {
|
||||
if orderbookAddress.Asks[i].ID == target.ID {
|
||||
orderbookAddress.Asks = append(orderbookAddress.Asks[:i],
|
||||
orderbookAddress.Asks[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case "insert":
|
||||
for _, target := range bidTargets {
|
||||
orderbookAddress.Bids = append(orderbookAddress.Bids, target)
|
||||
}
|
||||
|
||||
for _, target := range askTargets {
|
||||
orderbookAddress.Asks = append(orderbookAddress.Asks, target)
|
||||
}
|
||||
}
|
||||
|
||||
orderbook.ProcessOrderbook(exchName, p, *orderbookAddress, assetType)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FlushCache flushes w.ob data to be garbage collected and refreshed when a
|
||||
// connection is lost and reconnected
|
||||
func (w *WebsocketOrderbookLocal) FlushCache() {
|
||||
w.m.Lock()
|
||||
w.ob = nil
|
||||
w.m.Unlock()
|
||||
}
|
||||
|
||||
// WebsocketResponse defines generalised data from the websocket connection
|
||||
type WebsocketResponse struct {
|
||||
Type int
|
||||
Raw []byte
|
||||
}
|
||||
|
||||
// WebsocketOrderbookUpdate defines a websocket event in which the orderbook
|
||||
// has been updated in the orderbook package
|
||||
type WebsocketOrderbookUpdate struct {
|
||||
Pair pair.CurrencyPair
|
||||
Asset string
|
||||
Exchange string
|
||||
}
|
||||
|
||||
// TradeData defines trade data
|
||||
type TradeData struct {
|
||||
Timestamp time.Time
|
||||
CurrencyPair pair.CurrencyPair
|
||||
AssetType string
|
||||
Exchange string
|
||||
EventType string
|
||||
EventTime int64
|
||||
Price float64
|
||||
Amount float64
|
||||
Side string
|
||||
}
|
||||
|
||||
// TickerData defines ticker feed
|
||||
type TickerData struct {
|
||||
Timestamp time.Time
|
||||
Pair pair.CurrencyPair
|
||||
AssetType string
|
||||
Exchange string
|
||||
ClosePrice float64
|
||||
Quantity float64
|
||||
OpenPrice float64
|
||||
HighPrice float64
|
||||
LowPrice float64
|
||||
}
|
||||
|
||||
// KlineData defines kline feed
|
||||
type KlineData struct {
|
||||
Timestamp time.Time
|
||||
Pair pair.CurrencyPair
|
||||
AssetType string
|
||||
Exchange string
|
||||
StartTime time.Time
|
||||
CloseTime time.Time
|
||||
Interval string
|
||||
OpenPrice float64
|
||||
ClosePrice float64
|
||||
HighPrice float64
|
||||
LowPrice float64
|
||||
Volume float64
|
||||
}
|
||||
|
||||
// WebsocketPositionUpdated reflects a change in orders/contracts on an exchange
|
||||
type WebsocketPositionUpdated struct {
|
||||
Timestamp time.Time
|
||||
Pair pair.CurrencyPair
|
||||
AssetType string
|
||||
Exchange string
|
||||
}
|
||||
311
exchanges/exchange_websocket_test.go
Normal file
311
exchanges/exchange_websocket_test.go
Normal file
@@ -0,0 +1,311 @@
|
||||
package exchange
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/thrasher-/gocryptotrader/currency/pair"
|
||||
"github.com/thrasher-/gocryptotrader/exchanges/orderbook"
|
||||
)
|
||||
|
||||
var wsTest Base
|
||||
|
||||
func TestWebsocketInit(t *testing.T) {
|
||||
if wsTest.Websocket != nil {
|
||||
t.Error("test failed - WebsocketInit() error")
|
||||
}
|
||||
|
||||
wsTest.WebsocketInit()
|
||||
|
||||
if wsTest.Websocket == nil {
|
||||
t.Error("test failed - WebsocketInit() error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebsocket(t *testing.T) {
|
||||
if err := wsTest.Websocket.SetProxyAddress("testProxy"); err != nil {
|
||||
t.Error("test failed - SetProxyAddress", err)
|
||||
}
|
||||
|
||||
wsTest.WebsocketSetup(func() error { return nil },
|
||||
"testName",
|
||||
true,
|
||||
"testDefaultURL",
|
||||
"testRunningURL")
|
||||
|
||||
// Test variable setting and retreival
|
||||
if wsTest.Websocket.GetName() != "testName" {
|
||||
t.Error("test failed - WebsocketSetup")
|
||||
}
|
||||
|
||||
if !wsTest.Websocket.IsEnabled() {
|
||||
t.Error("test failed - WebsocketSetup")
|
||||
}
|
||||
|
||||
if wsTest.Websocket.GetProxyAddress() != "testProxy" {
|
||||
t.Error("test failed - WebsocketSetup")
|
||||
}
|
||||
|
||||
if wsTest.Websocket.GetDefaultURL() != "testDefaultURL" {
|
||||
t.Error("test failed - WebsocketSetup")
|
||||
}
|
||||
|
||||
if wsTest.Websocket.GetWebsocketURL() != "testRunningURL" {
|
||||
t.Error("test failed - WebsocketSetup")
|
||||
}
|
||||
|
||||
// Test websocket connect and shutdown functions
|
||||
comms := make(chan struct{}, 1)
|
||||
go func() {
|
||||
var count int
|
||||
for {
|
||||
if count == 4 {
|
||||
close(comms)
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-wsTest.Websocket.Connected:
|
||||
count++
|
||||
case <-wsTest.Websocket.Disconnected:
|
||||
count++
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// -- Not connected shutdown
|
||||
err := wsTest.Websocket.Shutdown()
|
||||
if err == nil {
|
||||
t.Fatal("test failed - should not be connected to able to shut down")
|
||||
}
|
||||
|
||||
// -- Normal connect
|
||||
err = wsTest.Websocket.Connect()
|
||||
if err != nil {
|
||||
t.Fatal("test failed - WebsocketSetup", err)
|
||||
}
|
||||
|
||||
// -- Already connected connect
|
||||
err = wsTest.Websocket.Connect()
|
||||
if err == nil {
|
||||
t.Fatal("test failed - should not connect, already connected")
|
||||
}
|
||||
|
||||
wsTest.Websocket.SetWebsocketURL("")
|
||||
|
||||
// -- Set true when already true
|
||||
err = wsTest.Websocket.SetEnabled(true)
|
||||
if err == nil {
|
||||
t.Fatal("test failed - setting enabled should not work")
|
||||
}
|
||||
|
||||
// -- Set false normal
|
||||
err = wsTest.Websocket.SetEnabled(false)
|
||||
if err != nil {
|
||||
t.Fatal("test failed - setting enabled should not work")
|
||||
}
|
||||
|
||||
// -- Set true normal
|
||||
err = wsTest.Websocket.SetEnabled(true)
|
||||
if err != nil {
|
||||
t.Fatal("test failed - setting enabled should not work")
|
||||
}
|
||||
|
||||
// -- Normal shutdown
|
||||
err = wsTest.Websocket.Shutdown()
|
||||
if err != nil {
|
||||
t.Fatal("test failed - WebsocketSetup", err)
|
||||
}
|
||||
|
||||
timer := time.NewTimer(5 * time.Second)
|
||||
select {
|
||||
case <-comms:
|
||||
case <-timer.C:
|
||||
t.Fatal("test failed - WebsocketSetup - timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInsertingSnapShots(t *testing.T) {
|
||||
var snapShot1 orderbook.Base
|
||||
asks := []orderbook.Item{
|
||||
orderbook.Item{Price: 6000, Amount: 1, ID: 1},
|
||||
orderbook.Item{Price: 6001, Amount: 0.5, ID: 2},
|
||||
orderbook.Item{Price: 6002, Amount: 2, ID: 3},
|
||||
orderbook.Item{Price: 6003, Amount: 3, ID: 4},
|
||||
orderbook.Item{Price: 6004, Amount: 5, ID: 5},
|
||||
orderbook.Item{Price: 6005, Amount: 2, ID: 6},
|
||||
orderbook.Item{Price: 6006, Amount: 1.5, ID: 7},
|
||||
orderbook.Item{Price: 6007, Amount: 0.5, ID: 8},
|
||||
orderbook.Item{Price: 6008, Amount: 23, ID: 9},
|
||||
orderbook.Item{Price: 6009, Amount: 9, ID: 10},
|
||||
orderbook.Item{Price: 6010, Amount: 7, ID: 11},
|
||||
}
|
||||
|
||||
bids := []orderbook.Item{
|
||||
orderbook.Item{Price: 5999, Amount: 1, ID: 12},
|
||||
orderbook.Item{Price: 5998, Amount: 0.5, ID: 13},
|
||||
orderbook.Item{Price: 5997, Amount: 2, ID: 14},
|
||||
orderbook.Item{Price: 5996, Amount: 3, ID: 15},
|
||||
orderbook.Item{Price: 5995, Amount: 5, ID: 16},
|
||||
orderbook.Item{Price: 5994, Amount: 2, ID: 17},
|
||||
orderbook.Item{Price: 5993, Amount: 1.5, ID: 18},
|
||||
orderbook.Item{Price: 5992, Amount: 0.5, ID: 19},
|
||||
orderbook.Item{Price: 5991, Amount: 23, ID: 20},
|
||||
orderbook.Item{Price: 5990, Amount: 9, ID: 21},
|
||||
orderbook.Item{Price: 5989, Amount: 7, ID: 22},
|
||||
}
|
||||
|
||||
snapShot1.Asks = asks
|
||||
snapShot1.Bids = bids
|
||||
snapShot1.AssetType = "SPOT"
|
||||
snapShot1.CurrencyPair = "BTCUSD"
|
||||
snapShot1.LastUpdated = time.Now()
|
||||
snapShot1.Pair = pair.NewCurrencyPairFromString("BTCUSD")
|
||||
|
||||
wsTest.Websocket.Orderbook.LoadSnapshot(snapShot1, "ExchangeTest")
|
||||
|
||||
var snapShot2 orderbook.Base
|
||||
asks = []orderbook.Item{
|
||||
orderbook.Item{Price: 51, Amount: 1, ID: 1},
|
||||
orderbook.Item{Price: 52, Amount: 0.5, ID: 2},
|
||||
orderbook.Item{Price: 53, Amount: 2, ID: 3},
|
||||
orderbook.Item{Price: 54, Amount: 3, ID: 4},
|
||||
orderbook.Item{Price: 55, Amount: 5, ID: 5},
|
||||
orderbook.Item{Price: 56, Amount: 2, ID: 6},
|
||||
orderbook.Item{Price: 57, Amount: 1.5, ID: 7},
|
||||
orderbook.Item{Price: 58, Amount: 0.5, ID: 8},
|
||||
orderbook.Item{Price: 59, Amount: 23, ID: 9},
|
||||
orderbook.Item{Price: 50, Amount: 9, ID: 10},
|
||||
orderbook.Item{Price: 60, Amount: 7, ID: 11},
|
||||
}
|
||||
|
||||
bids = []orderbook.Item{
|
||||
orderbook.Item{Price: 49, Amount: 1, ID: 12},
|
||||
orderbook.Item{Price: 48, Amount: 0.5, ID: 13},
|
||||
orderbook.Item{Price: 47, Amount: 2, ID: 14},
|
||||
orderbook.Item{Price: 46, Amount: 3, ID: 15},
|
||||
orderbook.Item{Price: 45, Amount: 5, ID: 16},
|
||||
orderbook.Item{Price: 44, Amount: 2, ID: 17},
|
||||
orderbook.Item{Price: 43, Amount: 1.5, ID: 18},
|
||||
orderbook.Item{Price: 42, Amount: 0.5, ID: 19},
|
||||
orderbook.Item{Price: 41, Amount: 23, ID: 20},
|
||||
orderbook.Item{Price: 40, Amount: 9, ID: 21},
|
||||
orderbook.Item{Price: 39, Amount: 7, ID: 22},
|
||||
}
|
||||
|
||||
snapShot2.Asks = asks
|
||||
snapShot2.Bids = bids
|
||||
snapShot2.AssetType = "SPOT"
|
||||
snapShot2.CurrencyPair = "LTCUSD"
|
||||
snapShot2.LastUpdated = time.Now()
|
||||
snapShot2.Pair = pair.NewCurrencyPairFromString("LTCUSD")
|
||||
|
||||
wsTest.Websocket.Orderbook.LoadSnapshot(snapShot2, "ExchangeTest")
|
||||
|
||||
var snapShot3 orderbook.Base
|
||||
asks = []orderbook.Item{
|
||||
orderbook.Item{Price: 51, Amount: 1, ID: 1},
|
||||
orderbook.Item{Price: 52, Amount: 0.5, ID: 2},
|
||||
orderbook.Item{Price: 53, Amount: 2, ID: 3},
|
||||
orderbook.Item{Price: 54, Amount: 3, ID: 4},
|
||||
orderbook.Item{Price: 55, Amount: 5, ID: 5},
|
||||
orderbook.Item{Price: 56, Amount: 2, ID: 6},
|
||||
orderbook.Item{Price: 57, Amount: 1.5, ID: 7},
|
||||
orderbook.Item{Price: 58, Amount: 0.5, ID: 8},
|
||||
orderbook.Item{Price: 59, Amount: 23, ID: 9},
|
||||
orderbook.Item{Price: 50, Amount: 9, ID: 10},
|
||||
orderbook.Item{Price: 60, Amount: 7, ID: 11},
|
||||
}
|
||||
|
||||
bids = []orderbook.Item{
|
||||
orderbook.Item{Price: 49, Amount: 1, ID: 12},
|
||||
orderbook.Item{Price: 48, Amount: 0.5, ID: 13},
|
||||
orderbook.Item{Price: 47, Amount: 2, ID: 14},
|
||||
orderbook.Item{Price: 46, Amount: 3, ID: 15},
|
||||
orderbook.Item{Price: 45, Amount: 5, ID: 16},
|
||||
orderbook.Item{Price: 44, Amount: 2, ID: 17},
|
||||
orderbook.Item{Price: 43, Amount: 1.5, ID: 18},
|
||||
orderbook.Item{Price: 42, Amount: 0.5, ID: 19},
|
||||
orderbook.Item{Price: 41, Amount: 23, ID: 20},
|
||||
orderbook.Item{Price: 40, Amount: 9, ID: 21},
|
||||
orderbook.Item{Price: 39, Amount: 7, ID: 22},
|
||||
}
|
||||
|
||||
snapShot3.Asks = asks
|
||||
snapShot3.Bids = bids
|
||||
snapShot3.AssetType = "FUTURES"
|
||||
snapShot3.CurrencyPair = "LTCUSD"
|
||||
snapShot3.LastUpdated = time.Now()
|
||||
snapShot3.Pair = pair.NewCurrencyPairFromString("LTCUSD")
|
||||
|
||||
wsTest.Websocket.Orderbook.LoadSnapshot(snapShot3, "ExchangeTest")
|
||||
|
||||
if len(wsTest.Websocket.Orderbook.ob) != 3 {
|
||||
t.Error("test failed - inserting orderbook data")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
LTCUSDPAIR := pair.NewCurrencyPairFromString("LTCUSD")
|
||||
BTCUSDPAIR := pair.NewCurrencyPairFromString("BTCUSD")
|
||||
|
||||
bidTargets := []orderbook.Item{
|
||||
orderbook.Item{Price: 49, Amount: 24}, // Ammend
|
||||
orderbook.Item{Price: 48, Amount: 0}, // Delete
|
||||
orderbook.Item{Price: 1337, Amount: 100}, // Append
|
||||
orderbook.Item{Price: 1336, Amount: 0}, // Ghost delete
|
||||
}
|
||||
|
||||
askTargets := []orderbook.Item{
|
||||
orderbook.Item{Price: 51, Amount: 24}, // Ammend
|
||||
orderbook.Item{Price: 52, Amount: 0}, // Delete
|
||||
orderbook.Item{Price: 1337, Amount: 100}, // Append
|
||||
orderbook.Item{Price: 1336, Amount: 0}, // Ghost delete
|
||||
}
|
||||
|
||||
err := wsTest.Websocket.Orderbook.Update(bidTargets,
|
||||
askTargets,
|
||||
LTCUSDPAIR,
|
||||
time.Now(),
|
||||
"ExchangeTest",
|
||||
"SPOT")
|
||||
|
||||
if err != nil {
|
||||
t.Error("test failed - OrderbookUpdate error", err)
|
||||
}
|
||||
|
||||
err = wsTest.Websocket.Orderbook.Update(bidTargets,
|
||||
askTargets,
|
||||
LTCUSDPAIR,
|
||||
time.Now(),
|
||||
"ExchangeTest",
|
||||
"FUTURES")
|
||||
|
||||
if err != nil {
|
||||
t.Error("test failed - OrderbookUpdate error", err)
|
||||
}
|
||||
|
||||
bidTargets = []orderbook.Item{
|
||||
orderbook.Item{Price: 5999, Amount: 24}, // Ammend
|
||||
orderbook.Item{Price: 5998, Amount: 0}, // Delete
|
||||
orderbook.Item{Price: 1337, Amount: 100}, // Append
|
||||
orderbook.Item{Price: 1336, Amount: 0}, // Ghost delete
|
||||
}
|
||||
|
||||
askTargets = []orderbook.Item{
|
||||
orderbook.Item{Price: 6000, Amount: 24}, // Ammend
|
||||
orderbook.Item{Price: 6001, Amount: 0}, // Delete
|
||||
orderbook.Item{Price: 1337, Amount: 100}, // Append
|
||||
orderbook.Item{Price: 1336, Amount: 0}, // Ghost delete
|
||||
}
|
||||
|
||||
err = wsTest.Websocket.Orderbook.Update(bidTargets,
|
||||
askTargets,
|
||||
BTCUSDPAIR,
|
||||
time.Now(),
|
||||
"ExchangeTest",
|
||||
"SPOT")
|
||||
|
||||
if err != nil {
|
||||
t.Error("test failed - OrderbookUpdate error", err)
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,6 @@ func (e *EXMO) SetDefaults() {
|
||||
e.Name = "EXMO"
|
||||
e.Enabled = false
|
||||
e.Verbose = false
|
||||
e.Websocket = false
|
||||
e.RESTPollingDelay = 10
|
||||
e.RequestCurrencyPairFormat.Delimiter = "_"
|
||||
e.RequestCurrencyPairFormat.Uppercase = true
|
||||
@@ -71,6 +70,7 @@ func (e *EXMO) SetDefaults() {
|
||||
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
|
||||
e.APIUrlDefault = exmoAPIURL
|
||||
e.APIUrl = e.APIUrlDefault
|
||||
e.WebsocketInit()
|
||||
}
|
||||
|
||||
// Setup takes in the supplied exchange configuration details and sets params
|
||||
@@ -85,7 +85,6 @@ func (e *EXMO) Setup(exch config.ExchangeConfig) {
|
||||
e.SetHTTPClientUserAgent(exch.HTTPUserAgent)
|
||||
e.RESTPollingDelay = exch.RESTPollingDelay
|
||||
e.Verbose = exch.Verbose
|
||||
e.Websocket = exch.Websocket
|
||||
e.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",")
|
||||
e.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",")
|
||||
e.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",")
|
||||
@@ -105,6 +104,10 @@ func (e *EXMO) Setup(exch config.ExchangeConfig) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = e.SetClientProxyAddress(exch.ProxyAddress)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -224,3 +224,8 @@ func (e *EXMO) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount floa
|
||||
func (e *EXMO) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) {
|
||||
return "", errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// GetWebsocket returns a pointer to the exchange websocket
|
||||
func (e *EXMO) GetWebsocket() (*exchange.Websocket, error) {
|
||||
return nil, errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@ func (g *Gateio) SetDefaults() {
|
||||
g.Name = "GateIO"
|
||||
g.Enabled = false
|
||||
g.Verbose = false
|
||||
g.Websocket = false
|
||||
g.RESTPollingDelay = 10
|
||||
g.RequestCurrencyPairFormat.Delimiter = "_"
|
||||
g.RequestCurrencyPairFormat.Uppercase = false
|
||||
@@ -62,6 +61,7 @@ func (g *Gateio) SetDefaults() {
|
||||
g.APIUrl = g.APIUrlDefault
|
||||
g.APIUrlSecondaryDefault = gateioMarketURL
|
||||
g.APIUrlSecondary = g.APIUrlSecondaryDefault
|
||||
g.WebsocketInit()
|
||||
}
|
||||
|
||||
// Setup sets user configuration
|
||||
@@ -77,7 +77,6 @@ func (g *Gateio) Setup(exch config.ExchangeConfig) {
|
||||
g.SetHTTPClientUserAgent(exch.HTTPUserAgent)
|
||||
g.RESTPollingDelay = exch.RESTPollingDelay
|
||||
g.Verbose = exch.Verbose
|
||||
g.Websocket = exch.Websocket
|
||||
g.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",")
|
||||
g.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",")
|
||||
g.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",")
|
||||
@@ -97,6 +96,10 @@ func (g *Gateio) Setup(exch config.ExchangeConfig) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = g.SetClientProxyAddress(exch.ProxyAddress)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ func (g *Gateio) Start(wg *sync.WaitGroup) {
|
||||
// Run implements the GateIO wrapper
|
||||
func (g *Gateio) Run() {
|
||||
if g.Verbose {
|
||||
log.Printf("%s Websocket: %s. (url: %s).\n", g.GetName(), common.IsEnabled(g.Websocket), g.WebsocketURL)
|
||||
log.Printf("%s Websocket: %s. (url: %s).\n", g.GetName(), common.IsEnabled(g.Websocket.IsEnabled()), g.WebsocketURL)
|
||||
log.Printf("%s polling delay: %ds.\n", g.GetName(), g.RESTPollingDelay)
|
||||
log.Printf("%s %d currencies enabled: %s.\n", g.GetName(), len(g.EnabledPairs), g.EnabledPairs)
|
||||
}
|
||||
@@ -175,3 +175,8 @@ func (g *Gateio) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount fl
|
||||
func (g *Gateio) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) {
|
||||
return "", errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// GetWebsocket returns a pointer to the exchange websocket
|
||||
func (g *Gateio) GetWebsocket() (*exchange.Websocket, error) {
|
||||
return nil, errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
@@ -100,7 +100,6 @@ func (g *Gemini) SetDefaults() {
|
||||
g.Name = "Gemini"
|
||||
g.Enabled = false
|
||||
g.Verbose = false
|
||||
g.Websocket = false
|
||||
g.RESTPollingDelay = 10
|
||||
g.RequestCurrencyPairFormat.Delimiter = ""
|
||||
g.RequestCurrencyPairFormat.Uppercase = true
|
||||
@@ -115,6 +114,7 @@ func (g *Gemini) SetDefaults() {
|
||||
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
|
||||
g.APIUrlDefault = geminiAPIURL
|
||||
g.APIUrl = g.APIUrlDefault
|
||||
g.WebsocketInit()
|
||||
}
|
||||
|
||||
// Setup sets exchange configuration parameters
|
||||
@@ -129,7 +129,6 @@ func (g *Gemini) Setup(exch config.ExchangeConfig) {
|
||||
g.SetHTTPClientUserAgent(exch.HTTPUserAgent)
|
||||
g.RESTPollingDelay = exch.RESTPollingDelay
|
||||
g.Verbose = exch.Verbose
|
||||
g.Websocket = exch.Websocket
|
||||
g.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",")
|
||||
g.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",")
|
||||
g.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",")
|
||||
@@ -153,6 +152,10 @@ func (g *Gemini) Setup(exch config.ExchangeConfig) {
|
||||
if exch.UseSandbox {
|
||||
g.APIUrl = geminiSandboxAPIURL
|
||||
}
|
||||
err = g.SetClientProxyAddress(exch.ProxyAddress)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -175,3 +175,8 @@ func (g *Gemini) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount fl
|
||||
func (g *Gemini) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) {
|
||||
return "", errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// GetWebsocket returns a pointer to the exchange websocket
|
||||
func (g *Gemini) GetWebsocket() (*exchange.Websocket, error) {
|
||||
return nil, errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/thrasher-/gocryptotrader/common"
|
||||
"github.com/thrasher-/gocryptotrader/config"
|
||||
"github.com/thrasher-/gocryptotrader/exchanges"
|
||||
@@ -49,6 +50,7 @@ const (
|
||||
// HitBTC is the overarching type across the hitbtc package
|
||||
type HitBTC struct {
|
||||
exchange.Base
|
||||
WebsocketConn *websocket.Conn
|
||||
}
|
||||
|
||||
// SetDefaults sets default settings for hitbtc
|
||||
@@ -57,7 +59,6 @@ func (p *HitBTC) SetDefaults() {
|
||||
p.Enabled = false
|
||||
p.Fee = 0
|
||||
p.Verbose = false
|
||||
p.Websocket = false
|
||||
p.RESTPollingDelay = 10
|
||||
p.RequestCurrencyPairFormat.Delimiter = ""
|
||||
p.RequestCurrencyPairFormat.Uppercase = true
|
||||
@@ -72,6 +73,7 @@ func (p *HitBTC) SetDefaults() {
|
||||
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
|
||||
p.APIUrlDefault = apiURL
|
||||
p.APIUrl = p.APIUrlDefault
|
||||
p.WebsocketInit()
|
||||
}
|
||||
|
||||
// Setup sets user exchange configuration settings
|
||||
@@ -86,7 +88,7 @@ func (p *HitBTC) Setup(exch config.ExchangeConfig) {
|
||||
p.SetHTTPClientUserAgent(exch.HTTPUserAgent)
|
||||
p.RESTPollingDelay = exch.RESTPollingDelay // Max 60000ms
|
||||
p.Verbose = exch.Verbose
|
||||
p.Websocket = exch.Websocket
|
||||
p.Websocket.SetEnabled(exch.Websocket)
|
||||
p.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",")
|
||||
p.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",")
|
||||
p.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",")
|
||||
@@ -106,6 +108,18 @@ func (p *HitBTC) Setup(exch config.ExchangeConfig) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = p.SetClientProxyAddress(exch.ProxyAddress)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = p.WebsocketSetup(p.WsConnect,
|
||||
exch.Name,
|
||||
exch.Websocket,
|
||||
hitbtcWebsocketAddress,
|
||||
exch.WebsocketURL)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,188 +1,372 @@
|
||||
package hitbtc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/beatgammit/turnpike"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/thrasher-/gocryptotrader/common"
|
||||
"github.com/thrasher-/gocryptotrader/currency/pair"
|
||||
"github.com/thrasher-/gocryptotrader/exchanges"
|
||||
"github.com/thrasher-/gocryptotrader/exchanges/orderbook"
|
||||
)
|
||||
|
||||
const (
|
||||
hitbtcWebsocketAddress = "wss://api.hitbtc.com"
|
||||
hitbtcWebsocketRealm = "realm1"
|
||||
hitbtcWebsocketTicker = "ticker"
|
||||
hitbtcWebsocketTrollbox = "trollbox"
|
||||
hitbtcWebsocketAddress = "wss://api.hitbtc.com/api/2/ws"
|
||||
rpcVersion = "2.0"
|
||||
)
|
||||
|
||||
// WebsocketTicker holds ticker data
|
||||
type WebsocketTicker struct {
|
||||
CurrencyPair string
|
||||
Last float64
|
||||
LowestAsk float64
|
||||
HighestBid float64
|
||||
PercentChange float64
|
||||
BaseVolume float64
|
||||
QuoteVolume float64
|
||||
IsFrozen bool
|
||||
High float64
|
||||
Low float64
|
||||
}
|
||||
|
||||
// OnTicker converts ticker to websocket ticker
|
||||
func OnTicker(args []interface{}, kwargs map[string]interface{}) {
|
||||
ticker := WebsocketTicker{}
|
||||
ticker.CurrencyPair = args[0].(string)
|
||||
ticker.Last, _ = strconv.ParseFloat(args[1].(string), 64)
|
||||
ticker.LowestAsk, _ = strconv.ParseFloat(args[2].(string), 64)
|
||||
ticker.HighestBid, _ = strconv.ParseFloat(args[3].(string), 64)
|
||||
ticker.PercentChange, _ = strconv.ParseFloat(args[4].(string), 64)
|
||||
ticker.BaseVolume, _ = strconv.ParseFloat(args[5].(string), 64)
|
||||
ticker.QuoteVolume, _ = strconv.ParseFloat(args[6].(string), 64)
|
||||
|
||||
if args[7].(float64) != 0 {
|
||||
ticker.IsFrozen = true
|
||||
} else {
|
||||
ticker.IsFrozen = false
|
||||
// WsConnect starts a new connection with the websocket API
|
||||
func (h *HitBTC) WsConnect() error {
|
||||
if !h.Websocket.IsEnabled() || !h.IsEnabled() {
|
||||
return errors.New(exchange.WebsocketNotEnabled)
|
||||
}
|
||||
|
||||
ticker.High, _ = strconv.ParseFloat(args[8].(string), 64)
|
||||
ticker.Low, _ = strconv.ParseFloat(args[9].(string), 64)
|
||||
}
|
||||
var dialer websocket.Dialer
|
||||
|
||||
// WebsocketTrollboxMessage contains trollbox message information
|
||||
type WebsocketTrollboxMessage struct {
|
||||
MessageNumber float64
|
||||
Username string
|
||||
Message string
|
||||
Reputation float64
|
||||
}
|
||||
|
||||
// OnTrollbox converts trollbox messages
|
||||
func OnTrollbox(args []interface{}, kwargs map[string]interface{}) {
|
||||
message := WebsocketTrollboxMessage{}
|
||||
message.MessageNumber, _ = args[1].(float64)
|
||||
message.Username = args[2].(string)
|
||||
message.Message = args[3].(string)
|
||||
if len(args) == 5 {
|
||||
message.Reputation = args[4].(float64)
|
||||
}
|
||||
}
|
||||
|
||||
// OnDepthOrTrade converts depth and trade data
|
||||
func OnDepthOrTrade(args []interface{}, kwargs map[string]interface{}) {
|
||||
for x := range args {
|
||||
data := args[x].(map[string]interface{})
|
||||
msgData := data["data"].(map[string]interface{})
|
||||
msgType := data["type"].(string)
|
||||
|
||||
switch msgType {
|
||||
case "orderBookModify":
|
||||
{
|
||||
type HitBTCWebsocketOrderbookModify struct {
|
||||
Type string
|
||||
Rate float64
|
||||
Amount float64
|
||||
}
|
||||
|
||||
orderModify := HitBTCWebsocketOrderbookModify{}
|
||||
orderModify.Type = msgData["type"].(string)
|
||||
|
||||
rateStr := msgData["rate"].(string)
|
||||
orderModify.Rate, _ = strconv.ParseFloat(rateStr, 64)
|
||||
|
||||
amountStr := msgData["amount"].(string)
|
||||
orderModify.Amount, _ = strconv.ParseFloat(amountStr, 64)
|
||||
}
|
||||
case "orderBookRemove":
|
||||
{
|
||||
type HitBTCWebsocketOrderbookRemove struct {
|
||||
Type string
|
||||
Rate float64
|
||||
}
|
||||
|
||||
orderRemoval := HitBTCWebsocketOrderbookRemove{}
|
||||
orderRemoval.Type = msgData["type"].(string)
|
||||
|
||||
rateStr := msgData["rate"].(string)
|
||||
orderRemoval.Rate, _ = strconv.ParseFloat(rateStr, 64)
|
||||
}
|
||||
case "newTrade":
|
||||
{
|
||||
type HitBTCWebsocketNewTrade struct {
|
||||
Type string
|
||||
TradeID int64
|
||||
Rate float64
|
||||
Amount float64
|
||||
Date string
|
||||
Total float64
|
||||
}
|
||||
|
||||
trade := HitBTCWebsocketNewTrade{}
|
||||
trade.Type = msgData["type"].(string)
|
||||
|
||||
tradeIDstr := msgData["tradeID"].(string)
|
||||
trade.TradeID, _ = strconv.ParseInt(tradeIDstr, 10, 64)
|
||||
|
||||
rateStr := msgData["rate"].(string)
|
||||
trade.Rate, _ = strconv.ParseFloat(rateStr, 64)
|
||||
|
||||
amountStr := msgData["amount"].(string)
|
||||
trade.Amount, _ = strconv.ParseFloat(amountStr, 64)
|
||||
|
||||
totalStr := msgData["total"].(string)
|
||||
trade.Rate, _ = strconv.ParseFloat(totalStr, 64)
|
||||
|
||||
trade.Date = msgData["date"].(string)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WebsocketClient initiates a websocket client
|
||||
func (p *HitBTC) WebsocketClient() {
|
||||
for p.Enabled && p.Websocket {
|
||||
c, err := turnpike.NewWebsocketClient(turnpike.JSON, hitbtcWebsocketAddress, nil)
|
||||
if h.Websocket.GetProxyAddress() != "" {
|
||||
proxy, err := url.Parse(h.Websocket.GetProxyAddress())
|
||||
if err != nil {
|
||||
log.Printf("%s Unable to connect to Websocket. Error: %s\n", p.GetName(), err)
|
||||
continue
|
||||
return err
|
||||
}
|
||||
|
||||
if p.Verbose {
|
||||
log.Printf("%s Connected to Websocket.\n", p.GetName())
|
||||
}
|
||||
dialer.Proxy = http.ProxyURL(proxy)
|
||||
}
|
||||
|
||||
_, err = c.JoinRealm(hitbtcWebsocketRealm, nil)
|
||||
var err error
|
||||
h.WebsocketConn, _, err = dialer.Dial(hitbtcWebsocketAddress, http.Header{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go h.WsReadData()
|
||||
go h.WsHandleData()
|
||||
|
||||
err = h.WsSubscribe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WsSubscribe subscribes to the relevant channels
|
||||
func (h *HitBTC) WsSubscribe() error {
|
||||
enabledPairs := h.GetEnabledCurrencies()
|
||||
for _, p := range enabledPairs {
|
||||
pF := exchange.FormatExchangeCurrency(h.GetName(), p)
|
||||
|
||||
tickerSubReq, err := common.JSONEncode(WsNotification{
|
||||
JSONRPCVersion: rpcVersion,
|
||||
Method: "subscribeTicker",
|
||||
Params: params{Symbol: pF.String()},
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("%s Unable to join realm. Error: %s\n", p.GetName(), err)
|
||||
continue
|
||||
return err
|
||||
}
|
||||
|
||||
if p.Verbose {
|
||||
log.Printf("%s Joined Websocket realm.\n", p.GetName())
|
||||
err = h.WebsocketConn.WriteMessage(websocket.TextMessage, tickerSubReq)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.ReceiveDone = make(chan bool)
|
||||
|
||||
if err := c.Subscribe(hitbtcWebsocketTicker, OnTicker); err != nil {
|
||||
log.Printf("%s Error subscribing to ticker channel: %s\n", p.GetName(), err)
|
||||
orderbookSubReq, err := common.JSONEncode(WsNotification{
|
||||
JSONRPCVersion: rpcVersion,
|
||||
Method: "subscribeOrderbook",
|
||||
Params: params{Symbol: pF.String()},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.Subscribe(hitbtcWebsocketTrollbox, OnTrollbox); err != nil {
|
||||
log.Printf("%s Error subscribing to trollbox channel: %s\n", p.GetName(), err)
|
||||
err = h.WebsocketConn.WriteMessage(websocket.TextMessage, orderbookSubReq)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for x := range p.EnabledPairs {
|
||||
currency := p.EnabledPairs[x]
|
||||
if err := c.Subscribe(currency, OnDepthOrTrade); err != nil {
|
||||
log.Printf("%s Error subscribing to %s channel: %s\n", p.GetName(), currency, err)
|
||||
tradeSubReq, err := common.JSONEncode(WsNotification{
|
||||
JSONRPCVersion: rpcVersion,
|
||||
Method: "subscribeTrades",
|
||||
Params: params{Symbol: pF.String()},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = h.WebsocketConn.WriteMessage(websocket.TextMessage, tradeSubReq)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WsReadData reads from the websocket connection
|
||||
func (h *HitBTC) WsReadData() {
|
||||
h.Websocket.Wg.Add(1)
|
||||
|
||||
defer func() {
|
||||
err := h.WebsocketConn.Close()
|
||||
if err != nil {
|
||||
h.Websocket.DataHandler <- fmt.Errorf("hitbtc_websocket.go - Unable to to close Websocket connection. Error: %s",
|
||||
err)
|
||||
}
|
||||
h.Websocket.Wg.Done()
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-h.Websocket.ShutdownC:
|
||||
return
|
||||
|
||||
default:
|
||||
_, resp, err := h.WebsocketConn.ReadMessage()
|
||||
if err != nil {
|
||||
h.Websocket.DataHandler <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if p.Verbose {
|
||||
log.Printf("%s Subscribed to websocket channels.\n", p.GetName())
|
||||
h.Websocket.TrafficAlert <- struct{}{}
|
||||
h.Websocket.Intercomm <- exchange.WebsocketResponse{Raw: resp}
|
||||
}
|
||||
|
||||
<-c.ReceiveDone
|
||||
log.Printf("%s Websocket client disconnected.\n", p.GetName())
|
||||
}
|
||||
}
|
||||
|
||||
// WsHandleData handles websocket data
|
||||
func (h *HitBTC) WsHandleData() {
|
||||
h.Websocket.Wg.Add(1)
|
||||
defer h.Websocket.Wg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-h.Websocket.ShutdownC:
|
||||
|
||||
case resp := <-h.Websocket.Intercomm:
|
||||
var init capture
|
||||
err := common.JSONDecode(resp.Raw, &init)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if init.Error.Message != "" || init.Error.Code != 0 {
|
||||
h.Websocket.DataHandler <- fmt.Errorf("hitbtc.go error - Code: %d, Message: %s",
|
||||
init.Error.Code,
|
||||
init.Error.Message)
|
||||
continue
|
||||
}
|
||||
|
||||
if init.Result {
|
||||
continue
|
||||
}
|
||||
|
||||
switch init.Method {
|
||||
case "ticker":
|
||||
var ticker WsTicker
|
||||
err := common.JSONDecode(resp.Raw, &ticker)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
ts, err := time.Parse(time.RFC3339, ticker.Params.Timestamp)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
h.Websocket.DataHandler <- exchange.TickerData{
|
||||
Exchange: h.GetName(),
|
||||
AssetType: "SPOT",
|
||||
Pair: pair.NewCurrencyPairFromString(ticker.Params.Symbol),
|
||||
Quantity: ticker.Params.Volume,
|
||||
Timestamp: ts,
|
||||
OpenPrice: ticker.Params.Open,
|
||||
HighPrice: ticker.Params.High,
|
||||
LowPrice: ticker.Params.Low,
|
||||
}
|
||||
|
||||
case "snapshotOrderbook":
|
||||
var obSnapshot WsOrderbook
|
||||
err := common.JSONDecode(resp.Raw, &obSnapshot)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = h.WsProcessOrderbookSnapshot(obSnapshot)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
case "updateOrderbook":
|
||||
var obUpdate WsOrderbook
|
||||
err := common.JSONDecode(resp.Raw, &obUpdate)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
h.WsProcessOrderbookUpdate(obUpdate)
|
||||
|
||||
case "snapshotTrades":
|
||||
var tradeSnapshot WsTrade
|
||||
err := common.JSONDecode(resp.Raw, &tradeSnapshot)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
case "updateTrades":
|
||||
var tradeUpdates WsTrade
|
||||
err := common.JSONDecode(resp.Raw, &tradeUpdates)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WsProcessOrderbookSnapshot processes a full orderbook snapshot to a local cache
|
||||
func (h *HitBTC) WsProcessOrderbookSnapshot(ob WsOrderbook) error {
|
||||
if len(ob.Params.Bid) == 0 || len(ob.Params.Ask) == 0 {
|
||||
return errors.New("hitbtc.go error - no orderbooks to process")
|
||||
}
|
||||
|
||||
var bids []orderbook.Item
|
||||
for _, bid := range ob.Params.Bid {
|
||||
bids = append(bids, orderbook.Item{Amount: bid.Size, Price: bid.Price})
|
||||
}
|
||||
|
||||
var asks []orderbook.Item
|
||||
for _, ask := range ob.Params.Ask {
|
||||
asks = append(asks, orderbook.Item{Amount: ask.Size, Price: ask.Price})
|
||||
}
|
||||
|
||||
p := pair.NewCurrencyPairFromString(ob.Params.Symbol)
|
||||
|
||||
var newOrderbook orderbook.Base
|
||||
newOrderbook.Asks = asks
|
||||
newOrderbook.Bids = bids
|
||||
newOrderbook.AssetType = "SPOT"
|
||||
newOrderbook.CurrencyPair = ob.Params.Symbol
|
||||
newOrderbook.LastUpdated = time.Now()
|
||||
newOrderbook.Pair = p
|
||||
|
||||
err := h.Websocket.Orderbook.LoadSnapshot(newOrderbook, h.GetName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{
|
||||
Exchange: h.GetName(),
|
||||
Asset: "SPOT",
|
||||
Pair: p,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WsProcessOrderbookUpdate updates a local cache
|
||||
func (h *HitBTC) WsProcessOrderbookUpdate(ob WsOrderbook) error {
|
||||
if len(ob.Params.Bid) == 0 && len(ob.Params.Ask) == 0 {
|
||||
return errors.New("hitbtc_websocket.go error - no data")
|
||||
}
|
||||
|
||||
var bids, asks []orderbook.Item
|
||||
for _, bid := range ob.Params.Bid {
|
||||
bids = append(bids, orderbook.Item{Price: bid.Price, Amount: bid.Size})
|
||||
}
|
||||
|
||||
for _, ask := range ob.Params.Ask {
|
||||
asks = append(asks, orderbook.Item{Price: ask.Price, Amount: ask.Size})
|
||||
}
|
||||
|
||||
p := pair.NewCurrencyPairFromString(ob.Params.Symbol)
|
||||
|
||||
err := h.Websocket.Orderbook.Update(bids, asks, p, time.Now(), h.GetName(), "SPOT")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{
|
||||
Exchange: h.GetName(),
|
||||
Asset: "SPOT",
|
||||
Pair: p,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type capture struct {
|
||||
Method string `json:"method"`
|
||||
Result bool `json:"result"`
|
||||
Error struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
// WsRequest defines a request obj for the JSON-RPC and gets a websocket
|
||||
// response
|
||||
type WsRequest struct {
|
||||
Method string `json:"method"`
|
||||
Params interface{} `json:"params,omitempty"`
|
||||
ID interface{} `json:"id"`
|
||||
}
|
||||
|
||||
// WsNotification defines a notification obj for the JSON-RPC this does not get
|
||||
// a websocket response
|
||||
type WsNotification struct {
|
||||
JSONRPCVersion string `json:"jsonrpc"`
|
||||
Method string `json:"method"`
|
||||
Params interface{} `json:"params"`
|
||||
}
|
||||
|
||||
type params struct {
|
||||
Symbol string `json:"symbol"`
|
||||
}
|
||||
|
||||
// WsTicker defines websocket ticker feed return params
|
||||
type WsTicker struct {
|
||||
Params struct {
|
||||
Ask float64 `json:"ask,string"`
|
||||
Bid float64 `json:"bid,string"`
|
||||
Last float64 `json:"last,string"`
|
||||
Open float64 `json:"open,string"`
|
||||
Low float64 `json:"low,string"`
|
||||
High float64 `json:"high,string"`
|
||||
Volume float64 `json:"volume,string"`
|
||||
VolumeQuote float64 `json:"volumeQuote,string"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Symbol string `json:"symbol"`
|
||||
} `json:"params"`
|
||||
}
|
||||
|
||||
// WsOrderbook defines websocket orderbook feed return params
|
||||
type WsOrderbook struct {
|
||||
Params struct {
|
||||
Ask []struct {
|
||||
Price float64 `json:"price,string"`
|
||||
Size float64 `json:"size,string"`
|
||||
} `json:"ask"`
|
||||
Bid []struct {
|
||||
Price float64 `json:"price,string"`
|
||||
Size float64 `json:"size,string"`
|
||||
} `json:"bid"`
|
||||
Symbol string `json:"symbol"`
|
||||
Sequence int64 `json:"sequence"`
|
||||
} `json:"params"`
|
||||
}
|
||||
|
||||
// WsTrade defines websocket trade feed return params
|
||||
type WsTrade struct {
|
||||
Params struct {
|
||||
Data []struct {
|
||||
ID int64 `json:"id"`
|
||||
Price float64 `json:"price,string"`
|
||||
Quantity float64 `json:"quantity,string"`
|
||||
Side string `json:"side"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
} `json:"data"`
|
||||
Symbol string `json:"symbol"`
|
||||
} `json:"params"`
|
||||
}
|
||||
|
||||
@@ -24,15 +24,11 @@ func (h *HitBTC) Start(wg *sync.WaitGroup) {
|
||||
// Run implements the HitBTC wrapper
|
||||
func (h *HitBTC) Run() {
|
||||
if h.Verbose {
|
||||
log.Printf("%s Websocket: %s (url: %s).\n", h.GetName(), common.IsEnabled(h.Websocket), hitbtcWebsocketAddress)
|
||||
log.Printf("%s Websocket: %s (url: %s).\n", h.GetName(), common.IsEnabled(h.Websocket.IsEnabled()), hitbtcWebsocketAddress)
|
||||
log.Printf("%s polling delay: %ds.\n", h.GetName(), h.RESTPollingDelay)
|
||||
log.Printf("%s %d currencies enabled: %s.\n", h.GetName(), len(h.EnabledPairs), h.EnabledPairs)
|
||||
}
|
||||
|
||||
if h.Websocket {
|
||||
go h.WebsocketClient()
|
||||
}
|
||||
|
||||
exchangeProducts, err := h.GetSymbolsDetailed()
|
||||
if err != nil {
|
||||
log.Printf("%s Failed to get available symbols.\n", h.GetName())
|
||||
@@ -207,3 +203,8 @@ func (h *HitBTC) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount fl
|
||||
func (h *HitBTC) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) {
|
||||
return "", errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// GetWebsocket returns a pointer to the exchange websocket
|
||||
func (h *HitBTC) GetWebsocket() (*exchange.Websocket, error) {
|
||||
return h.Websocket, nil
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/thrasher-/gocryptotrader/common"
|
||||
"github.com/thrasher-/gocryptotrader/config"
|
||||
exchange "github.com/thrasher-/gocryptotrader/exchanges"
|
||||
@@ -62,6 +63,7 @@ const (
|
||||
// HUOBI is the overarching type across this package
|
||||
type HUOBI struct {
|
||||
exchange.Base
|
||||
WebsocketConn *websocket.Conn
|
||||
}
|
||||
|
||||
// SetDefaults sets default values for the exchange
|
||||
@@ -70,7 +72,6 @@ func (h *HUOBI) SetDefaults() {
|
||||
h.Enabled = false
|
||||
h.Fee = 0
|
||||
h.Verbose = false
|
||||
h.Websocket = false
|
||||
h.RESTPollingDelay = 10
|
||||
h.RequestCurrencyPairFormat.Delimiter = ""
|
||||
h.RequestCurrencyPairFormat.Uppercase = false
|
||||
@@ -85,6 +86,7 @@ func (h *HUOBI) SetDefaults() {
|
||||
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
|
||||
h.APIUrlDefault = huobiAPIURL
|
||||
h.APIUrl = h.APIUrlDefault
|
||||
h.WebsocketInit()
|
||||
}
|
||||
|
||||
// Setup sets user configuration
|
||||
@@ -101,7 +103,7 @@ func (h *HUOBI) Setup(exch config.ExchangeConfig) {
|
||||
h.SetHTTPClientUserAgent(exch.HTTPUserAgent)
|
||||
h.RESTPollingDelay = exch.RESTPollingDelay
|
||||
h.Verbose = exch.Verbose
|
||||
h.Websocket = exch.Websocket
|
||||
h.Websocket.SetEnabled(exch.Websocket)
|
||||
h.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",")
|
||||
h.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",")
|
||||
h.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",")
|
||||
@@ -121,6 +123,18 @@ func (h *HUOBI) Setup(exch config.ExchangeConfig) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = h.SetClientProxyAddress(exch.ProxyAddress)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = h.WebsocketSetup(h.WsConnect,
|
||||
exch.Name,
|
||||
exch.Websocket,
|
||||
huobiSocketIOAddress,
|
||||
exch.WebsocketURL)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,237 +1,343 @@
|
||||
package huobi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/thrasher-/gocryptotrader/common"
|
||||
"github.com/thrasher-/socketio"
|
||||
"github.com/thrasher-/gocryptotrader/currency/pair"
|
||||
"github.com/thrasher-/gocryptotrader/exchanges"
|
||||
"github.com/thrasher-/gocryptotrader/exchanges/orderbook"
|
||||
)
|
||||
|
||||
const (
|
||||
huobiSocketIOAddress = "https://hq.huobi.com:443"
|
||||
|
||||
//Service API
|
||||
huobiSocketReqSymbolList = "reqSymbolList"
|
||||
huobiSocketReqSymbolDetail = "reqSymbolDetail"
|
||||
huobiSocketReqSubscribe = "reqMsgSubscribe"
|
||||
huobiSocketReqUnsubscribe = "reqMsgUnsubscribe"
|
||||
|
||||
// Market data API
|
||||
huobiSocketMarketDetail = "marketDetail"
|
||||
huobiSocketTradeDetail = "tradeDetail"
|
||||
huobiSocketMarketDepthTop = "marketDepthTop"
|
||||
huobiSocketMarketDepthTopShort = "marketDepthTopShort"
|
||||
huobiSocketMarketDepth = "marketDepth"
|
||||
huobiSocketMarketDepthTopDiff = "marketDepthTopDiff"
|
||||
huobiSocketMarketDepthDiff = "marketDepthDiff"
|
||||
huobiSocketMarketLastKline = "lastKLine"
|
||||
huobiSocketMarketLastTimeline = "lastTimeLine"
|
||||
huobiSocketMarketOverview = "marketOverview"
|
||||
huobiSocketMarketStatic = "marketStatic"
|
||||
|
||||
// History data API
|
||||
huobiSocketReqTimeline = "reqTimeLine"
|
||||
huobiSocketReqKline = "reqKLine"
|
||||
huobiSocketReqDepthTop = "reqMarketDepthTop"
|
||||
huobiSocketReqDepth = "reqMarketDepth"
|
||||
huobiSocketReqTradeDetailTop = "reqTradeDetailTop"
|
||||
huobiSocketReqMarketDetail = "reqMarketDetail"
|
||||
huobiSocketIOAddress = "wss://api.huobi.pro/ws"
|
||||
wsMarketKline = "market.%s.kline.1min"
|
||||
wsMarketDepth = "market.%s.depth.step0"
|
||||
wsMarketTrade = "market.%s.trade.detail"
|
||||
)
|
||||
|
||||
// HuobiSocket is a pointer to a IO Socket
|
||||
var HuobiSocket *socketio.SocketIO
|
||||
|
||||
// Depth holds depth information
|
||||
type Depth struct {
|
||||
SymbolID string `json:"symbolId"`
|
||||
Time float64 `json:"time"`
|
||||
Version float64 `json:"version"`
|
||||
BidName string `json:"bidName"`
|
||||
BidPrice []float64 `json:"bidPrice"`
|
||||
BidTotal []float64 `json:"bidTotal"`
|
||||
BidAmount []float64 `json:"bidAmount"`
|
||||
AskName string `json:"askName"`
|
||||
AskPrice []float64 `json:"askPrice"`
|
||||
AskTotal []float64 `json:"askTotal"`
|
||||
AskAmount []float64 `json:"askAmount"`
|
||||
}
|
||||
|
||||
// WebsocketTrade holds full trade data
|
||||
type WebsocketTrade struct {
|
||||
Price []float64 `json:"price"`
|
||||
Level []float64 `json:"level"`
|
||||
Amount []float64 `json:"amount"`
|
||||
AccuAmount []float64 `json:"accuAmount"`
|
||||
}
|
||||
|
||||
// WebsocketTradeDetail holds specific trade details
|
||||
type WebsocketTradeDetail struct {
|
||||
SymbolID string `json:"symbolId"`
|
||||
TradeID []int64 `json:"tradeId"`
|
||||
Price []float64 `json:"price"`
|
||||
Time []int64 `json:"time"`
|
||||
Amount []float64 `json:"amount"`
|
||||
TopBids []WebsocketTrade `json:"topBids"`
|
||||
TopAsks []WebsocketTrade `json:"topAsks"`
|
||||
}
|
||||
|
||||
// WebsocketMarketOverview holds market overview data
|
||||
type WebsocketMarketOverview struct {
|
||||
SymbolID string `json:"symbolId"`
|
||||
Last float64 `json:"priceNew"`
|
||||
Open float64 `json:"priceOpen"`
|
||||
High float64 `json:"priceHigh"`
|
||||
Low float64 `json:"priceLow"`
|
||||
Ask float64 `json:"priceAsk"`
|
||||
Bid float64 `json:"priceBid"`
|
||||
Volume float64 `json:"totalVolume"`
|
||||
TotalAmount float64 `json:"totalAmount"`
|
||||
}
|
||||
|
||||
// WebsocketLastTimeline holds timeline data
|
||||
type WebsocketLastTimeline struct {
|
||||
ID int64 `json:"_id"`
|
||||
SymbolID string `json:"symbolId"`
|
||||
Time int64 `json:"time"`
|
||||
LastPrice float64 `json:"priceLast"`
|
||||
Amount float64 `json:"amount"`
|
||||
Volume float64 `json:"volume"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
// WebsocketResponse is a general response type for websocket
|
||||
type WebsocketResponse struct {
|
||||
Version int `json:"version"`
|
||||
MsgType string `json:"msgType"`
|
||||
RequestIndex int64 `json:"requestIndex"`
|
||||
RetCode int64 `json:"retCode"`
|
||||
RetMessage string `json:"retMsg"`
|
||||
Payload map[string]interface{} `json:"payload"`
|
||||
}
|
||||
|
||||
// BuildHuobiWebsocketRequest packages a new request
|
||||
func (h *HUOBI) BuildHuobiWebsocketRequest(msgType string, requestIndex int64, symbolRequest []string) map[string]interface{} {
|
||||
request := map[string]interface{}{}
|
||||
request["version"] = 1
|
||||
request["msgType"] = msgType
|
||||
|
||||
if requestIndex != 0 {
|
||||
request["requestIndex"] = requestIndex
|
||||
// WsConnect initiates a new websocket connection
|
||||
func (h *HUOBI) WsConnect() error {
|
||||
if !h.Websocket.IsEnabled() || !h.IsEnabled() {
|
||||
return errors.New(exchange.WebsocketNotEnabled)
|
||||
}
|
||||
|
||||
if len(symbolRequest) != 0 {
|
||||
request["symbolIdList"] = symbolRequest
|
||||
}
|
||||
var dialer websocket.Dialer
|
||||
|
||||
return request
|
||||
}
|
||||
|
||||
// BuildHuobiWebsocketRequestExtra packages an extra request
|
||||
func (h *HUOBI) BuildHuobiWebsocketRequestExtra(msgType string, requestIndex int64, symbolIDList interface{}) interface{} {
|
||||
request := map[string]interface{}{}
|
||||
request["version"] = 1
|
||||
request["msgType"] = msgType
|
||||
|
||||
if requestIndex != 0 {
|
||||
request["requestIndex"] = requestIndex
|
||||
}
|
||||
|
||||
request["symbolList"] = symbolIDList
|
||||
return request
|
||||
}
|
||||
|
||||
// BuildHuobiWebsocketParamsList packages a parameter list
|
||||
func (h *HUOBI) BuildHuobiWebsocketParamsList(objectName, currency, pushType, period, count, from, to, percentage string) interface{} {
|
||||
list := map[string]interface{}{}
|
||||
list["symbolId"] = currency
|
||||
list["pushType"] = pushType
|
||||
|
||||
if period != "" {
|
||||
list["period"] = period
|
||||
}
|
||||
if percentage != "" {
|
||||
list["percent"] = percentage
|
||||
}
|
||||
if count != "" {
|
||||
list["count"] = count
|
||||
}
|
||||
if from != "" {
|
||||
list["from"] = from
|
||||
}
|
||||
if to != "" {
|
||||
list["to"] = to
|
||||
}
|
||||
|
||||
listArray := []map[string]interface{}{}
|
||||
listArray = append(listArray, list)
|
||||
|
||||
listCompleted := make(map[string][]map[string]interface{})
|
||||
listCompleted[objectName] = listArray
|
||||
return listCompleted
|
||||
}
|
||||
|
||||
// OnConnect handles connection establishment
|
||||
func (h *HUOBI) OnConnect(output chan socketio.Message) {
|
||||
if h.Verbose {
|
||||
log.Printf("%s Connected to Websocket.", h.GetName())
|
||||
}
|
||||
|
||||
for _, x := range h.EnabledPairs {
|
||||
currency := common.StringToLower(x)
|
||||
msg := h.BuildHuobiWebsocketRequestExtra(huobiSocketReqSubscribe, 100, h.BuildHuobiWebsocketParamsList(huobiSocketMarketOverview, currency, "pushLong", "", "", "", "", ""))
|
||||
result, err := common.JSONEncode(msg)
|
||||
if h.Websocket.GetProxyAddress() != "" {
|
||||
proxy, err := url.Parse(h.Websocket.GetProxyAddress())
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return err
|
||||
}
|
||||
output <- socketio.CreateMessageEvent("request", string(result), nil, HuobiSocket.Version)
|
||||
|
||||
dialer.Proxy = http.ProxyURL(proxy)
|
||||
}
|
||||
}
|
||||
|
||||
// OnDisconnect handles disconnection
|
||||
func (h *HUOBI) OnDisconnect(output chan socketio.Message) {
|
||||
log.Printf("%s Disconnected from websocket server.. Reconnecting.\n", h.GetName())
|
||||
h.WebsocketClient()
|
||||
}
|
||||
|
||||
// OnError handles error issues
|
||||
func (h *HUOBI) OnError() {
|
||||
log.Printf("%s Error with Websocket connection.. Reconnecting.\n", h.GetName())
|
||||
h.WebsocketClient()
|
||||
}
|
||||
|
||||
// OnMessage handles messages from the exchange
|
||||
func (h *HUOBI) OnMessage(message []byte, output chan socketio.Message) {
|
||||
}
|
||||
|
||||
// OnRequest handles requests
|
||||
func (h *HUOBI) OnRequest(message []byte, output chan socketio.Message) {
|
||||
response := WebsocketResponse{}
|
||||
err := common.JSONDecode(message, &response)
|
||||
var err error
|
||||
h.WebsocketConn, _, err = dialer.Dial(h.Websocket.GetWebsocketURL(), http.Header{})
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return err
|
||||
}
|
||||
|
||||
go h.WsHandleData()
|
||||
go h.WsReadData()
|
||||
|
||||
err = h.WsSubscribe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WebsocketClient creates a new websocket client
|
||||
func (h *HUOBI) WebsocketClient() {
|
||||
events := make(map[string]func(message []byte, output chan socketio.Message))
|
||||
events["request"] = h.OnRequest
|
||||
events["message"] = h.OnMessage
|
||||
// WsReadData reads data from the websocket connection
|
||||
func (h *HUOBI) WsReadData() {
|
||||
h.Websocket.Wg.Add(1)
|
||||
|
||||
HuobiSocket = &socketio.SocketIO{
|
||||
Version: 0.9,
|
||||
OnConnect: h.OnConnect,
|
||||
OnEvent: events,
|
||||
OnError: h.OnError,
|
||||
OnDisconnect: h.OnDisconnect,
|
||||
}
|
||||
|
||||
for h.Enabled && h.Websocket {
|
||||
err := socketio.ConnectToSocket(huobiSocketIOAddress, HuobiSocket)
|
||||
defer func() {
|
||||
err := h.WebsocketConn.Close()
|
||||
if err != nil {
|
||||
log.Printf("%s Unable to connect to Websocket. Err: %s\n", h.GetName(), err)
|
||||
continue
|
||||
h.Websocket.DataHandler <- fmt.Errorf("huobi_websocket.go - Unable to to close Websocket connection. Error: %s",
|
||||
err)
|
||||
}
|
||||
h.Websocket.Wg.Done()
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-h.Websocket.ShutdownC:
|
||||
return
|
||||
|
||||
default:
|
||||
_, resp, err := h.WebsocketConn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
h.Websocket.TrafficAlert <- struct{}{}
|
||||
|
||||
b := bytes.NewReader(resp)
|
||||
gReader, err := gzip.NewReader(b)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
unzipped, err := ioutil.ReadAll(gReader)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
gReader.Close()
|
||||
|
||||
h.Websocket.Intercomm <- exchange.WebsocketResponse{Raw: unzipped}
|
||||
}
|
||||
log.Printf("%s Disconnected from Websocket.\n", h.GetName())
|
||||
}
|
||||
}
|
||||
|
||||
// WsHandleData handles data read from the websocket connection
|
||||
func (h *HUOBI) WsHandleData() {
|
||||
h.Websocket.Wg.Add(1)
|
||||
defer h.Websocket.Wg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-h.Websocket.ShutdownC:
|
||||
case resp := <-h.Websocket.Intercomm:
|
||||
var init WsResponse
|
||||
err := common.JSONDecode(resp.Raw, &init)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if init.Status == "error" {
|
||||
h.Websocket.DataHandler <- fmt.Errorf("huobi.go Websocker error %s %s",
|
||||
init.ErrorCode,
|
||||
init.ErrorMessage)
|
||||
continue
|
||||
}
|
||||
|
||||
if init.Subscribed != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if init.Ping != 0 {
|
||||
err = h.WebsocketConn.WriteJSON(`{"pong":1337}`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case common.StringContains(init.Channel, "depth"):
|
||||
var depth WsDepth
|
||||
err := common.JSONDecode(resp.Raw, &depth)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
data := common.SplitStrings(depth.Channel, ".")
|
||||
|
||||
h.WsProcessOrderbook(depth, data[1])
|
||||
|
||||
case common.StringContains(init.Channel, "kline"):
|
||||
var kline WsKline
|
||||
err := common.JSONDecode(resp.Raw, &kline)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
data := common.SplitStrings(kline.Channel, ".")
|
||||
|
||||
h.Websocket.DataHandler <- exchange.KlineData{
|
||||
Timestamp: time.Unix(0, kline.Timestamp),
|
||||
Exchange: h.GetName(),
|
||||
AssetType: "SPOT",
|
||||
Pair: pair.NewCurrencyPairFromString(data[1]),
|
||||
OpenPrice: kline.Tick.Open,
|
||||
ClosePrice: kline.Tick.Close,
|
||||
HighPrice: kline.Tick.High,
|
||||
LowPrice: kline.Tick.Low,
|
||||
Volume: kline.Tick.Volume,
|
||||
}
|
||||
|
||||
case common.StringContains(init.Channel, "trade"):
|
||||
var trade WsTrade
|
||||
err := common.JSONDecode(resp.Raw, &trade)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
data := common.SplitStrings(trade.Channel, ".")
|
||||
|
||||
h.Websocket.DataHandler <- exchange.TradeData{
|
||||
Exchange: h.GetName(),
|
||||
AssetType: "SPOT",
|
||||
CurrencyPair: pair.NewCurrencyPairFromString(data[1]),
|
||||
Timestamp: time.Unix(0, trade.Tick.Timestamp),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WsProcessOrderbook processes new orderbook data
|
||||
func (h *HUOBI) WsProcessOrderbook(ob WsDepth, symbol string) error {
|
||||
var bids []orderbook.Item
|
||||
for _, data := range ob.Tick.Bids {
|
||||
bidLevel := data.([]interface{})
|
||||
bids = append(bids, orderbook.Item{Price: bidLevel[0].(float64),
|
||||
Amount: bidLevel[0].(float64)})
|
||||
}
|
||||
|
||||
var asks []orderbook.Item
|
||||
for _, data := range ob.Tick.Asks {
|
||||
askLevel := data.([]interface{})
|
||||
asks = append(asks, orderbook.Item{Price: askLevel[0].(float64),
|
||||
Amount: askLevel[0].(float64)})
|
||||
}
|
||||
|
||||
p := pair.NewCurrencyPairFromString(symbol)
|
||||
|
||||
var newOrderbook orderbook.Base
|
||||
newOrderbook.Asks = asks
|
||||
newOrderbook.Bids = bids
|
||||
newOrderbook.CurrencyPair = symbol
|
||||
newOrderbook.LastUpdated = time.Now()
|
||||
newOrderbook.Pair = p
|
||||
|
||||
err := h.Websocket.Orderbook.LoadSnapshot(newOrderbook, h.GetName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{
|
||||
Pair: p,
|
||||
Exchange: h.GetName(),
|
||||
Asset: "SPOT",
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WsSubscribe susbcribes to the current websocket streams based on the enabled
|
||||
// pair
|
||||
func (h *HUOBI) WsSubscribe() error {
|
||||
pairs := h.GetEnabledCurrencies()
|
||||
|
||||
for _, p := range pairs {
|
||||
fPair := exchange.FormatExchangeCurrency(h.GetName(), p)
|
||||
|
||||
depthTopic := fmt.Sprintf(wsMarketDepth, fPair.String())
|
||||
depthJSON, err := common.JSONEncode(WsRequest{Subscribe: depthTopic})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = h.WebsocketConn.WriteMessage(websocket.TextMessage, depthJSON)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
klineTopic := fmt.Sprintf(wsMarketKline, fPair.String())
|
||||
KlineJSON, err := common.JSONEncode(WsRequest{Subscribe: klineTopic})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = h.WebsocketConn.WriteMessage(websocket.TextMessage, KlineJSON)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tradeTopic := fmt.Sprintf(wsMarketTrade, fPair.String())
|
||||
tradeJSON, err := common.JSONEncode(WsRequest{Subscribe: tradeTopic})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = h.WebsocketConn.WriteMessage(websocket.TextMessage, tradeJSON)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WsRequest defines a request data structure
|
||||
type WsRequest struct {
|
||||
Topic string `json:"req,omitempty"`
|
||||
Subscribe string `json:"sub,omitempty"`
|
||||
ClientGeneratedID string `json:"id,omitempty"`
|
||||
}
|
||||
|
||||
// WsResponse defines a response from the websocket connection when there
|
||||
// is an error
|
||||
type WsResponse struct {
|
||||
TS int64 `json:"ts"`
|
||||
Status string `json:"status"`
|
||||
ErrorCode string `json:"err-code"`
|
||||
ErrorMessage string `json:"err-msg"`
|
||||
Ping int64 `json:"ping"`
|
||||
Channel string `json:"ch"`
|
||||
Subscribed string `json:"subbed"`
|
||||
}
|
||||
|
||||
// WsHeartBeat defines a heartbeat request
|
||||
type WsHeartBeat struct {
|
||||
ClientNonce int64 `json:"ping"`
|
||||
}
|
||||
|
||||
// WsDepth defines market depth websocket response
|
||||
type WsDepth struct {
|
||||
Channel string `json:"ch"`
|
||||
Timestamp int64 `json:"ts"`
|
||||
Tick struct {
|
||||
Bids []interface{} `json:"bids"`
|
||||
Asks []interface{} `json:"asks"`
|
||||
Timestamp int64 `json:"ts"`
|
||||
Version int64 `json:"version"`
|
||||
} `json:"tick"`
|
||||
}
|
||||
|
||||
// WsKline defines market kline websocket response
|
||||
type WsKline struct {
|
||||
Channel string `json:"ch"`
|
||||
Timestamp int64 `json:"ts"`
|
||||
Tick struct {
|
||||
ID int64 `json:"id"`
|
||||
Open float64 `json:"open"`
|
||||
Close float64 `json:"close"`
|
||||
Low float64 `json:"low"`
|
||||
High float64 `json:"high"`
|
||||
Amount float64 `json:"amount"`
|
||||
Volume float64 `json:"vol"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
}
|
||||
|
||||
// WsTrade defines market trade websocket response
|
||||
type WsTrade struct {
|
||||
Channel string `json:"ch"`
|
||||
Timestamp int64 `json:"ts"`
|
||||
Tick struct {
|
||||
ID int64 `json:"id"`
|
||||
Timestamp int64 `json:"ts"`
|
||||
Data []struct {
|
||||
Amount float64 `json:"amount"`
|
||||
Timestamp int64 `json:"ts"`
|
||||
ID big.Int `json:"id,number"`
|
||||
Price float64 `json:"price"`
|
||||
Direction string `json:"direction"`
|
||||
} `json:"data"`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,15 +25,11 @@ func (h *HUOBI) Start(wg *sync.WaitGroup) {
|
||||
// Run implements the HUOBI wrapper
|
||||
func (h *HUOBI) Run() {
|
||||
if h.Verbose {
|
||||
log.Printf("%s Websocket: %s (url: %s).\n", h.GetName(), common.IsEnabled(h.Websocket), huobiSocketIOAddress)
|
||||
log.Printf("%s Websocket: %s (url: %s).\n", h.GetName(), common.IsEnabled(h.Websocket.IsEnabled()), huobiSocketIOAddress)
|
||||
log.Printf("%s polling delay: %ds.\n", h.GetName(), h.RESTPollingDelay)
|
||||
log.Printf("%s %d currencies enabled: %s.\n", h.GetName(), len(h.EnabledPairs), h.EnabledPairs)
|
||||
}
|
||||
|
||||
if h.Websocket {
|
||||
go h.WebsocketClient()
|
||||
}
|
||||
|
||||
exchangeProducts, err := h.GetSymbols()
|
||||
if err != nil {
|
||||
log.Printf("%s Failed to get available symbols.\n", h.GetName())
|
||||
@@ -222,3 +218,8 @@ func (h *HUOBI) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount flo
|
||||
func (h *HUOBI) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) {
|
||||
return "", errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// GetWebsocket returns a pointer to the exchange websocket
|
||||
func (h *HUOBI) GetWebsocket() (*exchange.Websocket, error) {
|
||||
return h.Websocket, nil
|
||||
}
|
||||
|
||||
@@ -64,7 +64,6 @@ func (h *HUOBIHADAX) SetDefaults() {
|
||||
h.Enabled = false
|
||||
h.Fee = 0
|
||||
h.Verbose = false
|
||||
h.Websocket = false
|
||||
h.RESTPollingDelay = 10
|
||||
h.RequestCurrencyPairFormat.Delimiter = ""
|
||||
h.RequestCurrencyPairFormat.Uppercase = false
|
||||
@@ -79,6 +78,7 @@ func (h *HUOBIHADAX) SetDefaults() {
|
||||
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
|
||||
h.APIUrlDefault = huobihadaxAPIURL
|
||||
h.APIUrl = h.APIUrlDefault
|
||||
h.WebsocketInit()
|
||||
}
|
||||
|
||||
// Setup sets user configuration
|
||||
@@ -95,7 +95,6 @@ func (h *HUOBIHADAX) Setup(exch config.ExchangeConfig) {
|
||||
h.SetHTTPClientUserAgent(exch.HTTPUserAgent)
|
||||
h.RESTPollingDelay = exch.RESTPollingDelay
|
||||
h.Verbose = exch.Verbose
|
||||
h.Websocket = exch.Websocket
|
||||
h.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",")
|
||||
h.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",")
|
||||
h.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",")
|
||||
@@ -115,6 +114,10 @@ func (h *HUOBIHADAX) Setup(exch config.ExchangeConfig) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = h.SetClientProxyAddress(exch.ProxyAddress)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ func (h *HUOBIHADAX) Start(wg *sync.WaitGroup) {
|
||||
// Run implements the OKEX wrapper
|
||||
func (h *HUOBIHADAX) Run() {
|
||||
if h.Verbose {
|
||||
log.Printf("%s Websocket: %s. (url: %s).\n", h.GetName(), common.IsEnabled(h.Websocket), h.WebsocketURL)
|
||||
log.Printf("%s Websocket: %s. (url: %s).\n", h.GetName(), common.IsEnabled(h.Websocket.IsEnabled()), h.WebsocketURL)
|
||||
log.Printf("%s polling delay: %ds.\n", h.GetName(), h.RESTPollingDelay)
|
||||
log.Printf("%s %d currencies enabled: %s.\n", h.GetName(), len(h.EnabledPairs), h.EnabledPairs)
|
||||
}
|
||||
@@ -183,3 +183,8 @@ func (h *HUOBIHADAX) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amoun
|
||||
func (h *HUOBIHADAX) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) {
|
||||
return "", errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// GetWebsocket returns a pointer to the exchange websocket
|
||||
func (h *HUOBIHADAX) GetWebsocket() (*exchange.Websocket, error) {
|
||||
return nil, errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
@@ -46,7 +46,6 @@ func (i *ItBit) SetDefaults() {
|
||||
i.MakerFee = -0.10
|
||||
i.TakerFee = 0.50
|
||||
i.Verbose = false
|
||||
i.Websocket = false
|
||||
i.RESTPollingDelay = 10
|
||||
i.RequestCurrencyPairFormat.Delimiter = ""
|
||||
i.RequestCurrencyPairFormat.Uppercase = true
|
||||
@@ -61,6 +60,7 @@ func (i *ItBit) SetDefaults() {
|
||||
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
|
||||
i.APIUrlDefault = itbitAPIURL
|
||||
i.APIUrl = i.APIUrlDefault
|
||||
i.WebsocketInit()
|
||||
}
|
||||
|
||||
// Setup sets the exchange parameters from exchange config
|
||||
@@ -75,7 +75,6 @@ func (i *ItBit) Setup(exch config.ExchangeConfig) {
|
||||
i.SetHTTPClientUserAgent(exch.HTTPUserAgent)
|
||||
i.RESTPollingDelay = exch.RESTPollingDelay
|
||||
i.Verbose = exch.Verbose
|
||||
i.Websocket = exch.Websocket
|
||||
i.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",")
|
||||
i.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",")
|
||||
i.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",")
|
||||
@@ -95,6 +94,10 @@ func (i *ItBit) Setup(exch config.ExchangeConfig) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = i.SetClientProxyAddress(exch.ProxyAddress)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -177,3 +177,8 @@ func (i *ItBit) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount flo
|
||||
func (i *ItBit) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) {
|
||||
return "", errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// GetWebsocket returns a pointer to the exchange websocket
|
||||
func (i *ItBit) GetWebsocket() (*exchange.Websocket, error) {
|
||||
return nil, errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
@@ -58,7 +58,6 @@ func (k *Kraken) SetDefaults() {
|
||||
k.FiatFee = 0.35
|
||||
k.CryptoFee = 0.10
|
||||
k.Verbose = false
|
||||
k.Websocket = false
|
||||
k.RESTPollingDelay = 10
|
||||
k.Ticker = make(map[string]Ticker)
|
||||
k.RequestCurrencyPairFormat.Delimiter = ""
|
||||
@@ -75,6 +74,7 @@ func (k *Kraken) SetDefaults() {
|
||||
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
|
||||
k.APIUrlDefault = krakenAPIURL
|
||||
k.APIUrl = k.APIUrlDefault
|
||||
k.WebsocketInit()
|
||||
}
|
||||
|
||||
// Setup sets current exchange configuration
|
||||
@@ -89,7 +89,6 @@ func (k *Kraken) Setup(exch config.ExchangeConfig) {
|
||||
k.SetHTTPClientUserAgent(exch.HTTPUserAgent)
|
||||
k.RESTPollingDelay = exch.RESTPollingDelay
|
||||
k.Verbose = exch.Verbose
|
||||
k.Websocket = exch.Websocket
|
||||
k.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",")
|
||||
k.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",")
|
||||
k.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",")
|
||||
@@ -109,6 +108,10 @@ func (k *Kraken) Setup(exch config.ExchangeConfig) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = k.SetClientProxyAddress(exch.ProxyAddress)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -250,3 +250,8 @@ func (k *Kraken) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount fl
|
||||
func (k *Kraken) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) {
|
||||
return "", errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// GetWebsocket returns a pointer to the exchange websocket
|
||||
func (k *Kraken) GetWebsocket() (*exchange.Websocket, error) {
|
||||
return nil, errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@ func (l *LakeBTC) SetDefaults() {
|
||||
l.TakerFee = 0.2
|
||||
l.MakerFee = 0.15
|
||||
l.Verbose = false
|
||||
l.Websocket = false
|
||||
l.RESTPollingDelay = 10
|
||||
l.RequestCurrencyPairFormat.Delimiter = ""
|
||||
l.RequestCurrencyPairFormat.Uppercase = true
|
||||
@@ -62,6 +61,7 @@ func (l *LakeBTC) SetDefaults() {
|
||||
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
|
||||
l.APIUrlDefault = lakeBTCAPIURL
|
||||
l.APIUrl = l.APIUrlDefault
|
||||
l.WebsocketInit()
|
||||
}
|
||||
|
||||
// Setup sets exchange configuration profile
|
||||
@@ -76,7 +76,6 @@ func (l *LakeBTC) Setup(exch config.ExchangeConfig) {
|
||||
l.SetHTTPClientUserAgent(exch.HTTPUserAgent)
|
||||
l.RESTPollingDelay = exch.RESTPollingDelay
|
||||
l.Verbose = exch.Verbose
|
||||
l.Websocket = exch.Websocket
|
||||
l.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",")
|
||||
l.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",")
|
||||
l.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",")
|
||||
@@ -96,6 +95,10 @@ func (l *LakeBTC) Setup(exch config.ExchangeConfig) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = l.SetClientProxyAddress(exch.ProxyAddress)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -187,3 +187,8 @@ func (l *LakeBTC) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount f
|
||||
func (l *LakeBTC) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) {
|
||||
return "", errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// GetWebsocket returns a pointer to the exchange websocket
|
||||
func (l *LakeBTC) GetWebsocket() (*exchange.Websocket, error) {
|
||||
return nil, errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
@@ -50,7 +50,6 @@ func (l *Liqui) SetDefaults() {
|
||||
l.Enabled = false
|
||||
l.Fee = 0.25
|
||||
l.Verbose = false
|
||||
l.Websocket = false
|
||||
l.RESTPollingDelay = 10
|
||||
l.Ticker = make(map[string]Ticker)
|
||||
l.RequestCurrencyPairFormat.Delimiter = "_"
|
||||
@@ -69,6 +68,7 @@ func (l *Liqui) SetDefaults() {
|
||||
l.APIUrl = l.APIUrlDefault
|
||||
l.APIUrlSecondaryDefault = liquiAPIPrivateURL
|
||||
l.APIUrlSecondary = l.APIUrlSecondaryDefault
|
||||
l.WebsocketInit()
|
||||
}
|
||||
|
||||
// Setup sets exchange configuration parameters for liqui
|
||||
@@ -83,7 +83,6 @@ func (l *Liqui) Setup(exch config.ExchangeConfig) {
|
||||
l.SetHTTPClientUserAgent(exch.HTTPUserAgent)
|
||||
l.RESTPollingDelay = exch.RESTPollingDelay
|
||||
l.Verbose = exch.Verbose
|
||||
l.Websocket = exch.Websocket
|
||||
l.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",")
|
||||
l.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",")
|
||||
l.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",")
|
||||
@@ -103,6 +102,10 @@ func (l *Liqui) Setup(exch config.ExchangeConfig) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = l.SetClientProxyAddress(exch.ProxyAddress)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -196,3 +196,8 @@ func (l *Liqui) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount flo
|
||||
func (l *Liqui) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) {
|
||||
return "", errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// GetWebsocket returns a pointer to the exchange websocket
|
||||
func (l *Liqui) GetWebsocket() (*exchange.Websocket, error) {
|
||||
return nil, errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
@@ -116,7 +116,6 @@ func (l *LocalBitcoins) SetDefaults() {
|
||||
l.Enabled = false
|
||||
l.Verbose = false
|
||||
l.Verbose = false
|
||||
l.Websocket = false
|
||||
l.RESTPollingDelay = 10
|
||||
l.RequestCurrencyPairFormat.Delimiter = ""
|
||||
l.RequestCurrencyPairFormat.Uppercase = true
|
||||
@@ -130,6 +129,7 @@ func (l *LocalBitcoins) SetDefaults() {
|
||||
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
|
||||
l.APIUrlDefault = localbitcoinsAPIURL
|
||||
l.APIUrl = l.APIUrlDefault
|
||||
l.WebsocketInit()
|
||||
}
|
||||
|
||||
// Setup sets exchange configuration parameters
|
||||
@@ -144,7 +144,6 @@ func (l *LocalBitcoins) Setup(exch config.ExchangeConfig) {
|
||||
l.SetHTTPClientUserAgent(exch.HTTPUserAgent)
|
||||
l.RESTPollingDelay = exch.RESTPollingDelay
|
||||
l.Verbose = exch.Verbose
|
||||
l.Websocket = exch.Websocket
|
||||
l.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",")
|
||||
l.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",")
|
||||
l.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",")
|
||||
@@ -160,6 +159,10 @@ func (l *LocalBitcoins) Setup(exch config.ExchangeConfig) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = l.SetClientProxyAddress(exch.ProxyAddress)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -168,3 +168,8 @@ func (l *LocalBitcoins) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, am
|
||||
func (l *LocalBitcoins) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) {
|
||||
return "", errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// GetWebsocket returns a pointer to the exchange websocket
|
||||
func (l *LocalBitcoins) GetWebsocket() (*exchange.Websocket, error) {
|
||||
return nil, errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
|
||||
const (
|
||||
okcoinAPIURL = "https://www.okcoin.com/api/v1/"
|
||||
okcoinAPIURLChina = "https://www.okcoin.cn/api/v1/"
|
||||
okcoinAPIURLChina = "https://www.okcoin.com/api/v1/"
|
||||
okcoinAPIVersion = "1"
|
||||
okcoinWebsocketURL = "wss://real.okcoin.com:10440/websocket/okcoinapi"
|
||||
okcoinWebsocketURLChina = "wss://real.okcoin.cn:10440/websocket/okcoinapi"
|
||||
@@ -72,10 +72,6 @@ const (
|
||||
okcoinUnauthRate = 0
|
||||
)
|
||||
|
||||
var (
|
||||
okcoinDefaultsSet = false
|
||||
)
|
||||
|
||||
// OKCoin is the overarching type across this package
|
||||
type OKCoin struct {
|
||||
exchange.Base
|
||||
@@ -99,37 +95,11 @@ func (o *OKCoin) SetDefaults() {
|
||||
o.SetWebsocketErrorDefaults()
|
||||
o.Enabled = false
|
||||
o.Verbose = false
|
||||
o.Websocket = false
|
||||
o.RESTPollingDelay = 10
|
||||
o.AssetTypes = []string{ticker.Spot}
|
||||
o.SupportsAutoPairUpdating = false
|
||||
o.SupportsRESTTickerBatching = false
|
||||
|
||||
if okcoinDefaultsSet {
|
||||
o.APIUrlDefault = okcoinAPIURL
|
||||
o.APIUrl = o.APIUrlDefault
|
||||
o.Name = "OKCOIN International"
|
||||
o.WebsocketURL = okcoinWebsocketURL
|
||||
o.RequestCurrencyPairFormat.Delimiter = "_"
|
||||
o.RequestCurrencyPairFormat.Uppercase = false
|
||||
o.ConfigCurrencyPairFormat.Delimiter = "_"
|
||||
o.ConfigCurrencyPairFormat.Uppercase = true
|
||||
o.Requester = request.New(o.Name,
|
||||
request.NewRateLimit(time.Second, okcoinAuthRate),
|
||||
request.NewRateLimit(time.Second, okcoinUnauthRate),
|
||||
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
|
||||
} else {
|
||||
o.APIUrlDefault = okcoinAPIURLChina
|
||||
o.APIUrl = o.APIUrlDefault
|
||||
o.Name = "OKCOIN China"
|
||||
o.WebsocketURL = okcoinWebsocketURLChina
|
||||
okcoinDefaultsSet = true
|
||||
o.setCurrencyPairFormats()
|
||||
o.Requester = request.New(o.Name,
|
||||
request.NewRateLimit(time.Second, okcoinAuthRate),
|
||||
request.NewRateLimit(time.Second, okcoinUnauthRate),
|
||||
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
|
||||
}
|
||||
o.WebsocketInit()
|
||||
}
|
||||
|
||||
// Setup sets exchange configuration parameters
|
||||
@@ -137,6 +107,37 @@ func (o *OKCoin) Setup(exch config.ExchangeConfig) {
|
||||
if !exch.Enabled {
|
||||
o.SetEnabled(false)
|
||||
} else {
|
||||
if exch.Name == "OKCOIN International" {
|
||||
o.AssetTypes = append(o.AssetTypes, o.FuturesValues...)
|
||||
o.APIUrlDefault = okcoinAPIURL
|
||||
o.APIUrl = o.APIUrlDefault
|
||||
o.Name = "OKCOIN International"
|
||||
o.WebsocketURL = okcoinWebsocketURL
|
||||
o.setCurrencyPairFormats()
|
||||
o.Requester = request.New(o.Name,
|
||||
request.NewRateLimit(time.Second, okcoinAuthRate),
|
||||
request.NewRateLimit(time.Second, okcoinUnauthRate),
|
||||
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
|
||||
o.ConfigCurrencyPairFormat.Delimiter = "_"
|
||||
o.ConfigCurrencyPairFormat.Uppercase = true
|
||||
o.RequestCurrencyPairFormat.Uppercase = false
|
||||
o.RequestCurrencyPairFormat.Delimiter = "_"
|
||||
} else {
|
||||
o.APIUrlDefault = okcoinAPIURLChina
|
||||
o.APIUrl = o.APIUrlDefault
|
||||
o.Name = "OKCOIN China"
|
||||
o.WebsocketURL = okcoinWebsocketURLChina
|
||||
o.setCurrencyPairFormats()
|
||||
o.Requester = request.New(o.Name,
|
||||
request.NewRateLimit(time.Second, okcoinAuthRate),
|
||||
request.NewRateLimit(time.Second, okcoinUnauthRate),
|
||||
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
|
||||
o.ConfigCurrencyPairFormat.Delimiter = ""
|
||||
o.ConfigCurrencyPairFormat.Uppercase = true
|
||||
o.RequestCurrencyPairFormat.Uppercase = false
|
||||
o.RequestCurrencyPairFormat.Delimiter = ""
|
||||
}
|
||||
|
||||
o.Enabled = true
|
||||
o.AuthenticatedAPISupport = exch.AuthenticatedAPISupport
|
||||
o.SetAPIKeys(exch.APIKey, exch.APISecret, "", false)
|
||||
@@ -144,7 +145,7 @@ func (o *OKCoin) Setup(exch config.ExchangeConfig) {
|
||||
o.SetHTTPClientUserAgent(exch.HTTPUserAgent)
|
||||
o.RESTPollingDelay = exch.RESTPollingDelay
|
||||
o.Verbose = exch.Verbose
|
||||
o.Websocket = exch.Websocket
|
||||
o.Websocket.SetEnabled(exch.Websocket)
|
||||
o.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",")
|
||||
o.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",")
|
||||
o.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",")
|
||||
@@ -164,6 +165,18 @@ func (o *OKCoin) Setup(exch config.ExchangeConfig) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = o.SetClientProxyAddress(exch.ProxyAddress)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = o.WebsocketSetup(o.WsConnect,
|
||||
exch.Name,
|
||||
exch.Websocket,
|
||||
okcoinWebsocketURL,
|
||||
o.WebsocketURL)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -251,17 +251,6 @@ type WebsocketFutureIndex struct {
|
||||
Timestamp int64 `json:"timestamp,string"`
|
||||
}
|
||||
|
||||
// WebsocketTicker holds ticker data for websocket
|
||||
type WebsocketTicker struct {
|
||||
Timestamp float64
|
||||
Vol string
|
||||
Buy float64
|
||||
High float64
|
||||
Last float64
|
||||
Low float64
|
||||
Sell float64
|
||||
}
|
||||
|
||||
// WebsocketFuturesTicker holds futures ticker data for websocket
|
||||
type WebsocketFuturesTicker struct {
|
||||
Buy float64 `json:"buy"`
|
||||
@@ -275,13 +264,6 @@ type WebsocketFuturesTicker struct {
|
||||
Volume float64 `json:"vol,string"`
|
||||
}
|
||||
|
||||
// WebsocketOrderbook holds orderbook data for websocket
|
||||
type WebsocketOrderbook struct {
|
||||
Asks [][]float64 `json:"asks"`
|
||||
Bids [][]float64 `json:"bids"`
|
||||
Timestamp int64 `json:"timestamp,string"`
|
||||
}
|
||||
|
||||
// WebsocketUserinfo holds user info for websocket
|
||||
type WebsocketUserinfo struct {
|
||||
Info struct {
|
||||
|
||||
@@ -1,524 +1,258 @@
|
||||
package okcoin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/thrasher-/gocryptotrader/common"
|
||||
"github.com/thrasher-/gocryptotrader/currency/pair"
|
||||
"github.com/thrasher-/gocryptotrader/exchanges"
|
||||
)
|
||||
|
||||
const (
|
||||
okcoinWebsocketUSDRealTrades = "ok_usd_realtrades"
|
||||
okcoinWebsocketCNYRealTrades = "ok_cny_realtrades"
|
||||
okcoinWebsocketSpotUSDTrade = "ok_spotusd_trade"
|
||||
okcoinWebsocketSpotCNYTrade = "ok_spotcny_trade"
|
||||
okcoinWebsocketSpotUSDCancelOrder = "ok_spotusd_cancel_order"
|
||||
okcoinWebsocketSpotCNYCancelOrder = "ok_spotcny_cancel_order"
|
||||
okcoinWebsocketSpotUSDUserInfo = "ok_spotusd_userinfo"
|
||||
okcoinWebsocketSpotCNYUserInfo = "ok_spotcny_userinfo"
|
||||
okcoinWebsocketSpotUSDOrderInfo = "ok_spotusd_order_info"
|
||||
okcoinWebsocketSpotCNYOrderInfo = "ok_spotcny_order_info"
|
||||
okcoinWebsocketFuturesTrade = "ok_futuresusd_trade"
|
||||
okcoinWebsocketFuturesCancelOrder = "ok_futuresusd_cancel_order"
|
||||
okcoinWebsocketFuturesRealTrades = "ok_usd_future_realtrades"
|
||||
okcoinWebsocketFuturesUserInfo = "ok_futureusd_userinfo"
|
||||
okcoinWebsocketFuturesOrderInfo = "ok_futureusd_order_info"
|
||||
wsSubTicker = "ok_sub_spot_%s_ticker"
|
||||
wsSubDepthIncrement = "ok_sub_spot_%s_depth"
|
||||
wsSubDepthFull = "ok_sub_spot_%s_depth_%s"
|
||||
wsSubTrades = "ok_sub_spot_%s_deals"
|
||||
wsSubKline = "ok_sub_spot_%s_kline_%s"
|
||||
)
|
||||
|
||||
// PingHandler handles the keep alive
|
||||
func (o *OKCoin) PingHandler(message string) error {
|
||||
err := o.WebsocketConn.WriteControl(websocket.PingMessage, []byte("{'event':'ping'}"), time.Now().Add(time.Second))
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return o.WebsocketConn.WriteControl(websocket.PingMessage,
|
||||
[]byte("{'event':'ping'}"),
|
||||
time.Now().Add(time.Second))
|
||||
}
|
||||
|
||||
// AddChannel adds a new channel on the websocket client
|
||||
func (o *OKCoin) AddChannel(channel string) {
|
||||
func (o *OKCoin) AddChannel(channel string) error {
|
||||
event := WebsocketEvent{"addChannel", channel}
|
||||
json, err := common.JSONEncode(event)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
err = o.WebsocketConn.WriteMessage(websocket.TextMessage, json)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
if o.Verbose {
|
||||
log.Printf("%s Adding channel: %s\n", o.GetName(), channel)
|
||||
}
|
||||
return o.WebsocketConn.WriteMessage(websocket.TextMessage, json)
|
||||
}
|
||||
|
||||
// RemoveChannel removes a channel on the websocket client
|
||||
func (o *OKCoin) RemoveChannel(channel string) {
|
||||
event := WebsocketEvent{"removeChannel", channel}
|
||||
json, err := common.JSONEncode(event)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
err = o.WebsocketConn.WriteMessage(websocket.TextMessage, json)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
// WsConnect initiates a websocket connection
|
||||
func (o *OKCoin) WsConnect() error {
|
||||
if !o.Websocket.IsEnabled() || !o.IsEnabled() {
|
||||
return errors.New(exchange.WebsocketNotEnabled)
|
||||
}
|
||||
|
||||
if o.Verbose {
|
||||
log.Printf("%s Removing channel: %s\n", o.GetName(), channel)
|
||||
}
|
||||
}
|
||||
klineValues := []string{"1min", "3min", "5min", "15min", "30min", "1hour",
|
||||
"2hour", "4hour", "6hour", "12hour", "day", "3day", "week"}
|
||||
|
||||
// WebsocketSpotTrade handles spot trade request on the websocket client
|
||||
func (o *OKCoin) WebsocketSpotTrade(symbol, orderType string, price, amount float64) {
|
||||
values := make(map[string]string)
|
||||
values["symbol"] = symbol
|
||||
values["type"] = orderType
|
||||
values["price"] = strconv.FormatFloat(price, 'f', -1, 64)
|
||||
values["amount"] = strconv.FormatFloat(amount, 'f', -1, 64)
|
||||
|
||||
channel := okcoinWebsocketSpotUSDTrade
|
||||
if o.WebsocketURL == okcoinWebsocketURLChina {
|
||||
channel = okcoinWebsocketSpotCNYTrade
|
||||
}
|
||||
|
||||
o.AddChannelAuthenticated(channel, values)
|
||||
}
|
||||
|
||||
// WebsocketFuturesTrade handles a futures trade on the websocket client
|
||||
func (o *OKCoin) WebsocketFuturesTrade(symbol, contractType string, price, amount float64, orderType, matchPrice, leverage int) {
|
||||
values := make(map[string]string)
|
||||
values["symbol"] = symbol
|
||||
values["contract_type"] = contractType
|
||||
values["price"] = strconv.FormatFloat(price, 'f', -1, 64)
|
||||
values["amount"] = strconv.FormatFloat(amount, 'f', -1, 64)
|
||||
values["type"] = strconv.Itoa(orderType)
|
||||
values["match_price"] = strconv.Itoa(matchPrice)
|
||||
values["lever_rate"] = strconv.Itoa(orderType)
|
||||
o.AddChannelAuthenticated(okcoinWebsocketFuturesTrade, values)
|
||||
}
|
||||
|
||||
// WebsocketSpotCancel cancels a spot trade on the websocket client
|
||||
func (o *OKCoin) WebsocketSpotCancel(symbol string, orderID int64) {
|
||||
values := make(map[string]string)
|
||||
values["symbol"] = symbol
|
||||
values["order_id"] = strconv.FormatInt(orderID, 10)
|
||||
|
||||
channel := okcoinWebsocketSpotUSDCancelOrder
|
||||
if o.WebsocketURL == okcoinWebsocketURLChina {
|
||||
channel = okcoinWebsocketSpotCNYCancelOrder
|
||||
}
|
||||
|
||||
o.AddChannelAuthenticated(channel, values)
|
||||
}
|
||||
|
||||
// WebsocketFuturesCancel cancels a futures contract on the websocket client
|
||||
func (o *OKCoin) WebsocketFuturesCancel(symbol, contractType string, orderID int64) {
|
||||
values := make(map[string]string)
|
||||
values["symbol"] = symbol
|
||||
values["order_id"] = strconv.FormatInt(orderID, 10)
|
||||
values["contract_type"] = contractType
|
||||
o.AddChannelAuthenticated(okcoinWebsocketFuturesCancelOrder, values)
|
||||
}
|
||||
|
||||
// WebsocketSpotOrderInfo request information on an order on the websocket
|
||||
// client
|
||||
func (o *OKCoin) WebsocketSpotOrderInfo(symbol string, orderID int64) {
|
||||
values := make(map[string]string)
|
||||
values["symbol"] = symbol
|
||||
values["order_id"] = strconv.FormatInt(orderID, 10)
|
||||
|
||||
channel := okcoinWebsocketSpotUSDOrderInfo
|
||||
if o.WebsocketURL == okcoinWebsocketURLChina {
|
||||
channel = okcoinWebsocketSpotCNYOrderInfo
|
||||
}
|
||||
|
||||
o.AddChannelAuthenticated(channel, values)
|
||||
}
|
||||
|
||||
// WebsocketFuturesOrderInfo requests futures order info on the websocket client
|
||||
func (o *OKCoin) WebsocketFuturesOrderInfo(symbol, contractType string, orderID int64, orderStatus, currentPage, pageLength int) {
|
||||
values := make(map[string]string)
|
||||
values["symbol"] = symbol
|
||||
values["order_id"] = strconv.FormatInt(orderID, 10)
|
||||
values["contract_type"] = contractType
|
||||
values["status"] = strconv.Itoa(orderStatus)
|
||||
values["current_page"] = strconv.Itoa(currentPage)
|
||||
values["page_length"] = strconv.Itoa(pageLength)
|
||||
o.AddChannelAuthenticated(okcoinWebsocketFuturesOrderInfo, values)
|
||||
}
|
||||
|
||||
// ConvertToURLValues converts values to url.Values
|
||||
func (o *OKCoin) ConvertToURLValues(values map[string]string) url.Values {
|
||||
urlVals := url.Values{}
|
||||
for i, x := range values {
|
||||
urlVals.Set(i, x)
|
||||
}
|
||||
return urlVals
|
||||
}
|
||||
|
||||
// WebsocketSign signs values on the webcoket client
|
||||
func (o *OKCoin) WebsocketSign(values map[string]string) string {
|
||||
values["api_key"] = o.APIKey
|
||||
urlVals := o.ConvertToURLValues(values)
|
||||
return strings.ToUpper(common.HexEncodeToString(common.GetMD5([]byte(urlVals.Encode() + "&secret_key=" + o.APISecret))))
|
||||
}
|
||||
|
||||
// AddChannelAuthenticated adds an authenticated channel on the websocket client
|
||||
func (o *OKCoin) AddChannelAuthenticated(channel string, values map[string]string) {
|
||||
values["sign"] = o.WebsocketSign(values)
|
||||
event := WebsocketEventAuth{"addChannel", channel, values}
|
||||
json, err := common.JSONEncode(event)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
err = o.WebsocketConn.WriteMessage(websocket.TextMessage, json)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
if o.Verbose {
|
||||
log.Printf("%s Adding authenticated channel: %s\n", o.GetName(), channel)
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveChannelAuthenticated removes the added authenticated channel on the
|
||||
// websocket client
|
||||
func (o *OKCoin) RemoveChannelAuthenticated(conn *websocket.Conn, channel string, values map[string]string) {
|
||||
values["sign"] = o.WebsocketSign(values)
|
||||
event := WebsocketEventAuthRemove{"removeChannel", channel, values}
|
||||
json, err := common.JSONEncode(event)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
err = o.WebsocketConn.WriteMessage(websocket.TextMessage, json)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
if o.Verbose {
|
||||
log.Printf("%s Removing authenticated channel: %s\n", o.GetName(), channel)
|
||||
}
|
||||
}
|
||||
|
||||
// WebsocketClient starts a websocket client
|
||||
func (o *OKCoin) WebsocketClient() {
|
||||
klineValues := []string{"1min", "3min", "5min", "15min", "30min", "1hour", "2hour", "4hour", "6hour", "12hour", "day", "3day", "week"}
|
||||
var currencyChan, userinfoChan string
|
||||
|
||||
if o.WebsocketURL == okcoinWebsocketURLChina {
|
||||
currencyChan = okcoinWebsocketCNYRealTrades
|
||||
userinfoChan = okcoinWebsocketSpotCNYUserInfo
|
||||
} else {
|
||||
currencyChan = okcoinWebsocketUSDRealTrades
|
||||
userinfoChan = okcoinWebsocketSpotUSDUserInfo
|
||||
}
|
||||
|
||||
for o.Enabled && o.Websocket {
|
||||
var Dialer websocket.Dialer
|
||||
var err error
|
||||
o.WebsocketConn, _, err = Dialer.Dial(o.WebsocketURL, http.Header{})
|
||||
var dialer websocket.Dialer
|
||||
|
||||
if o.Websocket.GetProxyAddress() != "" {
|
||||
proxy, err := url.Parse(o.Websocket.GetProxyAddress())
|
||||
if err != nil {
|
||||
log.Printf("%s Unable to connect to Websocket. Error: %s\n", o.GetName(), err)
|
||||
continue
|
||||
return err
|
||||
}
|
||||
|
||||
if o.Verbose {
|
||||
log.Printf("%s Connected to Websocket.\n", o.GetName())
|
||||
dialer.Proxy = http.ProxyURL(proxy)
|
||||
}
|
||||
|
||||
var err error
|
||||
o.WebsocketConn, _, err = dialer.Dial(o.Websocket.GetWebsocketURL(),
|
||||
http.Header{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.WebsocketConn.SetPingHandler(o.PingHandler)
|
||||
|
||||
go o.WsReadData()
|
||||
go o.WsHandleData()
|
||||
|
||||
for _, p := range o.GetEnabledCurrencies() {
|
||||
fPair := exchange.FormatExchangeCurrency(o.GetName(), p)
|
||||
|
||||
o.AddChannel(fmt.Sprintf(wsSubDepthFull, fPair.String(), "20"))
|
||||
o.AddChannel(fmt.Sprintf(wsSubKline, fPair.String(), klineValues[0]))
|
||||
o.AddChannel(fmt.Sprintf(wsSubTicker, fPair.String()))
|
||||
o.AddChannel(fmt.Sprintf(wsSubTrades, fPair.String()))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WsReadData reads from the websocket connection
|
||||
func (o *OKCoin) WsReadData() {
|
||||
o.Websocket.Wg.Add(1)
|
||||
|
||||
defer func() {
|
||||
err := o.WebsocketConn.Close()
|
||||
if err != nil {
|
||||
o.Websocket.DataHandler <- fmt.Errorf("okcoin_websocket.go - Unable to to close Websocket connection. Error: %s",
|
||||
err)
|
||||
}
|
||||
o.Websocket.Wg.Done()
|
||||
}()
|
||||
|
||||
o.WebsocketConn.SetPingHandler(o.PingHandler)
|
||||
for {
|
||||
select {
|
||||
case <-o.Websocket.ShutdownC:
|
||||
return
|
||||
|
||||
if o.AuthenticatedAPISupport {
|
||||
if o.WebsocketURL == okcoinWebsocketURL {
|
||||
o.AddChannelAuthenticated(okcoinWebsocketFuturesRealTrades, map[string]string{})
|
||||
o.AddChannelAuthenticated(okcoinWebsocketFuturesUserInfo, map[string]string{})
|
||||
}
|
||||
o.AddChannelAuthenticated(currencyChan, map[string]string{})
|
||||
o.AddChannelAuthenticated(userinfoChan, map[string]string{})
|
||||
}
|
||||
|
||||
for _, x := range o.EnabledPairs {
|
||||
currency := common.StringToLower(x)
|
||||
currencyUL := currency[0:3] + "_" + currency[3:]
|
||||
if o.AuthenticatedAPISupport {
|
||||
o.WebsocketSpotOrderInfo(currencyUL, -1)
|
||||
}
|
||||
if o.WebsocketURL == okcoinWebsocketURL {
|
||||
o.AddChannel(fmt.Sprintf("ok_%s_future_index", currency))
|
||||
for _, y := range o.FuturesValues {
|
||||
if o.AuthenticatedAPISupport {
|
||||
o.WebsocketFuturesOrderInfo(currencyUL, y, -1, 1, 1, 50)
|
||||
}
|
||||
o.AddChannel(fmt.Sprintf("ok_%s_future_ticker_%s", currency, y))
|
||||
o.AddChannel(fmt.Sprintf("ok_%s_future_depth_%s_60", currency, y))
|
||||
o.AddChannel(fmt.Sprintf("ok_%s_future_trade_v1_%s", currency, y))
|
||||
for _, z := range klineValues {
|
||||
o.AddChannel(fmt.Sprintf("ok_future_%s_kline_%s_%s", currency, y, z))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
o.AddChannel(fmt.Sprintf("ok_%s_ticker", currency))
|
||||
o.AddChannel(fmt.Sprintf("ok_%s_depth60", currency))
|
||||
o.AddChannel(fmt.Sprintf("ok_%s_trades_v1", currency))
|
||||
|
||||
for _, y := range klineValues {
|
||||
o.AddChannel(fmt.Sprintf("ok_%s_kline_%s", currency, y))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for o.Enabled && o.Websocket {
|
||||
msgType, resp, err := o.WebsocketConn.ReadMessage()
|
||||
default:
|
||||
_, resp, err := o.WebsocketConn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
break
|
||||
o.Websocket.DataHandler <- err
|
||||
return
|
||||
}
|
||||
switch msgType {
|
||||
case websocket.TextMessage:
|
||||
response := []interface{}{}
|
||||
err = common.JSONDecode(resp, &response)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
o.Websocket.TrafficAlert <- struct{}{}
|
||||
o.Websocket.Intercomm <- exchange.WebsocketResponse{Raw: resp}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// WsHandleData handles stream data from the websocket connection
|
||||
func (o *OKCoin) WsHandleData() {
|
||||
o.Websocket.Wg.Add(1)
|
||||
defer o.Websocket.Wg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-o.Websocket.ShutdownC:
|
||||
return
|
||||
|
||||
case resp := <-o.Websocket.Intercomm:
|
||||
var init []WsResponse
|
||||
err := common.JSONDecode(resp.Raw, &init)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if init[0].ErrorCode != "" {
|
||||
log.Fatal(o.WebsocketErrors[init[0].ErrorCode])
|
||||
}
|
||||
|
||||
if init[0].Success {
|
||||
if init[0].Data == nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
for _, y := range response {
|
||||
z := y.(map[string]interface{})
|
||||
channel := z["channel"]
|
||||
data := z["data"]
|
||||
success := z["success"]
|
||||
errorcode := z["errorcode"]
|
||||
channelStr, ok := channel.(string)
|
||||
if init[0].Channel == "addChannel" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !ok {
|
||||
log.Println("Unable to convert channel to string")
|
||||
continue
|
||||
var currencyPairSlice []string
|
||||
splitChar := common.SplitStrings(init[0].Channel, "_")
|
||||
currencyPairSlice = append(currencyPairSlice,
|
||||
common.StringToUpper(splitChar[3]))
|
||||
currencyPairSlice = append(currencyPairSlice,
|
||||
common.StringToUpper(splitChar[4]))
|
||||
currencyPair := common.JoinStrings(currencyPairSlice, "-")
|
||||
|
||||
assetType := common.StringToUpper(splitChar[2])
|
||||
|
||||
switch {
|
||||
case common.StringContains(init[0].Channel, "ticker") &&
|
||||
common.StringContains(init[0].Channel, "spot"):
|
||||
var ticker WsTicker
|
||||
|
||||
err = common.JSONDecode(init[0].Data, &ticker)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
||||
}
|
||||
|
||||
o.Websocket.DataHandler <- exchange.TickerData{
|
||||
Timestamp: time.Unix(0, ticker.Timestamp),
|
||||
Pair: pair.NewCurrencyPairFromString(currencyPair),
|
||||
AssetType: assetType,
|
||||
Exchange: o.GetName(),
|
||||
ClosePrice: ticker.Close,
|
||||
OpenPrice: ticker.Open,
|
||||
HighPrice: ticker.Last,
|
||||
LowPrice: ticker.Low,
|
||||
Quantity: ticker.Volume,
|
||||
}
|
||||
|
||||
case common.StringContains(init[0].Channel, "depth"):
|
||||
var orderbook WsOrderbook
|
||||
|
||||
err = common.JSONDecode(init[0].Data, &orderbook)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
o.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{
|
||||
Pair: pair.NewCurrencyPairFromString(currencyPair),
|
||||
Exchange: o.GetName(),
|
||||
Asset: assetType,
|
||||
}
|
||||
|
||||
case common.StringContains(init[0].Channel, "kline"):
|
||||
var klineData [][]interface{}
|
||||
|
||||
err = common.JSONDecode(init[0].Data, &klineData)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var klines []WsKlines
|
||||
for _, data := range klineData {
|
||||
var newKline WsKlines
|
||||
|
||||
newKline.Timestamp, _ = strconv.ParseInt(data[0].(string), 10, 64)
|
||||
newKline.Open, _ = strconv.ParseFloat(data[1].(string), 64)
|
||||
newKline.High, _ = strconv.ParseFloat(data[1].(string), 64)
|
||||
newKline.Low, _ = strconv.ParseFloat(data[1].(string), 64)
|
||||
newKline.Close, _ = strconv.ParseFloat(data[1].(string), 64)
|
||||
newKline.Volume, _ = strconv.ParseFloat(data[1].(string), 64)
|
||||
|
||||
klines = append(klines, newKline)
|
||||
}
|
||||
|
||||
for _, data := range klines {
|
||||
o.Websocket.DataHandler <- exchange.KlineData{
|
||||
Timestamp: time.Unix(0, data.Timestamp),
|
||||
Pair: pair.NewCurrencyPairFromString(currencyPair),
|
||||
AssetType: assetType,
|
||||
Exchange: o.GetName(),
|
||||
OpenPrice: data.Open,
|
||||
ClosePrice: data.Close,
|
||||
HighPrice: data.High,
|
||||
LowPrice: data.Low,
|
||||
Volume: data.Volume,
|
||||
}
|
||||
}
|
||||
|
||||
if success != "true" && success != nil {
|
||||
errorCodeStr, ok := errorcode.(string)
|
||||
if !ok {
|
||||
log.Printf("%s Websocket: Unable to convert errorcode to string.\n", o.GetName())
|
||||
log.Printf("%s Websocket: channel %s error code: %s.\n", o.GetName(), channelStr, errorcode)
|
||||
} else {
|
||||
log.Printf("%s Websocket: channel %s error: %s.\n", o.GetName(), channelStr, o.WebsocketErrors[errorCodeStr])
|
||||
}
|
||||
continue
|
||||
}
|
||||
case common.StringContains(init[0].Channel, "spot") &&
|
||||
common.StringContains(init[0].Channel, "deals"):
|
||||
var dealsData [][]interface{}
|
||||
err = common.JSONDecode(init[0].Data, &dealsData)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if success == "true" {
|
||||
if data == nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
var deals []WsDeals
|
||||
for _, data := range dealsData {
|
||||
var newDeal WsDeals
|
||||
newDeal.TID, _ = strconv.ParseInt(data[0].(string), 10, 64)
|
||||
newDeal.Price, _ = strconv.ParseFloat(data[1].(string), 64)
|
||||
newDeal.Amount, _ = strconv.ParseFloat(data[2].(string), 64)
|
||||
newDeal.Timestamp, _ = data[3].(string)
|
||||
newDeal.Type, _ = data[4].(string)
|
||||
|
||||
dataJSON, err := common.JSONEncode(data)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
|
||||
switch true {
|
||||
case common.StringContains(channelStr, "ticker") && !common.StringContains(channelStr, "future"):
|
||||
tickerValues := []string{"buy", "high", "last", "low", "sell", "timestamp"}
|
||||
tickerMap := data.(map[string]interface{})
|
||||
ticker := WebsocketTicker{}
|
||||
ticker.Vol = tickerMap["vol"].(string)
|
||||
|
||||
for _, z := range tickerValues {
|
||||
result := reflect.TypeOf(tickerMap[z]).String()
|
||||
if result == "string" {
|
||||
value, errTickVals := strconv.ParseFloat(tickerMap[z].(string), 64)
|
||||
if errTickVals != nil {
|
||||
log.Println(errTickVals)
|
||||
continue
|
||||
}
|
||||
|
||||
switch z {
|
||||
case "buy":
|
||||
ticker.Buy = value
|
||||
case "high":
|
||||
ticker.High = value
|
||||
case "last":
|
||||
ticker.Last = value
|
||||
case "low":
|
||||
ticker.Low = value
|
||||
case "sell":
|
||||
ticker.Sell = value
|
||||
case "timestamp":
|
||||
ticker.Timestamp = value
|
||||
}
|
||||
|
||||
} else if result == "float64" {
|
||||
switch z {
|
||||
case "buy":
|
||||
ticker.Buy = tickerMap[z].(float64)
|
||||
case "high":
|
||||
ticker.High = tickerMap[z].(float64)
|
||||
case "last":
|
||||
ticker.Last = tickerMap[z].(float64)
|
||||
case "low":
|
||||
ticker.Low = tickerMap[z].(float64)
|
||||
case "sell":
|
||||
ticker.Sell = tickerMap[z].(float64)
|
||||
case "timestamp":
|
||||
ticker.Timestamp = tickerMap[z].(float64)
|
||||
}
|
||||
}
|
||||
}
|
||||
case common.StringContains(channelStr, "ticker") && common.StringContains(channelStr, "future"):
|
||||
ticker := WebsocketFuturesTicker{}
|
||||
err = common.JSONDecode(dataJSON, &ticker)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
case common.StringContains(channelStr, "depth"):
|
||||
orderbook := WebsocketOrderbook{}
|
||||
err = common.JSONDecode(dataJSON, &orderbook)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
case common.StringContains(channelStr, "trades_v1") || common.StringContains(channelStr, "trade_v1"):
|
||||
type TradeResponse struct {
|
||||
Data [][]string
|
||||
}
|
||||
|
||||
trades := TradeResponse{}
|
||||
err = common.JSONDecode(dataJSON, &trades.Data)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
// to-do: convert from string array to trade struct
|
||||
case common.StringContains(channelStr, "kline"):
|
||||
klines := []interface{}{}
|
||||
|
||||
err = common.JSONDecode(dataJSON, &klines)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
case common.StringContains(channelStr, "spot") && common.StringContains(channelStr, "realtrades"):
|
||||
if string(dataJSON) == "null" {
|
||||
continue
|
||||
}
|
||||
realtrades := WebsocketRealtrades{}
|
||||
|
||||
err = common.JSONDecode(dataJSON, &realtrades)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
case common.StringContains(channelStr, "future") && common.StringContains(channelStr, "realtrades"):
|
||||
if string(dataJSON) == "null" {
|
||||
continue
|
||||
}
|
||||
realtrades := WebsocketFuturesRealtrades{}
|
||||
|
||||
err = common.JSONDecode(dataJSON, &realtrades)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
case common.StringContains(channelStr, "spot") && common.StringContains(channelStr, "trade") || common.StringContains(channelStr, "futures") && common.StringContains(channelStr, "trade"):
|
||||
tradeOrder := WebsocketTradeOrderResponse{}
|
||||
|
||||
err = common.JSONDecode(dataJSON, &tradeOrder)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
case common.StringContains(channelStr, "cancel_order"):
|
||||
cancelOrder := WebsocketTradeOrderResponse{}
|
||||
|
||||
err = common.JSONDecode(dataJSON, &cancelOrder)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
case common.StringContains(channelStr, "spot") && common.StringContains(channelStr, "userinfo"):
|
||||
userinfo := WebsocketUserinfo{}
|
||||
|
||||
err = common.JSONDecode(dataJSON, &userinfo)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
case common.StringContains(channelStr, "futureusd_userinfo"):
|
||||
userinfo := WebsocketFuturesUserInfo{}
|
||||
|
||||
err = common.JSONDecode(dataJSON, &userinfo)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
case common.StringContains(channelStr, "spot") && common.StringContains(channelStr, "order_info"):
|
||||
type OrderInfoResponse struct {
|
||||
Result bool `json:"result"`
|
||||
Orders []WebsocketOrder `json:"orders"`
|
||||
}
|
||||
var orders OrderInfoResponse
|
||||
|
||||
err = common.JSONDecode(dataJSON, &orders)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
case common.StringContains(channelStr, "futureusd_order_info"):
|
||||
type OrderInfoResponse struct {
|
||||
Result bool `json:"result"`
|
||||
Orders []WebsocketFuturesOrder `json:"orders"`
|
||||
}
|
||||
var orders OrderInfoResponse
|
||||
|
||||
err = common.JSONDecode(dataJSON, &orders)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
case common.StringContains(channelStr, "future_index"):
|
||||
index := WebsocketFutureIndex{}
|
||||
|
||||
err = common.JSONDecode(dataJSON, &index)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
deals = append(deals, newDeal)
|
||||
}
|
||||
}
|
||||
}
|
||||
o.WebsocketConn.Close()
|
||||
log.Printf("%s Websocket client disconnected.", o.GetName())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -567,3 +301,54 @@ func (o *OKCoin) SetWebsocketErrorDefaults() {
|
||||
"20025": "Leverage rate error",
|
||||
}
|
||||
}
|
||||
|
||||
// WsOrderbook defines orderbook data from websocket connection
|
||||
type WsOrderbook struct {
|
||||
Asks [][]string `json:"asks"`
|
||||
Bids [][]string `json:"bids"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
// WsResponse defines initial response stream
|
||||
type WsResponse struct {
|
||||
Channel string `json:"channel"`
|
||||
Result bool `json:"result"`
|
||||
Success bool `json:"success"`
|
||||
ErrorCode string `json:"errorcode"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
// WsKlines defines a Kline response data from the websocket connection
|
||||
type WsKlines struct {
|
||||
Timestamp int64
|
||||
Open float64
|
||||
High float64
|
||||
Low float64
|
||||
Close float64
|
||||
Volume float64
|
||||
}
|
||||
|
||||
// WsTicker holds ticker data for websocket
|
||||
type WsTicker struct {
|
||||
High float64 `json:"high,string"`
|
||||
Volume float64 `json:"vol,string"`
|
||||
Last float64 `json:"last,string"`
|
||||
Low float64 `json:"low,string"`
|
||||
Buy float64 `json:"buy,string"`
|
||||
Change float64 `json:"change,string"`
|
||||
Sell float64 `json:"sell,string"`
|
||||
DayLow float64 `json:"dayLow,string"`
|
||||
Close float64 `json:"close,string"`
|
||||
DayHigh float64 `json:"dayHigh,string"`
|
||||
Open float64 `json:"open,string"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
// WsDeals defines a deal response from the websocket connection
|
||||
type WsDeals struct {
|
||||
TID int64
|
||||
Price float64
|
||||
Amount float64
|
||||
Timestamp string
|
||||
Type string
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func (o *OKCoin) Start(wg *sync.WaitGroup) {
|
||||
// Run implements the OKCoin wrapper
|
||||
func (o *OKCoin) Run() {
|
||||
if o.Verbose {
|
||||
log.Printf("%s Websocket: %s. (url: %s).\n", o.GetName(), common.IsEnabled(o.Websocket), o.WebsocketURL)
|
||||
log.Printf("%s Websocket: %s. (url: %s).\n", o.GetName(), common.IsEnabled(o.Websocket.IsEnabled()), o.WebsocketURL)
|
||||
log.Printf("%s polling delay: %ds.\n", o.GetName(), o.RESTPollingDelay)
|
||||
log.Printf("%s %d currencies enabled: %s.\n", o.GetName(), len(o.EnabledPairs), o.EnabledPairs)
|
||||
}
|
||||
@@ -56,10 +56,6 @@ func (o *OKCoin) Run() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if o.Websocket {
|
||||
go o.WebsocketClient()
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateTicker updates and returns the ticker for a currency pair
|
||||
@@ -238,3 +234,8 @@ func (o *OKCoin) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount fl
|
||||
func (o *OKCoin) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) {
|
||||
return "", errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// GetWebsocket returns a pointer to the exchange websocket
|
||||
func (o *OKCoin) GetWebsocket() (*exchange.Websocket, error) {
|
||||
return o.Websocket, nil
|
||||
}
|
||||
|
||||
@@ -101,7 +101,6 @@ func (o *OKEX) SetDefaults() {
|
||||
o.Name = "OKEX"
|
||||
o.Enabled = false
|
||||
o.Verbose = false
|
||||
o.Websocket = false
|
||||
o.RESTPollingDelay = 10
|
||||
o.RequestCurrencyPairFormat.Delimiter = "_"
|
||||
o.RequestCurrencyPairFormat.Uppercase = false
|
||||
@@ -116,6 +115,7 @@ func (o *OKEX) SetDefaults() {
|
||||
o.APIUrlDefault = apiURL
|
||||
o.APIUrl = o.APIUrlDefault
|
||||
o.AssetTypes = []string{ticker.Spot}
|
||||
o.WebsocketInit()
|
||||
}
|
||||
|
||||
// Setup method sets current configuration details if enabled
|
||||
@@ -130,7 +130,7 @@ func (o *OKEX) Setup(exch config.ExchangeConfig) {
|
||||
o.SetHTTPClientUserAgent(exch.HTTPUserAgent)
|
||||
o.RESTPollingDelay = exch.RESTPollingDelay
|
||||
o.Verbose = exch.Verbose
|
||||
o.Websocket = exch.Websocket
|
||||
o.Websocket.SetEnabled(exch.Websocket)
|
||||
o.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",")
|
||||
o.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",")
|
||||
o.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",")
|
||||
@@ -150,6 +150,18 @@ func (o *OKEX) Setup(exch config.ExchangeConfig) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = o.SetClientProxyAddress(exch.ProxyAddress)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = o.WebsocketSetup(o.WsConnect,
|
||||
exch.Name,
|
||||
exch.Websocket,
|
||||
okexDefaultWebsocketURL,
|
||||
exch.WebsocketURL)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -588,7 +600,11 @@ func (o *OKEX) PlaceContractOrders(symbol, contractType, position string, levera
|
||||
return 0, o.GetErrorCode(code)
|
||||
}
|
||||
|
||||
return contractMap["order_id"].(float64), nil
|
||||
if orderID, ok := contractMap["order_id"]; ok {
|
||||
return orderID.(float64), nil
|
||||
}
|
||||
|
||||
return 0, errors.New("orderID returned nil")
|
||||
}
|
||||
|
||||
// GetContractFuturesTradeHistory returns OKEX Contract Trade History (Not for Personal)
|
||||
|
||||
@@ -19,11 +19,13 @@ type ContractPrice struct {
|
||||
Error interface{} `json:"error_code"`
|
||||
}
|
||||
|
||||
// MultiStreamData contains raw data from okex
|
||||
type MultiStreamData struct {
|
||||
Channel string `json:"channel"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
// TickerStreamData contains ticker stream data from okex
|
||||
type TickerStreamData struct {
|
||||
Buy string `json:"buy"`
|
||||
Change string `json:"change"`
|
||||
@@ -37,9 +39,13 @@ type TickerStreamData struct {
|
||||
Vol string `json:"vol"`
|
||||
}
|
||||
|
||||
// DealsStreamData defines Deals data
|
||||
type DealsStreamData = [][]string
|
||||
|
||||
// KlineStreamData defines kline data
|
||||
type KlineStreamData = [][]string
|
||||
|
||||
// DepthStreamData defines orderbook depth
|
||||
type DepthStreamData struct {
|
||||
Asks [][]string `json:"asks"`
|
||||
Bids [][]string `json:"bids"`
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
package okex
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/thrasher-/gocryptotrader/common"
|
||||
"github.com/thrasher-/gocryptotrader/currency/pair"
|
||||
"github.com/thrasher-/gocryptotrader/exchanges"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -22,129 +27,282 @@ func (o *OKEX) writeToWebsocket(message string) error {
|
||||
return o.WebsocketConn.WriteMessage(websocket.TextMessage, []byte(message))
|
||||
}
|
||||
|
||||
func (o *OKEX) websocketConnect() {
|
||||
var Dialer websocket.Dialer
|
||||
// WsConnect initiates a websocket connection
|
||||
func (o *OKEX) WsConnect() error {
|
||||
if !o.Websocket.IsEnabled() || !o.IsEnabled() {
|
||||
return errors.New(exchange.WebsocketNotEnabled)
|
||||
}
|
||||
|
||||
var dialer websocket.Dialer
|
||||
|
||||
if o.Websocket.GetProxyAddress() != "" {
|
||||
proxy, err := url.Parse(o.Websocket.GetProxyAddress())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dialer.Proxy = http.ProxyURL(proxy)
|
||||
}
|
||||
|
||||
var err error
|
||||
o.WebsocketConn, _, err = dialer.Dial(o.Websocket.GetWebsocketURL(),
|
||||
http.Header{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s Unable to connect to Websocket. Error: %s",
|
||||
o.Name,
|
||||
err)
|
||||
}
|
||||
|
||||
go o.WsHandleData()
|
||||
go o.WsReadData()
|
||||
go o.wsPingHandler()
|
||||
|
||||
err = o.WsSubscribe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error: Could not subscribe to the OKEX websocket %s",
|
||||
err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WsSubscribe subscribes to the websocket channels
|
||||
func (o *OKEX) WsSubscribe() error {
|
||||
myEnabledSubscriptionChannels := []string{}
|
||||
|
||||
for _, pair := range o.EnabledPairs {
|
||||
myEnabledSubscriptionChannels = append(myEnabledSubscriptionChannels, fmt.Sprintf("{'event':'addChannel','channel':'ok_sub_spot_%s_ticker'}", pair))
|
||||
myEnabledSubscriptionChannels = append(myEnabledSubscriptionChannels, fmt.Sprintf("{'event':'addChannel','channel':'ok_sub_spot_%s_depth'}", pair))
|
||||
myEnabledSubscriptionChannels = append(myEnabledSubscriptionChannels, fmt.Sprintf("{'event':'addChannel','channel':'ok_sub_spot_%s_deals'}", pair))
|
||||
myEnabledSubscriptionChannels = append(myEnabledSubscriptionChannels, fmt.Sprintf("{'event':'addChannel','channel':'ok_sub_spot_%s_kline_1min'}", pair))
|
||||
|
||||
// ----------- deprecate when usd pairs are upgraded to usdt ----------
|
||||
checkSymbol := common.SplitStrings(pair, "_")
|
||||
for i := range checkSymbol {
|
||||
if common.StringContains(checkSymbol[i], "usdt") {
|
||||
break
|
||||
}
|
||||
if common.StringContains(checkSymbol[i], "usd") {
|
||||
checkSymbol[i] = "usdt"
|
||||
}
|
||||
}
|
||||
|
||||
symbolRedone := common.JoinStrings(checkSymbol, "_")
|
||||
// ----------- deprecate when usd pairs are upgraded to usdt ----------
|
||||
|
||||
myEnabledSubscriptionChannels = append(myEnabledSubscriptionChannels,
|
||||
fmt.Sprintf("{'event':'addChannel','channel':'ok_sub_spot_%s_ticker'}",
|
||||
symbolRedone))
|
||||
|
||||
myEnabledSubscriptionChannels = append(myEnabledSubscriptionChannels,
|
||||
fmt.Sprintf("{'event':'addChannel','channel':'ok_sub_spot_%s_depth'}",
|
||||
symbolRedone))
|
||||
|
||||
myEnabledSubscriptionChannels = append(myEnabledSubscriptionChannels,
|
||||
fmt.Sprintf("{'event':'addChannel','channel':'ok_sub_spot_%s_deals'}",
|
||||
symbolRedone))
|
||||
|
||||
myEnabledSubscriptionChannels = append(myEnabledSubscriptionChannels,
|
||||
fmt.Sprintf("{'event':'addChannel','channel':'ok_sub_spot_%s_kline_1min'}",
|
||||
symbolRedone))
|
||||
}
|
||||
|
||||
mySubscriptionString := "[" + strings.Join(myEnabledSubscriptionChannels, ",") + "]"
|
||||
|
||||
o.WebsocketConn, _, err = Dialer.Dial(okexDefaultWebsocketURL, http.Header{})
|
||||
|
||||
if err != nil {
|
||||
log.Printf("%s Unable to connect to Websocket. Error: %s\n", o.Name, err)
|
||||
return
|
||||
for _, outgoing := range myEnabledSubscriptionChannels {
|
||||
err := o.writeToWebsocket(outgoing)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if o.Verbose {
|
||||
log.Printf("%s Connected to Websocket.\n", o.Name)
|
||||
log.Printf("Subscription String is %s\n", mySubscriptionString)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("Subscription String is %s\n", mySubscriptionString)
|
||||
// WsReadData reads data from the websocket connection
|
||||
func (o *OKEX) WsReadData() {
|
||||
o.Websocket.Wg.Add(1)
|
||||
|
||||
// subscribe to all the desired subscriptions
|
||||
err = o.writeToWebsocket(mySubscriptionString)
|
||||
defer func() {
|
||||
err := o.WebsocketConn.Close()
|
||||
if err != nil {
|
||||
o.Websocket.DataHandler <- fmt.Errorf("okex_websocket.go - Unable to to close Websocket connection. Error: %s",
|
||||
err)
|
||||
}
|
||||
o.Websocket.Wg.Done()
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Error: Could not subscribe to the OKEX websocket %s", err)
|
||||
return
|
||||
for {
|
||||
select {
|
||||
case <-o.Websocket.ShutdownC:
|
||||
return
|
||||
|
||||
default:
|
||||
_, resp, err := o.WebsocketConn.ReadMessage()
|
||||
if err != nil {
|
||||
o.Websocket.DataHandler <- err
|
||||
return
|
||||
}
|
||||
o.Websocket.TrafficAlert <- struct{}{}
|
||||
o.Websocket.Intercomm <- exchange.WebsocketResponse{Raw: resp}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WebsocketClient the main function handling the OKEX websocket
|
||||
// Documentation URL: https://github.com/okcoin-okex/API-docs-OKEx.com/blob/master/API-For-Spot-EN/WEBSOCKET%20API%20for%20SPOT.md
|
||||
func (o *OKEX) WebsocketClient() {
|
||||
for o.Enabled && o.Websocket {
|
||||
o.websocketConnect()
|
||||
func (o *OKEX) wsPingHandler() {
|
||||
o.Websocket.Wg.Add(1)
|
||||
defer o.Websocket.Wg.Done()
|
||||
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(time.Second * 27)
|
||||
o.writeToWebsocket("{'event':'ping'}")
|
||||
log.Printf("%s sent Ping message\n", o.GetName())
|
||||
}
|
||||
}()
|
||||
ticker := time.NewTicker(time.Second * 27)
|
||||
|
||||
for o.Enabled && o.Websocket {
|
||||
msgType, resp, err := o.WebsocketConn.ReadMessage()
|
||||
for {
|
||||
select {
|
||||
case <-o.Websocket.ShutdownC:
|
||||
return
|
||||
|
||||
case <-ticker.C:
|
||||
err := o.writeToWebsocket("{'event':'ping'}")
|
||||
if err != nil {
|
||||
log.Printf("Error: Could not read from the OKEX websocket %s", err)
|
||||
o.websocketConnect()
|
||||
continue
|
||||
o.Websocket.DataHandler <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WsHandleData handles the read data from the websocket connection
|
||||
func (o *OKEX) WsHandleData() {
|
||||
o.Websocket.Wg.Add(1)
|
||||
defer o.Websocket.Wg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-o.Websocket.ShutdownC:
|
||||
return
|
||||
|
||||
case resp := <-o.Websocket.Intercomm:
|
||||
multiStreamDataArr := []MultiStreamData{}
|
||||
|
||||
err := common.JSONDecode(resp.Raw, &multiStreamDataArr)
|
||||
if err != nil {
|
||||
if strings.Contains(string(resp.Raw), "pong") {
|
||||
continue
|
||||
} else {
|
||||
log.Fatal("okex.go error -", err)
|
||||
}
|
||||
}
|
||||
|
||||
switch msgType {
|
||||
case websocket.TextMessage:
|
||||
multiStreamDataArr := []MultiStreamData{}
|
||||
|
||||
err = common.JSONDecode(resp, &multiStreamDataArr)
|
||||
|
||||
if err != nil {
|
||||
if strings.Contains(string(resp), "pong") {
|
||||
log.Printf("%s received Pong message\n", o.GetName())
|
||||
} else {
|
||||
log.Printf("%s some other error happened: %s", o.GetName(), err)
|
||||
continue
|
||||
for _, multiStreamData := range multiStreamDataArr {
|
||||
var errResponse ErrorResponse
|
||||
if common.StringContains(string(resp.Raw), "error_msg") {
|
||||
err = common.JSONDecode(resp.Raw, &errResponse)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
o.Websocket.DataHandler <- fmt.Errorf("okex.go error - %s resp: %s ",
|
||||
errResponse.ErrorMsg,
|
||||
string(resp.Raw))
|
||||
continue
|
||||
}
|
||||
|
||||
for _, multiStreamData := range multiStreamDataArr {
|
||||
if strings.Contains(multiStreamData.Channel, "ticker") {
|
||||
// ticker data
|
||||
ticker := TickerStreamData{}
|
||||
tickerDecodeError := common.JSONDecode(multiStreamData.Data, &ticker)
|
||||
var newPair string
|
||||
var assetType string
|
||||
currencyPairSlice := common.SplitStrings(multiStreamData.Channel, "_")
|
||||
if len(currencyPairSlice) > 5 {
|
||||
newPair = currencyPairSlice[3] + "_" + currencyPairSlice[4]
|
||||
assetType = currencyPairSlice[2]
|
||||
}
|
||||
|
||||
if tickerDecodeError != nil {
|
||||
log.Printf("OKEX Ticker Decode Error: %s", tickerDecodeError)
|
||||
continue
|
||||
if strings.Contains(multiStreamData.Channel, "ticker") {
|
||||
var ticker TickerStreamData
|
||||
|
||||
err = common.JSONDecode(multiStreamData.Data, &ticker)
|
||||
if err != nil {
|
||||
log.Fatal("OKEX Ticker Decode Error:", err)
|
||||
}
|
||||
|
||||
o.Websocket.DataHandler <- exchange.TickerData{
|
||||
Timestamp: time.Unix(0, int64(ticker.Timestamp)),
|
||||
Exchange: o.GetName(),
|
||||
AssetType: assetType,
|
||||
}
|
||||
|
||||
} else if strings.Contains(multiStreamData.Channel, "deals") {
|
||||
var deals DealsStreamData
|
||||
|
||||
err = common.JSONDecode(multiStreamData.Data, &deals)
|
||||
if err != nil {
|
||||
log.Fatal("OKEX Deals Decode Error:", err)
|
||||
}
|
||||
|
||||
for _, trade := range deals {
|
||||
price, _ := strconv.ParseFloat(trade[1], 64)
|
||||
amount, _ := strconv.ParseFloat(trade[2], 64)
|
||||
time, _ := time.Parse(time.RFC3339, trade[3])
|
||||
|
||||
o.Websocket.DataHandler <- exchange.TradeData{
|
||||
Timestamp: time,
|
||||
Exchange: o.GetName(),
|
||||
AssetType: assetType,
|
||||
CurrencyPair: pair.NewCurrencyPairFromString(newPair),
|
||||
Price: price,
|
||||
Amount: amount,
|
||||
EventType: trade[4],
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("OKEX Channel: %s\tData: %s\n", multiStreamData.Channel, multiStreamData.Data)
|
||||
} else if strings.Contains(multiStreamData.Channel, "deals") {
|
||||
// orderbook data
|
||||
deals := DealsStreamData{}
|
||||
decodeError := common.JSONDecode(multiStreamData.Data, &deals)
|
||||
} else if strings.Contains(multiStreamData.Channel, "kline") {
|
||||
var klines KlineStreamData
|
||||
|
||||
if decodeError != nil {
|
||||
log.Printf("OKEX Deals Decode Error: %s", decodeError)
|
||||
continue
|
||||
err := common.JSONDecode(multiStreamData.Data, &klines)
|
||||
if err != nil {
|
||||
log.Fatal("OKEX Klines Decode Error:", err)
|
||||
}
|
||||
|
||||
for _, kline := range klines {
|
||||
ntime, _ := strconv.ParseInt(kline[0], 10, 64)
|
||||
open, _ := strconv.ParseFloat(kline[1], 64)
|
||||
high, _ := strconv.ParseFloat(kline[2], 64)
|
||||
low, _ := strconv.ParseFloat(kline[3], 64)
|
||||
close, _ := strconv.ParseFloat(kline[4], 64)
|
||||
volume, _ := strconv.ParseFloat(kline[5], 64)
|
||||
|
||||
o.Websocket.DataHandler <- exchange.KlineData{
|
||||
Timestamp: time.Unix(ntime, 0),
|
||||
Pair: pair.NewCurrencyPairFromString(newPair),
|
||||
AssetType: assetType,
|
||||
Exchange: o.GetName(),
|
||||
OpenPrice: open,
|
||||
HighPrice: high,
|
||||
LowPrice: low,
|
||||
ClosePrice: close,
|
||||
Volume: volume,
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("OKEX Channel: %s\tData: %s\n", multiStreamData.Channel, multiStreamData.Data)
|
||||
} else if strings.Contains(multiStreamData.Channel, "kline") {
|
||||
// 1 min kline data
|
||||
klines := KlineStreamData{}
|
||||
decodeError := common.JSONDecode(multiStreamData.Data, &klines)
|
||||
} else if strings.Contains(multiStreamData.Channel, "depth") {
|
||||
var depth DepthStreamData
|
||||
|
||||
if decodeError != nil {
|
||||
log.Printf("OKEX Klines Decode Error: %s", decodeError)
|
||||
continue
|
||||
}
|
||||
err := common.JSONDecode(multiStreamData.Data, &depth)
|
||||
if err != nil {
|
||||
log.Fatal("OKEX Depth Decode Error:", err)
|
||||
}
|
||||
|
||||
log.Printf("OKEX Channel: %s\tData: %s\n", multiStreamData.Channel, multiStreamData.Data)
|
||||
} else if strings.Contains(multiStreamData.Channel, "depth") {
|
||||
// market depth data
|
||||
depth := DepthStreamData{}
|
||||
decodeError := common.JSONDecode(multiStreamData.Data, &depth)
|
||||
|
||||
if decodeError != nil {
|
||||
log.Printf("OKEX Depth Decode Error: %s", decodeError)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("OKEX Channel: %s\tData: %s\n", multiStreamData.Channel, multiStreamData.Data)
|
||||
o.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{
|
||||
Exchange: o.GetName(),
|
||||
Asset: assetType,
|
||||
Pair: pair.NewCurrencyPairFromString(newPair),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorResponse defines an error response type from the websocket connection
|
||||
type ErrorResponse struct {
|
||||
Result bool `json:"result"`
|
||||
ErrorMsg string `json:"error_msg"`
|
||||
ErrorCode int64 `json:"error_code"`
|
||||
}
|
||||
|
||||
// Request defines the JSON request structure to the websocket server
|
||||
type Request struct {
|
||||
Event string `json:"event"`
|
||||
Channel string `json:"channel"`
|
||||
Parameters string `json:"parameters,omitempty"`
|
||||
}
|
||||
|
||||
@@ -24,14 +24,10 @@ func (o *OKEX) Start(wg *sync.WaitGroup) {
|
||||
// Run implements the OKEX wrapper
|
||||
func (o *OKEX) Run() {
|
||||
if o.Verbose {
|
||||
log.Printf("%s Websocket: %s. (url: %s).\n", o.GetName(), common.IsEnabled(o.Websocket), o.WebsocketURL)
|
||||
log.Printf("%s Websocket: %s. (url: %s).\n", o.GetName(), common.IsEnabled(o.Websocket.IsEnabled()), o.WebsocketURL)
|
||||
log.Printf("%s polling delay: %ds.\n", o.GetName(), o.RESTPollingDelay)
|
||||
log.Printf("%s %d currencies enabled: %s.\n", o.GetName(), len(o.EnabledPairs), o.EnabledPairs)
|
||||
}
|
||||
|
||||
if o.Websocket {
|
||||
go o.WebsocketClient()
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateTicker updates and returns the ticker for a currency pair
|
||||
@@ -204,3 +200,8 @@ func (o *OKEX) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount floa
|
||||
func (o *OKEX) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) {
|
||||
return "", errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// GetWebsocket returns a pointer to the exchange websocket
|
||||
func (o *OKEX) GetWebsocket() (*exchange.Websocket, error) {
|
||||
return o.Websocket, nil
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ var (
|
||||
type Item struct {
|
||||
Amount float64
|
||||
Price float64
|
||||
ID int64
|
||||
}
|
||||
|
||||
// Base holds the fields for the orderbook base
|
||||
@@ -36,6 +37,7 @@ type Base struct {
|
||||
Bids []Item `json:"bids"`
|
||||
Asks []Item `json:"asks"`
|
||||
LastUpdated time.Time `json:"last_updated"`
|
||||
AssetType string
|
||||
}
|
||||
|
||||
// Orderbook holds the orderbook information for a currency pair and type
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/thrasher-/gocryptotrader/common"
|
||||
"github.com/thrasher-/gocryptotrader/config"
|
||||
"github.com/thrasher-/gocryptotrader/exchanges"
|
||||
@@ -55,6 +56,7 @@ const (
|
||||
// Poloniex is the overarching type across the poloniex package
|
||||
type Poloniex struct {
|
||||
exchange.Base
|
||||
WebsocketConn *websocket.Conn
|
||||
}
|
||||
|
||||
// SetDefaults sets default settings for poloniex
|
||||
@@ -63,7 +65,6 @@ func (p *Poloniex) SetDefaults() {
|
||||
p.Enabled = false
|
||||
p.Fee = 0
|
||||
p.Verbose = false
|
||||
p.Websocket = false
|
||||
p.RESTPollingDelay = 10
|
||||
p.RequestCurrencyPairFormat.Delimiter = "_"
|
||||
p.RequestCurrencyPairFormat.Uppercase = true
|
||||
@@ -78,6 +79,7 @@ func (p *Poloniex) SetDefaults() {
|
||||
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
|
||||
p.APIUrlDefault = poloniexAPIURL
|
||||
p.APIUrl = p.APIUrlDefault
|
||||
p.WebsocketInit()
|
||||
}
|
||||
|
||||
// Setup sets user exchange configuration settings
|
||||
@@ -92,7 +94,7 @@ func (p *Poloniex) Setup(exch config.ExchangeConfig) {
|
||||
p.SetHTTPClientUserAgent(exch.HTTPUserAgent)
|
||||
p.RESTPollingDelay = exch.RESTPollingDelay
|
||||
p.Verbose = exch.Verbose
|
||||
p.Websocket = exch.Websocket
|
||||
p.Websocket.SetEnabled(exch.Websocket)
|
||||
p.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",")
|
||||
p.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",")
|
||||
p.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",")
|
||||
@@ -112,6 +114,18 @@ func (p *Poloniex) Setup(exch config.ExchangeConfig) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = p.SetClientProxyAddress(exch.ProxyAddress)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = p.WebsocketSetup(p.WsConnect,
|
||||
exch.Name,
|
||||
exch.Websocket,
|
||||
poloniexWebsocketAddress,
|
||||
exch.WebsocketURL)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -301,3 +301,36 @@ type WebsocketTrollboxMessage struct {
|
||||
Message string
|
||||
Reputation float64
|
||||
}
|
||||
|
||||
// WsCommand defines the request params after a websocket connection has been
|
||||
// established
|
||||
type WsCommand struct {
|
||||
Command string `json:"command"`
|
||||
Channel interface{} `json:"channel"`
|
||||
APIKey string `json:"key,omitempty"`
|
||||
Payload string `json:"payload,omitempty"`
|
||||
Sign string `json:"sign,omitempty"`
|
||||
}
|
||||
|
||||
// WsTicker defines the websocket ticker response
|
||||
type WsTicker struct {
|
||||
LastPrice float64
|
||||
LowestAsk float64
|
||||
HighestBid float64
|
||||
PercentageChange float64
|
||||
BaseCurrencyVolume24H float64
|
||||
QuoteCurrencyVolume24H float64
|
||||
IsFrozen bool
|
||||
HighestTradeIn24H float64
|
||||
LowestTradePrice24H float64
|
||||
}
|
||||
|
||||
// WsTrade defines the websocket trade response
|
||||
type WsTrade struct {
|
||||
Symbol string
|
||||
TradeID int64
|
||||
Side string
|
||||
Volume float64
|
||||
Price float64
|
||||
Timestamp int64
|
||||
}
|
||||
|
||||
@@ -1,166 +1,764 @@
|
||||
package poloniex
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/beatgammit/turnpike"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/thrasher-/gocryptotrader/common"
|
||||
"github.com/thrasher-/gocryptotrader/currency/pair"
|
||||
"github.com/thrasher-/gocryptotrader/exchanges"
|
||||
"github.com/thrasher-/gocryptotrader/exchanges/orderbook"
|
||||
)
|
||||
|
||||
const (
|
||||
poloniexWebsocketAddress = "wss://api.poloniex.com"
|
||||
poloniexWebsocketRealm = "realm1"
|
||||
poloniexWebsocketTicker = "ticker"
|
||||
poloniexWebsocketTrollbox = "trollbox"
|
||||
poloniexWebsocketAddress = "wss://api2.poloniex.com"
|
||||
wsAccountNotificationID = 1000
|
||||
wsTickerDataID = 1002
|
||||
ws24HourExchangeVolumeID = 1003
|
||||
wsHeartbeat = 1010
|
||||
)
|
||||
|
||||
// OnTicker converts ticker data to a websocketTicker
|
||||
func OnTicker(args []interface{}, kwargs map[string]interface{}) {
|
||||
ticker := WebsocketTicker{}
|
||||
ticker.CurrencyPair = args[0].(string)
|
||||
ticker.Last, _ = strconv.ParseFloat(args[1].(string), 64)
|
||||
ticker.LowestAsk, _ = strconv.ParseFloat(args[2].(string), 64)
|
||||
ticker.HighestBid, _ = strconv.ParseFloat(args[3].(string), 64)
|
||||
ticker.PercentChange, _ = strconv.ParseFloat(args[4].(string), 64)
|
||||
ticker.BaseVolume, _ = strconv.ParseFloat(args[5].(string), 64)
|
||||
ticker.QuoteVolume, _ = strconv.ParseFloat(args[6].(string), 64)
|
||||
|
||||
if args[7].(float64) != 0 {
|
||||
ticker.IsFrozen = true
|
||||
} else {
|
||||
ticker.IsFrozen = false
|
||||
// WsConnect initiates a websocket connection
|
||||
func (p *Poloniex) WsConnect() error {
|
||||
if !p.Websocket.IsEnabled() || !p.IsEnabled() {
|
||||
return errors.New(exchange.WebsocketNotEnabled)
|
||||
}
|
||||
|
||||
ticker.High, _ = strconv.ParseFloat(args[8].(string), 64)
|
||||
ticker.Low, _ = strconv.ParseFloat(args[9].(string), 64)
|
||||
}
|
||||
|
||||
// OnTrollbox handles trollbox messages
|
||||
func OnTrollbox(args []interface{}, kwargs map[string]interface{}) {
|
||||
message := WebsocketTrollboxMessage{}
|
||||
message.MessageNumber, _ = args[1].(float64)
|
||||
message.Username = args[2].(string)
|
||||
message.Message = args[3].(string)
|
||||
if len(args) == 5 {
|
||||
message.Reputation = args[4].(float64)
|
||||
}
|
||||
}
|
||||
|
||||
// OnDepthOrTrade handles orderbook depth and trade events
|
||||
func OnDepthOrTrade(args []interface{}, kwargs map[string]interface{}) {
|
||||
for x := range args {
|
||||
data := args[x].(map[string]interface{})
|
||||
msgData := data["data"].(map[string]interface{})
|
||||
msgType := data["type"].(string)
|
||||
|
||||
switch msgType {
|
||||
case "orderBookModify":
|
||||
{
|
||||
type PoloniexWebsocketOrderbookModify struct {
|
||||
Type string
|
||||
Rate float64
|
||||
Amount float64
|
||||
}
|
||||
|
||||
orderModify := PoloniexWebsocketOrderbookModify{}
|
||||
orderModify.Type = msgData["type"].(string)
|
||||
|
||||
rateStr := msgData["rate"].(string)
|
||||
orderModify.Rate, _ = strconv.ParseFloat(rateStr, 64)
|
||||
|
||||
amountStr := msgData["amount"].(string)
|
||||
orderModify.Amount, _ = strconv.ParseFloat(amountStr, 64)
|
||||
}
|
||||
case "orderBookRemove":
|
||||
{
|
||||
type PoloniexWebsocketOrderbookRemove struct {
|
||||
Type string
|
||||
Rate float64
|
||||
}
|
||||
|
||||
orderRemoval := PoloniexWebsocketOrderbookRemove{}
|
||||
orderRemoval.Type = msgData["type"].(string)
|
||||
|
||||
rateStr := msgData["rate"].(string)
|
||||
orderRemoval.Rate, _ = strconv.ParseFloat(rateStr, 64)
|
||||
}
|
||||
case "newTrade":
|
||||
{
|
||||
type PoloniexWebsocketNewTrade struct {
|
||||
Type string
|
||||
TradeID int64
|
||||
Rate float64
|
||||
Amount float64
|
||||
Date string
|
||||
Total float64
|
||||
}
|
||||
|
||||
trade := PoloniexWebsocketNewTrade{}
|
||||
trade.Type = msgData["type"].(string)
|
||||
|
||||
tradeIDstr := msgData["tradeID"].(string)
|
||||
trade.TradeID, _ = strconv.ParseInt(tradeIDstr, 10, 64)
|
||||
|
||||
rateStr := msgData["rate"].(string)
|
||||
trade.Rate, _ = strconv.ParseFloat(rateStr, 64)
|
||||
|
||||
amountStr := msgData["amount"].(string)
|
||||
trade.Amount, _ = strconv.ParseFloat(amountStr, 64)
|
||||
|
||||
totalStr := msgData["total"].(string)
|
||||
trade.Rate, _ = strconv.ParseFloat(totalStr, 64)
|
||||
|
||||
trade.Date = msgData["date"].(string)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WebsocketClient creates a new websocket client
|
||||
func (p *Poloniex) WebsocketClient() {
|
||||
for p.Enabled && p.Websocket {
|
||||
c, err := turnpike.NewWebsocketClient(turnpike.JSON, poloniexWebsocketAddress, nil)
|
||||
var dialer websocket.Dialer
|
||||
if p.Websocket.GetProxyAddress() != "" {
|
||||
proxy, err := url.Parse(p.Websocket.GetProxyAddress())
|
||||
if err != nil {
|
||||
log.Printf("%s Unable to connect to Websocket. Error: %s\n", p.GetName(), err)
|
||||
continue
|
||||
return err
|
||||
}
|
||||
|
||||
if p.Verbose {
|
||||
log.Printf("%s Connected to Websocket.\n", p.GetName())
|
||||
}
|
||||
dialer.Proxy = http.ProxyURL(proxy)
|
||||
}
|
||||
|
||||
_, err = c.JoinRealm(poloniexWebsocketRealm, nil)
|
||||
var err error
|
||||
p.WebsocketConn, _, err = dialer.Dial(p.Websocket.GetWebsocketURL(),
|
||||
http.Header{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go p.WsReadData()
|
||||
go p.WsHandleData()
|
||||
|
||||
return p.WsSubscribe()
|
||||
}
|
||||
|
||||
// WsSubscribe subscribes to the websocket feeds
|
||||
func (p *Poloniex) WsSubscribe() error {
|
||||
tickerJSON, err := common.JSONEncode(WsCommand{
|
||||
Command: "subscribe",
|
||||
Channel: wsTickerDataID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = p.WebsocketConn.WriteMessage(websocket.TextMessage, tickerJSON)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pairs := p.GetEnabledCurrencies()
|
||||
for _, nextPair := range pairs {
|
||||
fPair := exchange.FormatExchangeCurrency(p.GetName(), nextPair)
|
||||
|
||||
orderbookJSON, err := common.JSONEncode(WsCommand{
|
||||
Command: "subscribe",
|
||||
Channel: fPair.String(),
|
||||
})
|
||||
|
||||
err = p.WebsocketConn.WriteMessage(websocket.TextMessage, orderbookJSON)
|
||||
if err != nil {
|
||||
log.Printf("%s Unable to join realm. Error: %s\n", p.GetName(), err)
|
||||
continue
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if p.Verbose {
|
||||
log.Printf("%s Joined Websocket realm.\n", p.GetName())
|
||||
// WsReadData reads data from the websocket connection
|
||||
func (p *Poloniex) WsReadData() {
|
||||
p.Websocket.Wg.Add(1)
|
||||
|
||||
defer func() {
|
||||
err := p.WebsocketConn.Close()
|
||||
if err != nil {
|
||||
p.Websocket.DataHandler <- fmt.Errorf("poloniex_websocket.go - Unable to to close Websocket connection. Error: %s",
|
||||
err)
|
||||
}
|
||||
p.Websocket.Wg.Done()
|
||||
}()
|
||||
|
||||
c.ReceiveDone = make(chan bool)
|
||||
for {
|
||||
select {
|
||||
case <-p.Websocket.ShutdownC:
|
||||
return
|
||||
|
||||
if err := c.Subscribe(poloniexWebsocketTicker, OnTicker); err != nil {
|
||||
log.Printf("%s Error subscribing to ticker channel: %s\n", p.GetName(), err)
|
||||
}
|
||||
|
||||
if err := c.Subscribe(poloniexWebsocketTrollbox, OnTrollbox); err != nil {
|
||||
log.Printf("%s Error subscribing to trollbox channel: %s\n", p.GetName(), err)
|
||||
}
|
||||
|
||||
for x := range p.EnabledPairs {
|
||||
currency := p.EnabledPairs[x]
|
||||
if err := c.Subscribe(currency, OnDepthOrTrade); err != nil {
|
||||
log.Printf("%s Error subscribing to %s channel: %s\n", p.GetName(), currency, err)
|
||||
default:
|
||||
_, resp, err := p.WebsocketConn.ReadMessage()
|
||||
if err != nil {
|
||||
p.Websocket.DataHandler <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if p.Verbose {
|
||||
log.Printf("%s Subscribed to websocket channels.\n", p.GetName())
|
||||
p.Websocket.TrafficAlert <- struct{}{}
|
||||
p.Websocket.Intercomm <- exchange.WebsocketResponse{Raw: resp}
|
||||
}
|
||||
|
||||
<-c.ReceiveDone
|
||||
log.Printf("%s Websocket client disconnected.\n", p.GetName())
|
||||
}
|
||||
}
|
||||
|
||||
// WsHandleData handles data from the websocket connection
|
||||
func (p *Poloniex) WsHandleData() {
|
||||
p.Websocket.Wg.Add(1)
|
||||
defer p.Websocket.Wg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-p.Websocket.ShutdownC:
|
||||
return
|
||||
|
||||
case resp := <-p.Websocket.Intercomm:
|
||||
var check []interface{}
|
||||
err := common.JSONDecode(resp.Raw, &check)
|
||||
if err != nil {
|
||||
log.Fatal("poloniex_websocket.go - ", err)
|
||||
}
|
||||
|
||||
switch len(check) {
|
||||
case 1:
|
||||
if check[0].(float64) == wsHeartbeat {
|
||||
continue
|
||||
}
|
||||
|
||||
case 2:
|
||||
switch check[0].(type) {
|
||||
case float64:
|
||||
subscriptionID := check[0].(float64)
|
||||
if subscriptionID == ws24HourExchangeVolumeID ||
|
||||
subscriptionID == wsAccountNotificationID ||
|
||||
subscriptionID == wsTickerDataID {
|
||||
if check[1].(float64) != 1 {
|
||||
p.Websocket.DataHandler <- errors.New("poloniex.go error - Subcription failed")
|
||||
continue
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
case string:
|
||||
orderbookSubscriptionID := check[0].(string)
|
||||
if check[1].(float64) != 1 {
|
||||
p.Websocket.DataHandler <- fmt.Errorf("poloniex.go error - orderbook subscription failed with symbol %s",
|
||||
orderbookSubscriptionID)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
case 3:
|
||||
switch len(check[2].([]interface{})) {
|
||||
case 1:
|
||||
// Snapshot
|
||||
datalevel1 := check[2].([]interface{})
|
||||
datalevel2 := datalevel1[0].([]interface{})
|
||||
|
||||
switch datalevel2[1].(type) {
|
||||
case float64:
|
||||
err := p.WsProcessOrderbookUpdate(datalevel2,
|
||||
CurrencyPairID[int64(check[0].(float64))])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
p.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{
|
||||
Exchange: p.GetName(),
|
||||
Asset: "SPOT",
|
||||
// Pair: pair.NewCurrencyPairFromString(currencyPair),
|
||||
}
|
||||
|
||||
case map[string]interface{}:
|
||||
datalevel3 := datalevel2[1].(map[string]interface{})
|
||||
currencyPair, ok := datalevel3["currencyPair"].(string)
|
||||
if !ok {
|
||||
log.Fatal("poloniex.go error - could not find currency pair in map")
|
||||
}
|
||||
|
||||
orderbookData, ok := datalevel3["orderBook"].([]interface{})
|
||||
if !ok {
|
||||
log.Fatal("poloniex.go error - could not find orderbook data in map")
|
||||
}
|
||||
|
||||
err := p.WsProcessOrderbookSnapshot(orderbookData, currencyPair)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
p.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{
|
||||
Exchange: p.GetName(),
|
||||
Asset: "SPOT",
|
||||
Pair: pair.NewCurrencyPairFromString(currencyPair),
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
case 10:
|
||||
tickerData := check[2].([]interface{})
|
||||
var ticker WsTicker
|
||||
|
||||
ticker.LastPrice, _ = tickerData[0].(float64)
|
||||
// ticker.LowestAsk, _ = strconv.ParseFloat(tickerData[1].(string), 64)
|
||||
ticker.HighestBid, _ = strconv.ParseFloat(tickerData[2].(string), 64)
|
||||
ticker.PercentageChange, _ = strconv.ParseFloat(tickerData[3].(string), 64)
|
||||
ticker.BaseCurrencyVolume24H, _ = strconv.ParseFloat(tickerData[4].(string), 64)
|
||||
ticker.QuoteCurrencyVolume24H, _ = strconv.ParseFloat(tickerData[5].(string), 64)
|
||||
frozen, _ := strconv.ParseInt(tickerData[6].(string), 10, 64)
|
||||
if frozen == 1 {
|
||||
ticker.IsFrozen = true
|
||||
}
|
||||
ticker.HighestTradeIn24H, _ = tickerData[7].(float64)
|
||||
ticker.LowestTradePrice24H, _ = strconv.ParseFloat(tickerData[8].(string), 64)
|
||||
|
||||
p.Websocket.DataHandler <- exchange.TickerData{
|
||||
Timestamp: time.Now(),
|
||||
Exchange: p.GetName(),
|
||||
AssetType: "SPOT",
|
||||
LowPrice: ticker.LowestAsk,
|
||||
HighPrice: ticker.HighestBid,
|
||||
}
|
||||
|
||||
default:
|
||||
for _, element := range check[2].([]interface{}) {
|
||||
switch element.(type) {
|
||||
case []interface{}:
|
||||
data := element.([]interface{})
|
||||
if data[0].(string) == "o" {
|
||||
p.WsProcessOrderbookUpdate(data, CurrencyPairID[int64(check[0].(float64))])
|
||||
continue
|
||||
}
|
||||
|
||||
var trade WsTrade
|
||||
|
||||
id, _ := strconv.ParseInt(data[0].(string), 10, 64)
|
||||
trade.Symbol = CurrencyPairID[id]
|
||||
trade.TradeID, _ = data[0].(int64)
|
||||
trade.Side, _ = data[0].(string)
|
||||
trade.Volume, _ = data[0].(float64)
|
||||
trade.Price, _ = data[0].(float64)
|
||||
trade.Timestamp, _ = data[0].(int64)
|
||||
|
||||
p.Websocket.DataHandler <- exchange.TradeData{
|
||||
Timestamp: time.Unix(trade.Timestamp, 0),
|
||||
// CurrencyPair: pair.NewCurrencyPairFromString(trade.Symbol),
|
||||
Side: trade.Side,
|
||||
Amount: trade.Volume,
|
||||
Price: trade.Price,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WsProcessOrderbookSnapshot processes a new orderbook snapshot into a local
|
||||
// of orderbooks
|
||||
func (p *Poloniex) WsProcessOrderbookSnapshot(ob []interface{}, symbol string) error {
|
||||
askdata := ob[0].(map[string]interface{})
|
||||
var asks []orderbook.Item
|
||||
for price, volume := range askdata {
|
||||
assetPrice, err := strconv.ParseFloat(price, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
assetVolume, err := strconv.ParseFloat(volume.(string), 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
asks = append(asks, orderbook.Item{
|
||||
Price: assetPrice,
|
||||
Amount: assetVolume,
|
||||
})
|
||||
}
|
||||
|
||||
bidData := ob[1].(map[string]interface{})
|
||||
var bids []orderbook.Item
|
||||
for price, volume := range bidData {
|
||||
assetPrice, err := strconv.ParseFloat(price, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
assetVolume, err := strconv.ParseFloat(volume.(string), 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bids = append(bids, orderbook.Item{
|
||||
Price: assetPrice,
|
||||
Amount: assetVolume,
|
||||
})
|
||||
}
|
||||
|
||||
var newOrderbook orderbook.Base
|
||||
newOrderbook.Asks = asks
|
||||
newOrderbook.Bids = bids
|
||||
newOrderbook.AssetType = "SPOT"
|
||||
newOrderbook.CurrencyPair = symbol
|
||||
newOrderbook.LastUpdated = time.Now()
|
||||
newOrderbook.Pair = pair.NewCurrencyPairFromString(symbol)
|
||||
|
||||
return p.Websocket.Orderbook.LoadSnapshot(newOrderbook, p.GetName())
|
||||
}
|
||||
|
||||
// WsProcessOrderbookUpdate processses new orderbook updates
|
||||
func (p *Poloniex) WsProcessOrderbookUpdate(target []interface{}, symbol string) error {
|
||||
sideCheck := target[1].(float64)
|
||||
|
||||
cP := pair.NewCurrencyPairFromString(symbol)
|
||||
|
||||
price, err := strconv.ParseFloat(target[2].(string), 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
volume, err := strconv.ParseFloat(target[3].(string), 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if sideCheck == 0 {
|
||||
return p.Websocket.Orderbook.Update(nil,
|
||||
[]orderbook.Item{orderbook.Item{Price: price, Amount: volume}},
|
||||
cP,
|
||||
time.Now(),
|
||||
p.GetName(),
|
||||
"SPOT")
|
||||
}
|
||||
|
||||
return p.Websocket.Orderbook.Update([]orderbook.Item{orderbook.Item{Price: price, Amount: volume}},
|
||||
nil,
|
||||
cP,
|
||||
time.Now(),
|
||||
p.GetName(),
|
||||
"SPOT")
|
||||
}
|
||||
|
||||
// CurrencyPairID contains a list of IDS for currency pairs.
|
||||
var CurrencyPairID = map[int64]string{
|
||||
7: "BTC_BCN",
|
||||
14: "BTC_BTS",
|
||||
15: "BTC_BURST",
|
||||
20: "BTC_CLAM",
|
||||
25: "BTC_DGB",
|
||||
27: "BTC_DOGE",
|
||||
24: "BTC_DASH",
|
||||
38: "BTC_GAME",
|
||||
43: "BTC_HUC",
|
||||
50: "BTC_LTC",
|
||||
51: "BTC_MAID",
|
||||
58: "BTC_OMNI",
|
||||
61: "BTC_NAV",
|
||||
64: "BTC_NMC",
|
||||
69: "BTC_NXT",
|
||||
75: "BTC_PPC",
|
||||
89: "BTC_STR",
|
||||
92: "BTC_SYS",
|
||||
97: "BTC_VIA",
|
||||
100: "BTC_VTC",
|
||||
108: "BTC_XCP",
|
||||
114: "BTC_XMR",
|
||||
116: "BTC_XPM",
|
||||
117: "BTC_XRP",
|
||||
112: "BTC_XEM",
|
||||
148: "BTC_ETH",
|
||||
150: "BTC_SC",
|
||||
153: "BTC_EXP",
|
||||
155: "BTC_FCT",
|
||||
160: "BTC_AMP",
|
||||
162: "BTC_DCR",
|
||||
163: "BTC_LSK",
|
||||
167: "BTC_LBC",
|
||||
168: "BTC_STEEM",
|
||||
170: "BTC_SBD",
|
||||
171: "BTC_ETC",
|
||||
174: "BTC_REP",
|
||||
177: "BTC_ARDR",
|
||||
178: "BTC_ZEC",
|
||||
182: "BTC_STRAT",
|
||||
184: "BTC_PASC",
|
||||
185: "BTC_GNT",
|
||||
187: "BTC_GNO",
|
||||
189: "BTC_BCH",
|
||||
192: "BTC_ZRX",
|
||||
194: "BTC_CVC",
|
||||
196: "BTC_OMG",
|
||||
198: "BTC_GAS",
|
||||
200: "BTC_STORJ",
|
||||
201: "BTC_EOS",
|
||||
204: "BTC_SNT",
|
||||
207: "BTC_KNC",
|
||||
210: "BTC_BAT",
|
||||
213: "BTC_LOOM",
|
||||
221: "BTC_QTUM",
|
||||
121: "USDT_BTC",
|
||||
216: "USDT_DOGE",
|
||||
122: "USDT_DASH",
|
||||
123: "USDT_LTC",
|
||||
124: "USDT_NXT",
|
||||
125: "USDT_STR",
|
||||
126: "USDT_XMR",
|
||||
127: "USDT_XRP",
|
||||
149: "USDT_ETH",
|
||||
219: "USDT_SC",
|
||||
218: "USDT_LSK",
|
||||
173: "USDT_ETC",
|
||||
175: "USDT_REP",
|
||||
180: "USDT_ZEC",
|
||||
217: "USDT_GNT",
|
||||
191: "USDT_BCH",
|
||||
220: "USDT_ZRX",
|
||||
203: "USDT_EOS",
|
||||
206: "USDT_SNT",
|
||||
209: "USDT_KNC",
|
||||
212: "USDT_BAT",
|
||||
215: "USDT_LOOM",
|
||||
223: "USDT_QTUM",
|
||||
129: "XMR_BCN",
|
||||
132: "XMR_DASH",
|
||||
137: "XMR_LTC",
|
||||
138: "XMR_MAID",
|
||||
140: "XMR_NXT",
|
||||
181: "XMR_ZEC",
|
||||
166: "ETH_LSK",
|
||||
169: "ETH_STEEM",
|
||||
172: "ETH_ETC",
|
||||
176: "ETH_REP",
|
||||
179: "ETH_ZEC",
|
||||
186: "ETH_GNT",
|
||||
188: "ETH_GNO",
|
||||
190: "ETH_BCH",
|
||||
193: "ETH_ZRX",
|
||||
195: "ETH_CVC",
|
||||
197: "ETH_OMG",
|
||||
199: "ETH_GAS",
|
||||
202: "ETH_EOS",
|
||||
205: "ETH_SNT",
|
||||
208: "ETH_KNC",
|
||||
211: "ETH_BAT",
|
||||
214: "ETH_LOOM",
|
||||
222: "ETH_QTUM",
|
||||
224: "USDC_BTC",
|
||||
226: "USDC_USDT",
|
||||
225: "USDC_ETH",
|
||||
}
|
||||
|
||||
// CurrencyID defines IDs to a currency supported by the exchange
|
||||
var CurrencyID = map[int64]string{
|
||||
1: "1CR",
|
||||
2: "ABY",
|
||||
3: "AC",
|
||||
4: "ACH",
|
||||
5: "ADN",
|
||||
6: "AEON",
|
||||
7: "AERO",
|
||||
8: "AIR",
|
||||
9: "APH",
|
||||
10: "AUR",
|
||||
11: "AXIS",
|
||||
12: "BALLS",
|
||||
13: "BANK",
|
||||
14: "BBL",
|
||||
15: "BBR",
|
||||
16: "BCC",
|
||||
17: "BCN",
|
||||
18: "BDC",
|
||||
19: "BDG",
|
||||
20: "BELA",
|
||||
21: "BITS",
|
||||
22: "BLK",
|
||||
23: "BLOCK",
|
||||
24: "BLU",
|
||||
25: "BNS",
|
||||
26: "BONES",
|
||||
27: "BOST",
|
||||
28: "BTC",
|
||||
29: "BTCD",
|
||||
30: "BTCS",
|
||||
31: "BTM",
|
||||
32: "BTS",
|
||||
33: "BURN",
|
||||
34: "BURST",
|
||||
35: "C2",
|
||||
36: "CACH",
|
||||
37: "CAI",
|
||||
38: "CC",
|
||||
39: "CCN",
|
||||
40: "CGA",
|
||||
41: "CHA",
|
||||
42: "CINNI",
|
||||
43: "CLAM",
|
||||
44: "CNL",
|
||||
45: "CNMT",
|
||||
46: "CNOTE",
|
||||
47: "COMM",
|
||||
48: "CON",
|
||||
49: "CORG",
|
||||
50: "CRYPT",
|
||||
51: "CURE",
|
||||
52: "CYC",
|
||||
53: "DGB",
|
||||
54: "DICE",
|
||||
55: "DIEM",
|
||||
56: "DIME",
|
||||
57: "DIS",
|
||||
58: "DNS",
|
||||
59: "DOGE",
|
||||
60: "DASH",
|
||||
61: "DRKC",
|
||||
62: "DRM",
|
||||
63: "DSH",
|
||||
64: "DVK",
|
||||
65: "EAC",
|
||||
66: "EBT",
|
||||
67: "ECC",
|
||||
68: "EFL",
|
||||
69: "EMC2",
|
||||
70: "EMO",
|
||||
71: "ENC",
|
||||
72: "eTOK",
|
||||
73: "EXE",
|
||||
74: "FAC",
|
||||
75: "FCN",
|
||||
76: "FIBRE",
|
||||
77: "FLAP",
|
||||
78: "FLDC",
|
||||
79: "FLT",
|
||||
80: "FOX",
|
||||
81: "FRAC",
|
||||
82: "FRK",
|
||||
83: "FRQ",
|
||||
84: "FVZ",
|
||||
85: "FZ",
|
||||
86: "FZN",
|
||||
87: "GAP",
|
||||
88: "GDN",
|
||||
89: "GEMZ",
|
||||
90: "GEO",
|
||||
91: "GIAR",
|
||||
92: "GLB",
|
||||
93: "GAME",
|
||||
94: "GML",
|
||||
95: "GNS",
|
||||
96: "GOLD",
|
||||
97: "GPC",
|
||||
98: "GPUC",
|
||||
99: "GRCX",
|
||||
100: "GRS",
|
||||
101: "GUE",
|
||||
102: "H2O",
|
||||
103: "HIRO",
|
||||
104: "HOT",
|
||||
105: "HUC",
|
||||
106: "HVC",
|
||||
107: "HYP",
|
||||
108: "HZ",
|
||||
109: "IFC",
|
||||
110: "ITC",
|
||||
111: "IXC",
|
||||
112: "JLH",
|
||||
113: "JPC",
|
||||
114: "JUG",
|
||||
115: "KDC",
|
||||
116: "KEY",
|
||||
117: "LC",
|
||||
118: "LCL",
|
||||
119: "LEAF",
|
||||
120: "LGC",
|
||||
121: "LOL",
|
||||
122: "LOVE",
|
||||
123: "LQD",
|
||||
124: "LTBC",
|
||||
125: "LTC",
|
||||
126: "LTCX",
|
||||
127: "MAID",
|
||||
128: "MAST",
|
||||
129: "MAX",
|
||||
130: "MCN",
|
||||
131: "MEC",
|
||||
132: "METH",
|
||||
133: "MIL",
|
||||
134: "MIN",
|
||||
135: "MINT",
|
||||
136: "MMC",
|
||||
137: "MMNXT",
|
||||
138: "MMXIV",
|
||||
139: "MNTA",
|
||||
140: "MON",
|
||||
141: "MRC",
|
||||
142: "MRS",
|
||||
143: "OMNI",
|
||||
144: "MTS",
|
||||
145: "MUN",
|
||||
146: "MYR",
|
||||
147: "MZC",
|
||||
148: "N5X",
|
||||
149: "NAS",
|
||||
150: "NAUT",
|
||||
151: "NAV",
|
||||
152: "NBT",
|
||||
153: "NEOS",
|
||||
154: "NL",
|
||||
155: "NMC",
|
||||
156: "NOBL",
|
||||
157: "NOTE",
|
||||
158: "NOXT",
|
||||
159: "NRS",
|
||||
160: "NSR",
|
||||
161: "NTX",
|
||||
162: "NXT",
|
||||
163: "NXTI",
|
||||
164: "OPAL",
|
||||
165: "PAND",
|
||||
166: "PAWN",
|
||||
167: "PIGGY",
|
||||
168: "PINK",
|
||||
169: "PLX",
|
||||
170: "PMC",
|
||||
171: "POT",
|
||||
172: "PPC",
|
||||
173: "PRC",
|
||||
174: "PRT",
|
||||
175: "PTS",
|
||||
176: "Q2C",
|
||||
177: "QBK",
|
||||
178: "QCN",
|
||||
179: "QORA",
|
||||
180: "QTL",
|
||||
181: "RBY",
|
||||
182: "RDD",
|
||||
183: "RIC",
|
||||
184: "RZR",
|
||||
185: "SDC",
|
||||
186: "SHIBE",
|
||||
187: "SHOPX",
|
||||
188: "SILK",
|
||||
189: "SJCX",
|
||||
190: "SLR",
|
||||
191: "SMC",
|
||||
192: "SOC",
|
||||
193: "SPA",
|
||||
194: "SQL",
|
||||
195: "SRCC",
|
||||
196: "SRG",
|
||||
197: "SSD",
|
||||
198: "STR",
|
||||
199: "SUM",
|
||||
200: "SUN",
|
||||
201: "SWARM",
|
||||
202: "SXC",
|
||||
203: "SYNC",
|
||||
204: "SYS",
|
||||
205: "TAC",
|
||||
206: "TOR",
|
||||
207: "TRUST",
|
||||
208: "TWE",
|
||||
209: "UIS",
|
||||
210: "ULTC",
|
||||
211: "UNITY",
|
||||
212: "URO",
|
||||
213: "USDE",
|
||||
214: "USDT",
|
||||
215: "UTC",
|
||||
216: "UTIL",
|
||||
217: "UVC",
|
||||
218: "VIA",
|
||||
219: "VOOT",
|
||||
220: "VRC",
|
||||
221: "VTC",
|
||||
222: "WC",
|
||||
223: "WDC",
|
||||
224: "WIKI",
|
||||
225: "WOLF",
|
||||
226: "X13",
|
||||
227: "XAI",
|
||||
228: "XAP",
|
||||
229: "XBC",
|
||||
230: "XC",
|
||||
231: "XCH",
|
||||
232: "XCN",
|
||||
233: "XCP",
|
||||
234: "XCR",
|
||||
235: "XDN",
|
||||
236: "XDP",
|
||||
237: "XHC",
|
||||
238: "XLB",
|
||||
239: "XMG",
|
||||
240: "XMR",
|
||||
241: "XPB",
|
||||
242: "XPM",
|
||||
243: "XRP",
|
||||
244: "XSI",
|
||||
245: "XST",
|
||||
246: "XSV",
|
||||
247: "XUSD",
|
||||
248: "XXC",
|
||||
249: "YACC",
|
||||
250: "YANG",
|
||||
251: "YC",
|
||||
252: "YIN",
|
||||
253: "XVC",
|
||||
254: "FLO",
|
||||
256: "XEM",
|
||||
258: "ARCH",
|
||||
260: "HUGE",
|
||||
261: "GRC",
|
||||
263: "IOC",
|
||||
265: "INDEX",
|
||||
267: "ETH",
|
||||
268: "SC",
|
||||
269: "BCY",
|
||||
270: "EXP",
|
||||
271: "FCT",
|
||||
272: "BITUSD",
|
||||
273: "BITCNY",
|
||||
274: "RADS",
|
||||
275: "AMP",
|
||||
276: "VOX",
|
||||
277: "DCR",
|
||||
278: "LSK",
|
||||
279: "DAO",
|
||||
280: "LBC",
|
||||
281: "STEEM",
|
||||
282: "SBD",
|
||||
283: "ETC",
|
||||
284: "REP",
|
||||
285: "ARDR",
|
||||
286: "ZEC",
|
||||
287: "STRAT",
|
||||
288: "NXC",
|
||||
289: "PASC",
|
||||
290: "GNT",
|
||||
291: "GNO",
|
||||
292: "BCH",
|
||||
293: "ZRX",
|
||||
294: "CVC",
|
||||
295: "OMG",
|
||||
296: "GAS",
|
||||
297: "STORJ",
|
||||
298: "EOS",
|
||||
299: "USDC",
|
||||
300: "SNT",
|
||||
301: "KNC",
|
||||
302: "BAT",
|
||||
303: "LOOM",
|
||||
304: "QTUM",
|
||||
}
|
||||
|
||||
@@ -13,54 +13,50 @@ import (
|
||||
)
|
||||
|
||||
// Start starts the Poloniex go routine
|
||||
func (po *Poloniex) Start(wg *sync.WaitGroup) {
|
||||
func (p *Poloniex) Start(wg *sync.WaitGroup) {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
po.Run()
|
||||
p.Run()
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
// Run implements the Poloniex wrapper
|
||||
func (po *Poloniex) Run() {
|
||||
if po.Verbose {
|
||||
log.Printf("%s Websocket: %s (url: %s).\n", po.GetName(), common.IsEnabled(po.Websocket), poloniexWebsocketAddress)
|
||||
log.Printf("%s polling delay: %ds.\n", po.GetName(), po.RESTPollingDelay)
|
||||
log.Printf("%s %d currencies enabled: %s.\n", po.GetName(), len(po.EnabledPairs), po.EnabledPairs)
|
||||
func (p *Poloniex) Run() {
|
||||
if p.Verbose {
|
||||
log.Printf("%s Websocket: %s (url: %s).\n", p.GetName(), common.IsEnabled(p.Websocket.IsEnabled()), poloniexWebsocketAddress)
|
||||
log.Printf("%s polling delay: %ds.\n", p.GetName(), p.RESTPollingDelay)
|
||||
log.Printf("%s %d currencies enabled: %s.\n", p.GetName(), len(p.EnabledPairs), p.EnabledPairs)
|
||||
}
|
||||
|
||||
if po.Websocket {
|
||||
go po.WebsocketClient()
|
||||
}
|
||||
|
||||
exchangeCurrencies, err := po.GetExchangeCurrencies()
|
||||
exchangeCurrencies, err := p.GetExchangeCurrencies()
|
||||
if err != nil {
|
||||
log.Printf("%s Failed to get available symbols.\n", po.GetName())
|
||||
log.Printf("%s Failed to get available symbols.\n", p.GetName())
|
||||
} else {
|
||||
forceUpdate := false
|
||||
if common.StringDataCompare(po.AvailablePairs, "BTC_USDT") {
|
||||
if common.StringDataCompare(p.AvailablePairs, "BTC_USDT") {
|
||||
log.Printf("%s contains invalid pair, forcing upgrade of available currencies.\n",
|
||||
po.GetName())
|
||||
p.GetName())
|
||||
forceUpdate = true
|
||||
}
|
||||
err = po.UpdateCurrencies(exchangeCurrencies, false, forceUpdate)
|
||||
err = p.UpdateCurrencies(exchangeCurrencies, false, forceUpdate)
|
||||
if err != nil {
|
||||
log.Printf("%s Failed to update available currencies %s.\n", po.GetName(), err)
|
||||
log.Printf("%s Failed to update available currencies %s.\n", p.GetName(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateTicker updates and returns the ticker for a currency pair
|
||||
func (po *Poloniex) UpdateTicker(currencyPair pair.CurrencyPair, assetType string) (ticker.Price, error) {
|
||||
func (p *Poloniex) UpdateTicker(currencyPair pair.CurrencyPair, assetType string) (ticker.Price, error) {
|
||||
var tickerPrice ticker.Price
|
||||
tick, err := po.GetTicker()
|
||||
tick, err := p.GetTicker()
|
||||
if err != nil {
|
||||
return tickerPrice, err
|
||||
}
|
||||
|
||||
for _, x := range po.GetEnabledCurrencies() {
|
||||
for _, x := range p.GetEnabledCurrencies() {
|
||||
var tp ticker.Price
|
||||
curr := exchange.FormatExchangeCurrency(po.GetName(), x).String()
|
||||
curr := exchange.FormatExchangeCurrency(p.GetName(), x).String()
|
||||
tp.Pair = x
|
||||
tp.Ask = tick[curr].LowestAsk
|
||||
tp.Bid = tick[curr].HighestBid
|
||||
@@ -68,39 +64,39 @@ func (po *Poloniex) UpdateTicker(currencyPair pair.CurrencyPair, assetType strin
|
||||
tp.Last = tick[curr].Last
|
||||
tp.Low = tick[curr].Low24Hr
|
||||
tp.Volume = tick[curr].BaseVolume
|
||||
ticker.ProcessTicker(po.GetName(), x, tp, assetType)
|
||||
ticker.ProcessTicker(p.GetName(), x, tp, assetType)
|
||||
}
|
||||
return ticker.GetTicker(po.Name, currencyPair, assetType)
|
||||
return ticker.GetTicker(p.Name, currencyPair, assetType)
|
||||
}
|
||||
|
||||
// GetTickerPrice returns the ticker for a currency pair
|
||||
func (po *Poloniex) GetTickerPrice(currencyPair pair.CurrencyPair, assetType string) (ticker.Price, error) {
|
||||
tickerNew, err := ticker.GetTicker(po.GetName(), currencyPair, assetType)
|
||||
func (p *Poloniex) GetTickerPrice(currencyPair pair.CurrencyPair, assetType string) (ticker.Price, error) {
|
||||
tickerNew, err := ticker.GetTicker(p.GetName(), currencyPair, assetType)
|
||||
if err != nil {
|
||||
return po.UpdateTicker(currencyPair, assetType)
|
||||
return p.UpdateTicker(currencyPair, assetType)
|
||||
}
|
||||
return tickerNew, nil
|
||||
}
|
||||
|
||||
// GetOrderbookEx returns orderbook base on the currency pair
|
||||
func (po *Poloniex) GetOrderbookEx(currencyPair pair.CurrencyPair, assetType string) (orderbook.Base, error) {
|
||||
ob, err := orderbook.GetOrderbook(po.GetName(), currencyPair, assetType)
|
||||
func (p *Poloniex) GetOrderbookEx(currencyPair pair.CurrencyPair, assetType string) (orderbook.Base, error) {
|
||||
ob, err := orderbook.GetOrderbook(p.GetName(), currencyPair, assetType)
|
||||
if err != nil {
|
||||
return po.UpdateOrderbook(currencyPair, assetType)
|
||||
return p.UpdateOrderbook(currencyPair, assetType)
|
||||
}
|
||||
return ob, nil
|
||||
}
|
||||
|
||||
// UpdateOrderbook updates and returns the orderbook for a currency pair
|
||||
func (po *Poloniex) UpdateOrderbook(currencyPair pair.CurrencyPair, assetType string) (orderbook.Base, error) {
|
||||
func (p *Poloniex) UpdateOrderbook(currencyPair pair.CurrencyPair, assetType string) (orderbook.Base, error) {
|
||||
var orderBook orderbook.Base
|
||||
orderbookNew, err := po.GetOrderbook("", 1000)
|
||||
orderbookNew, err := p.GetOrderbook("", 1000)
|
||||
if err != nil {
|
||||
return orderBook, err
|
||||
}
|
||||
|
||||
for _, x := range po.GetEnabledCurrencies() {
|
||||
currency := exchange.FormatExchangeCurrency(po.Name, x).String()
|
||||
for _, x := range p.GetEnabledCurrencies() {
|
||||
currency := exchange.FormatExchangeCurrency(p.Name, x).String()
|
||||
data, ok := orderbookNew.Data[currency]
|
||||
if !ok {
|
||||
continue
|
||||
@@ -120,17 +116,17 @@ func (po *Poloniex) UpdateOrderbook(currencyPair pair.CurrencyPair, assetType st
|
||||
obItems = append(obItems, orderbook.Item{Amount: obData.Amount, Price: obData.Price})
|
||||
}
|
||||
orderBook.Asks = obItems
|
||||
orderbook.ProcessOrderbook(po.Name, x, orderBook, assetType)
|
||||
orderbook.ProcessOrderbook(p.Name, x, orderBook, assetType)
|
||||
}
|
||||
return orderbook.GetOrderbook(po.Name, currencyPair, assetType)
|
||||
return orderbook.GetOrderbook(p.Name, currencyPair, assetType)
|
||||
}
|
||||
|
||||
// GetExchangeAccountInfo retrieves balances for all enabled currencies for the
|
||||
// Poloniex exchange
|
||||
func (po *Poloniex) GetExchangeAccountInfo() (exchange.AccountInfo, error) {
|
||||
func (p *Poloniex) GetExchangeAccountInfo() (exchange.AccountInfo, error) {
|
||||
var response exchange.AccountInfo
|
||||
response.ExchangeName = po.GetName()
|
||||
accountBalance, err := po.GetBalances()
|
||||
response.ExchangeName = p.GetName()
|
||||
accountBalance, err := p.GetBalances()
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
@@ -146,64 +142,69 @@ func (po *Poloniex) GetExchangeAccountInfo() (exchange.AccountInfo, error) {
|
||||
|
||||
// GetExchangeFundTransferHistory returns funding history, deposits and
|
||||
// withdrawals
|
||||
func (po *Poloniex) GetExchangeFundTransferHistory() ([]exchange.FundHistory, error) {
|
||||
func (p *Poloniex) GetExchangeFundTransferHistory() ([]exchange.FundHistory, error) {
|
||||
var fundHistory []exchange.FundHistory
|
||||
return fundHistory, errors.New("not supported on exchange")
|
||||
}
|
||||
|
||||
// GetExchangeHistory returns historic trade data since exchange opening.
|
||||
func (po *Poloniex) GetExchangeHistory(p pair.CurrencyPair, assetType string) ([]exchange.TradeHistory, error) {
|
||||
func (p *Poloniex) GetExchangeHistory(cP pair.CurrencyPair, assetType string) ([]exchange.TradeHistory, error) {
|
||||
var resp []exchange.TradeHistory
|
||||
|
||||
return resp, errors.New("trade history not yet implemented")
|
||||
}
|
||||
|
||||
// SubmitExchangeOrder submits a new order
|
||||
func (po *Poloniex) SubmitExchangeOrder(p pair.CurrencyPair, side exchange.OrderSide, orderType exchange.OrderType, amount, price float64, clientID string) (int64, error) {
|
||||
func (p *Poloniex) SubmitExchangeOrder(cP pair.CurrencyPair, side exchange.OrderSide, orderType exchange.OrderType, amount, price float64, clientID string) (int64, error) {
|
||||
return 0, errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// ModifyExchangeOrder will allow of changing orderbook placement and limit to
|
||||
// market conversion
|
||||
func (po *Poloniex) ModifyExchangeOrder(orderID int64, action exchange.ModifyOrder) (int64, error) {
|
||||
func (p *Poloniex) ModifyExchangeOrder(orderID int64, action exchange.ModifyOrder) (int64, error) {
|
||||
return 0, errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// CancelExchangeOrder cancels an order by its corresponding ID number
|
||||
func (po *Poloniex) CancelExchangeOrder(orderID int64) error {
|
||||
func (p *Poloniex) CancelExchangeOrder(orderID int64) error {
|
||||
return errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// CancelAllExchangeOrders cancels all orders associated with a currency pair
|
||||
func (po *Poloniex) CancelAllExchangeOrders() error {
|
||||
func (p *Poloniex) CancelAllExchangeOrders() error {
|
||||
return errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// GetExchangeOrderInfo returns information on a current open order
|
||||
func (po *Poloniex) GetExchangeOrderInfo(orderID int64) (exchange.OrderDetail, error) {
|
||||
func (p *Poloniex) GetExchangeOrderInfo(orderID int64) (exchange.OrderDetail, error) {
|
||||
var orderDetail exchange.OrderDetail
|
||||
return orderDetail, errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// GetExchangeDepositAddress returns a deposit address for a specified currency
|
||||
func (po *Poloniex) GetExchangeDepositAddress(cryptocurrency pair.CurrencyItem) (string, error) {
|
||||
func (p *Poloniex) GetExchangeDepositAddress(cryptocurrency pair.CurrencyItem) (string, error) {
|
||||
return "", errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// WithdrawCryptoExchangeFunds returns a withdrawal ID when a withdrawal is
|
||||
// submitted
|
||||
func (po *Poloniex) WithdrawCryptoExchangeFunds(address string, cryptocurrency pair.CurrencyItem, amount float64) (string, error) {
|
||||
func (p *Poloniex) WithdrawCryptoExchangeFunds(address string, cryptocurrency pair.CurrencyItem, amount float64) (string, error) {
|
||||
return "", errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// WithdrawFiatExchangeFunds returns a withdrawal ID when a
|
||||
// withdrawal is submitted
|
||||
func (po *Poloniex) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount float64) (string, error) {
|
||||
func (p *Poloniex) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount float64) (string, error) {
|
||||
return "", errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// WithdrawFiatExchangeFundsToInternationalBank returns a withdrawal ID when a
|
||||
// withdrawal is submitted
|
||||
func (po *Poloniex) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) {
|
||||
func (p *Poloniex) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) {
|
||||
return "", errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// GetWebsocket returns a pointer to the exchange websocket
|
||||
func (p *Poloniex) GetWebsocket() (*exchange.Websocket, error) {
|
||||
return p.Websocket, nil
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -16,7 +17,8 @@ import (
|
||||
var supportedMethods = []string{"GET", "POST", "HEAD", "PUT", "DELETE", "OPTIONS", "CONNECT"}
|
||||
|
||||
const (
|
||||
maxRequestJobs = 50
|
||||
maxRequestJobs = 50
|
||||
proxyTLSTimeout = 15 * time.Second
|
||||
)
|
||||
|
||||
// Requester struct for the request client
|
||||
@@ -191,6 +193,7 @@ func (r *Requester) GetRateLimit(auth bool) *RateLimit {
|
||||
|
||||
// New returns a new Requester
|
||||
func New(name string, authLimit, unauthLimit *RateLimit, httpRequester *http.Client) *Requester {
|
||||
|
||||
return &Requester{
|
||||
HTTPClient: httpRequester,
|
||||
UnauthLimit: unauthLimit,
|
||||
@@ -380,3 +383,16 @@ func (r *Requester) SendPayload(method, path string, headers map[string]string,
|
||||
}
|
||||
return resp.Error
|
||||
}
|
||||
|
||||
// SetProxy sets a proxy address to the client transport
|
||||
func (r *Requester) SetProxy(p *url.URL) error {
|
||||
if p.String() == "" {
|
||||
return errors.New("No proxy URL supplied")
|
||||
}
|
||||
|
||||
r.HTTPClient.Transport = &http.Transport{
|
||||
Proxy: http.ProxyURL(p),
|
||||
TLSHandshakeTimeout: proxyTLSTimeout,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -53,7 +53,6 @@ func (w *WEX) SetDefaults() {
|
||||
w.Enabled = false
|
||||
w.Fee = 0.2
|
||||
w.Verbose = false
|
||||
w.Websocket = false
|
||||
w.RESTPollingDelay = 10
|
||||
w.Ticker = make(map[string]Ticker)
|
||||
w.RequestCurrencyPairFormat.Delimiter = "_"
|
||||
@@ -72,6 +71,7 @@ func (w *WEX) SetDefaults() {
|
||||
w.APIUrl = w.APIUrlDefault
|
||||
w.APIUrlSecondaryDefault = wexAPIPrivateURL
|
||||
w.APIUrlSecondary = w.APIUrlSecondaryDefault
|
||||
w.WebsocketInit()
|
||||
}
|
||||
|
||||
// Setup sets exchange configuration parameters for WEX
|
||||
@@ -86,7 +86,6 @@ func (w *WEX) Setup(exch config.ExchangeConfig) {
|
||||
w.SetHTTPClientUserAgent(exch.HTTPUserAgent)
|
||||
w.RESTPollingDelay = exch.RESTPollingDelay
|
||||
w.Verbose = exch.Verbose
|
||||
w.Websocket = exch.Websocket
|
||||
w.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",")
|
||||
w.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",")
|
||||
w.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",")
|
||||
@@ -106,6 +105,10 @@ func (w *WEX) Setup(exch config.ExchangeConfig) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = w.SetClientProxyAddress(exch.ProxyAddress)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ func (w *WEX) Start(wg *sync.WaitGroup) {
|
||||
// Run implements the WEX wrapper
|
||||
func (w *WEX) Run() {
|
||||
if w.Verbose {
|
||||
log.Printf("%s Websocket: %s.", w.GetName(), common.IsEnabled(w.Websocket))
|
||||
log.Printf("%s Websocket: %s.", w.GetName(), common.IsEnabled(w.Websocket.IsEnabled()))
|
||||
log.Printf("%s polling delay: %ds.\n", w.GetName(), w.RESTPollingDelay)
|
||||
log.Printf("%s %d currencies enabled: %s.\n", w.GetName(), len(w.EnabledPairs), w.EnabledPairs)
|
||||
}
|
||||
@@ -206,3 +206,8 @@ func (w *WEX) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount float
|
||||
func (w *WEX) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) {
|
||||
return "", errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// GetWebsocket returns a pointer to the exchange websocket
|
||||
func (w *WEX) GetWebsocket() (*exchange.Websocket, error) {
|
||||
return nil, errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
@@ -51,7 +51,6 @@ func (y *Yobit) SetDefaults() {
|
||||
y.Enabled = true
|
||||
y.Fee = 0.2
|
||||
y.Verbose = false
|
||||
y.Websocket = false
|
||||
y.RESTPollingDelay = 10
|
||||
y.AuthenticatedAPISupport = true
|
||||
y.Ticker = make(map[string]Ticker)
|
||||
@@ -71,6 +70,7 @@ func (y *Yobit) SetDefaults() {
|
||||
y.APIUrl = y.APIUrlDefault
|
||||
y.APIUrlSecondaryDefault = apiPrivateURL
|
||||
y.APIUrlSecondary = y.APIUrlSecondaryDefault
|
||||
y.WebsocketInit()
|
||||
}
|
||||
|
||||
// Setup sets exchange configuration parameters for Yobit
|
||||
@@ -83,7 +83,7 @@ func (y *Yobit) Setup(exch config.ExchangeConfig) {
|
||||
y.SetAPIKeys(exch.APIKey, exch.APISecret, "", false)
|
||||
y.RESTPollingDelay = exch.RESTPollingDelay
|
||||
y.Verbose = exch.Verbose
|
||||
y.Websocket = exch.Websocket
|
||||
y.Websocket.SetEnabled(exch.Websocket)
|
||||
y.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",")
|
||||
y.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",")
|
||||
y.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",")
|
||||
@@ -105,6 +105,10 @@ func (y *Yobit) Setup(exch config.ExchangeConfig) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = y.SetClientProxyAddress(exch.ProxyAddress)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ func (y *Yobit) Start(wg *sync.WaitGroup) {
|
||||
// Run implements the Yobit wrapper
|
||||
func (y *Yobit) Run() {
|
||||
if y.Verbose {
|
||||
log.Printf("%s Websocket: %s.", y.GetName(), common.IsEnabled(y.Websocket))
|
||||
log.Printf("%s Websocket: %s.", y.GetName(), common.IsEnabled(y.Websocket.IsEnabled()))
|
||||
log.Printf("%s polling delay: %ds.\n", y.GetName(), y.RESTPollingDelay)
|
||||
log.Printf("%s %d currencies enabled: %s.\n", y.GetName(), len(y.EnabledPairs), y.EnabledPairs)
|
||||
}
|
||||
@@ -188,3 +188,8 @@ func (y *Yobit) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount flo
|
||||
func (y *Yobit) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) {
|
||||
return "", errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// GetWebsocket returns a pointer to the exchange websocket
|
||||
func (y *Yobit) GetWebsocket() (*exchange.Websocket, error) {
|
||||
return nil, errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
@@ -48,7 +48,6 @@ func (z *ZB) SetDefaults() {
|
||||
z.Enabled = false
|
||||
z.Fee = 0
|
||||
z.Verbose = false
|
||||
z.Websocket = false
|
||||
z.RESTPollingDelay = 10
|
||||
z.RequestCurrencyPairFormat.Delimiter = "_"
|
||||
z.RequestCurrencyPairFormat.Uppercase = false
|
||||
@@ -65,6 +64,7 @@ func (z *ZB) SetDefaults() {
|
||||
z.APIUrl = z.APIUrlDefault
|
||||
z.APIUrlSecondaryDefault = zbMarketURL
|
||||
z.APIUrlSecondary = z.APIUrlSecondaryDefault
|
||||
z.WebsocketInit()
|
||||
}
|
||||
|
||||
// Setup sets user configuration
|
||||
@@ -80,7 +80,7 @@ func (z *ZB) Setup(exch config.ExchangeConfig) {
|
||||
z.SetHTTPClientUserAgent(exch.HTTPUserAgent)
|
||||
z.RESTPollingDelay = exch.RESTPollingDelay
|
||||
z.Verbose = exch.Verbose
|
||||
z.Websocket = exch.Websocket
|
||||
z.Websocket.SetEnabled(exch.Websocket)
|
||||
z.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",")
|
||||
z.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",")
|
||||
z.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",")
|
||||
@@ -100,6 +100,10 @@ func (z *ZB) Setup(exch config.ExchangeConfig) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = z.SetClientProxyAddress(exch.ProxyAddress)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ func (z *ZB) Start(wg *sync.WaitGroup) {
|
||||
// Run implements the OKEX wrapper
|
||||
func (z *ZB) Run() {
|
||||
if z.Verbose {
|
||||
log.Printf("%s Websocket: %s. (url: %s).\n", z.GetName(), common.IsEnabled(z.Websocket), z.WebsocketURL)
|
||||
log.Printf("%s Websocket: %s. (url: %s).\n", z.GetName(), common.IsEnabled(z.Websocket.IsEnabled()), z.WebsocketURL)
|
||||
log.Printf("%s polling delay: %ds.\n", z.GetName(), z.RESTPollingDelay)
|
||||
log.Printf("%s %d currencies enabled: %s.\n", z.GetName(), len(z.EnabledPairs), z.EnabledPairs)
|
||||
}
|
||||
@@ -184,3 +184,8 @@ func (z *ZB) WithdrawFiatExchangeFunds(currency pair.CurrencyItem, amount float6
|
||||
func (z *ZB) WithdrawFiatExchangeFundsToInternationalBank(currency pair.CurrencyItem, amount float64) (string, error) {
|
||||
return "", errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
// GetWebsocket returns a pointer to the exchange websocket
|
||||
func (z *ZB) GetWebsocket() (*exchange.Websocket, error) {
|
||||
return nil, errors.New("not yet implemented")
|
||||
}
|
||||
|
||||
12
main.go
12
main.go
@@ -59,6 +59,8 @@ func main() {
|
||||
flag.StringVar(&bot.dataDir, "datadir", common.GetDefaultDataDir(runtime.GOOS), "default data directory for GoCryptoTrader files")
|
||||
dryrun := flag.Bool("dryrun", false, "dry runs bot, doesn't save config file")
|
||||
version := flag.Bool("version", false, "retrieves current GoCryptoTrader version")
|
||||
verbosity := flag.Bool("verbose", false, "-verbose increases logging verbosity for GoCryptoTrader")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if *version {
|
||||
@@ -133,10 +135,6 @@ func main() {
|
||||
bot.portfolio.SeedPortfolio(bot.config.Portfolio)
|
||||
SeedExchangeAccountInfo(GetAllEnabledExchangeAccountInfo().Data)
|
||||
|
||||
go portfolio.StartPortfolioWatcher()
|
||||
go TickerUpdaterRoutine()
|
||||
go OrderbookUpdaterRoutine()
|
||||
|
||||
if bot.config.Webserver.Enabled {
|
||||
listenAddr := bot.config.Webserver.ListenAddress
|
||||
log.Printf(
|
||||
@@ -159,6 +157,12 @@ func main() {
|
||||
log.Println("HTTP RESTful Webserver support disabled.")
|
||||
}
|
||||
|
||||
go portfolio.StartPortfolioWatcher()
|
||||
|
||||
go TickerUpdaterRoutine(*verbosity)
|
||||
go OrderbookUpdaterRoutine(*verbosity)
|
||||
go WebsocketRoutine(*verbosity)
|
||||
|
||||
<-bot.shutdown
|
||||
Shutdown()
|
||||
}
|
||||
|
||||
225
routines.go
225
routines.go
@@ -1,11 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/thrasher-/gocryptotrader/common"
|
||||
"github.com/thrasher-/gocryptotrader/currency"
|
||||
"github.com/thrasher-/gocryptotrader/currency/pair"
|
||||
"github.com/thrasher-/gocryptotrader/currency/symbol"
|
||||
@@ -51,7 +53,7 @@ func printConvertCurrencyFormat(origCurrency string, origPrice float64) string {
|
||||
)
|
||||
}
|
||||
|
||||
func printTickerSummary(result ticker.Price, p pair.CurrencyPair, assetType, exchangeName string, err error) {
|
||||
func printTickerSummary(result ticker.Price, p pair.CurrencyPair, assetType, exchangeName string, err error, verbose bool) {
|
||||
if err != nil {
|
||||
log.Printf("Failed to get %s %s ticker. Error: %s",
|
||||
p.Pair().String(),
|
||||
@@ -60,6 +62,10 @@ func printTickerSummary(result ticker.Price, p pair.CurrencyPair, assetType, exc
|
||||
return
|
||||
}
|
||||
|
||||
if !verbose {
|
||||
return
|
||||
}
|
||||
|
||||
stats.Add(exchangeName, p, assetType, result.Last, result.Volume)
|
||||
if currency.IsFiatCurrency(p.SecondCurrency.String()) && p.SecondCurrency.String() != bot.config.Currency.FiatDisplayCurrency {
|
||||
origCurrency := p.SecondCurrency.Upper().String()
|
||||
@@ -100,7 +106,7 @@ func printTickerSummary(result ticker.Price, p pair.CurrencyPair, assetType, exc
|
||||
}
|
||||
}
|
||||
|
||||
func printOrderbookSummary(result orderbook.Base, p pair.CurrencyPair, assetType, exchangeName string, err error) {
|
||||
func printOrderbookSummary(result orderbook.Base, p pair.CurrencyPair, assetType, exchangeName string, err error, verbose bool) {
|
||||
if err != nil {
|
||||
log.Printf("Failed to get %s %s orderbook. Error: %s",
|
||||
p.Pair().String(),
|
||||
@@ -108,6 +114,11 @@ func printOrderbookSummary(result orderbook.Base, p pair.CurrencyPair, assetType
|
||||
err)
|
||||
return
|
||||
}
|
||||
|
||||
if !verbose {
|
||||
return
|
||||
}
|
||||
|
||||
bidsAmount, bidsValue := result.CalculateTotalBids()
|
||||
asksAmount, asksValue := result.CalculateTotalAsks()
|
||||
|
||||
@@ -157,10 +168,9 @@ func printOrderbookSummary(result orderbook.Base, p pair.CurrencyPair, assetType
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func relayWebsocketEvent(result interface{}, event, assetType, exchangeName string) {
|
||||
func relayWebsocketEvent(result interface{}, event, assetType, exchangeName string, verbose bool) {
|
||||
evt := WebsocketEvent{
|
||||
Data: result,
|
||||
Event: event,
|
||||
@@ -169,14 +179,16 @@ func relayWebsocketEvent(result interface{}, event, assetType, exchangeName stri
|
||||
}
|
||||
err := BroadcastWebsocketMessage(evt)
|
||||
if err != nil {
|
||||
log.Println(fmt.Errorf("Failed to broadcast websocket event. Error: %s",
|
||||
err))
|
||||
if verbose {
|
||||
log.Println(fmt.Errorf("Failed to broadcast websocket event. Error: %s",
|
||||
err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TickerUpdaterRoutine fetches and updates the ticker for all enabled
|
||||
// currency pairs and exchanges
|
||||
func TickerUpdaterRoutine() {
|
||||
func TickerUpdaterRoutine(verbose bool) {
|
||||
log.Println("Starting ticker updater routine.")
|
||||
var wg sync.WaitGroup
|
||||
for {
|
||||
@@ -205,11 +217,11 @@ func TickerUpdaterRoutine() {
|
||||
} else {
|
||||
result, err = exch.GetTickerPrice(c, assetType)
|
||||
}
|
||||
printTickerSummary(result, c, assetType, exchangeName, err)
|
||||
printTickerSummary(result, c, assetType, exchangeName, err, verbose)
|
||||
if err == nil {
|
||||
bot.comms.StageTickerData(exchangeName, assetType, result)
|
||||
if bot.config.Webserver.Enabled {
|
||||
relayWebsocketEvent(result, "ticker_update", assetType, exchangeName)
|
||||
relayWebsocketEvent(result, "ticker_update", assetType, exchangeName, verbose)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,14 +238,16 @@ func TickerUpdaterRoutine() {
|
||||
}(x, &wg)
|
||||
}
|
||||
wg.Wait()
|
||||
log.Println("All enabled currency tickers fetched.")
|
||||
if verbose {
|
||||
log.Println("All enabled currency tickers fetched.")
|
||||
}
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
}
|
||||
|
||||
// OrderbookUpdaterRoutine fetches and updates the orderbooks for all enabled
|
||||
// currency pairs and exchanges
|
||||
func OrderbookUpdaterRoutine() {
|
||||
func OrderbookUpdaterRoutine(verbose bool) {
|
||||
log.Println("Starting orderbook updater routine.")
|
||||
var wg sync.WaitGroup
|
||||
for {
|
||||
@@ -256,11 +270,11 @@ func OrderbookUpdaterRoutine() {
|
||||
|
||||
processOrderbook := func(exch exchange.IBotExchange, c pair.CurrencyPair, assetType string) {
|
||||
result, err := exch.UpdateOrderbook(c, assetType)
|
||||
printOrderbookSummary(result, c, assetType, exchangeName, err)
|
||||
printOrderbookSummary(result, c, assetType, exchangeName, err, verbose)
|
||||
if err == nil {
|
||||
bot.comms.StageOrderbookData(exchangeName, assetType, result)
|
||||
if bot.config.Webserver.Enabled {
|
||||
relayWebsocketEvent(result, "orderbook_update", assetType, exchangeName)
|
||||
relayWebsocketEvent(result, "orderbook_update", assetType, exchangeName, verbose)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -273,7 +287,190 @@ func OrderbookUpdaterRoutine() {
|
||||
}(x, &wg)
|
||||
}
|
||||
wg.Wait()
|
||||
log.Println("All enabled currency orderbooks fetched.")
|
||||
if verbose {
|
||||
log.Println("All enabled currency orderbooks fetched.")
|
||||
}
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
}
|
||||
|
||||
// WebsocketRoutine Initial routine management system for websocket
|
||||
func WebsocketRoutine(verbose bool) {
|
||||
log.Println("Connecting exchange websocket services...")
|
||||
|
||||
for i := range bot.exchanges {
|
||||
go func(i int) {
|
||||
if verbose {
|
||||
log.Printf("Establishing websocket connection for %s",
|
||||
bot.exchanges[i].GetName())
|
||||
}
|
||||
|
||||
ws, err := bot.exchanges[i].GetWebsocket()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Data handler routine
|
||||
go WebsocketDataHandler(ws, verbose)
|
||||
|
||||
err = ws.Connect()
|
||||
if err != nil {
|
||||
switch err.Error() {
|
||||
case exchange.WebsocketNotEnabled:
|
||||
// Store in memory if enabled in future
|
||||
default:
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
}
|
||||
|
||||
var shutdowner = make(chan struct{}, 1)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Websocketshutdown shuts down the exchange routines and then shuts down
|
||||
// governing routines
|
||||
func Websocketshutdown(ws *exchange.Websocket) error {
|
||||
err := ws.Shutdown() // shutdown routines on the exchange
|
||||
if err != nil {
|
||||
log.Fatalf("routines.go error - failed to shutodwn %s", err)
|
||||
}
|
||||
|
||||
timer := time.NewTimer(5 * time.Second)
|
||||
c := make(chan struct{}, 1)
|
||||
|
||||
go func(c chan struct{}) {
|
||||
close(shutdowner)
|
||||
wg.Wait()
|
||||
c <- struct{}{}
|
||||
}(c)
|
||||
|
||||
select {
|
||||
case <-timer.C:
|
||||
return errors.New("routines.go error - failed to shutdown routines")
|
||||
|
||||
case <-c:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// streamDiversion is a diversion switch from websocket to REST or other
|
||||
// alternative feed
|
||||
func streamDiversion(ws *exchange.Websocket, verbose bool) {
|
||||
wg.Add(1)
|
||||
defer wg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-shutdowner:
|
||||
return
|
||||
|
||||
case <-ws.Connected:
|
||||
if verbose {
|
||||
log.Printf("exchange %s websocket feed connected", ws.GetName())
|
||||
}
|
||||
|
||||
case <-ws.Disconnected:
|
||||
if verbose {
|
||||
log.Printf("exchange %s websocket feed disconnected, switching to REST functionality",
|
||||
ws.GetName())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WebsocketDataHandler handles websocket data coming from a websocket feed
|
||||
// associated with an exchange
|
||||
func WebsocketDataHandler(ws *exchange.Websocket, verbose bool) {
|
||||
wg.Add(1)
|
||||
defer wg.Done()
|
||||
|
||||
go streamDiversion(ws, verbose)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-shutdowner:
|
||||
return
|
||||
|
||||
case data := <-ws.DataHandler:
|
||||
switch data.(type) {
|
||||
case string:
|
||||
switch data.(string) {
|
||||
case exchange.WebsocketNotEnabled:
|
||||
if verbose {
|
||||
log.Printf("routines.go warning - exchange %s weboscket not enabled",
|
||||
ws.GetName())
|
||||
}
|
||||
|
||||
default:
|
||||
log.Println(data.(string))
|
||||
}
|
||||
|
||||
case error:
|
||||
switch {
|
||||
case common.StringContains(data.(error).Error(), "close 1006"):
|
||||
go WebsocketReconnect(ws, verbose)
|
||||
continue
|
||||
default:
|
||||
log.Fatalf("routines.go exchange %s websocket error - %s", ws.GetName(), data)
|
||||
}
|
||||
|
||||
case exchange.TradeData:
|
||||
// Trade Data
|
||||
if verbose {
|
||||
log.Println("Websocket trades Updated: ", data.(exchange.TradeData))
|
||||
}
|
||||
|
||||
case exchange.TickerData:
|
||||
// Ticker data
|
||||
if verbose {
|
||||
log.Println("Websocket Ticker Updated: ", data.(exchange.TickerData))
|
||||
}
|
||||
case exchange.KlineData:
|
||||
// Kline data
|
||||
if verbose {
|
||||
log.Println("Websocket Kline Updated: ", data.(exchange.KlineData))
|
||||
}
|
||||
case exchange.WebsocketOrderbookUpdate:
|
||||
// Orderbook data
|
||||
if verbose {
|
||||
log.Println("Websocket Orderbook Updated:", data.(exchange.WebsocketOrderbookUpdate))
|
||||
}
|
||||
default:
|
||||
if verbose {
|
||||
log.Println("Websocket Unknown type: ", data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WebsocketReconnect tries to reconnect to a websocket stream
|
||||
func WebsocketReconnect(ws *exchange.Websocket, verbose bool) {
|
||||
if verbose {
|
||||
log.Printf("Websocket reconnection requested for %s", ws.GetName())
|
||||
}
|
||||
|
||||
err := ws.Shutdown()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
defer wg.Done()
|
||||
|
||||
ticker := time.NewTicker(3 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-shutdowner:
|
||||
return
|
||||
|
||||
case <-ticker.C:
|
||||
err = ws.Connect()
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
80
testdata/configtest.json
vendored
80
testdata/configtest.json
vendored
File diff suppressed because one or more lines are too long
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
@@ -235,6 +236,10 @@ func StartWebsocketHandler() {
|
||||
|
||||
// BroadcastWebsocketMessage meow
|
||||
func BroadcastWebsocketMessage(evt WebsocketEvent) error {
|
||||
if !wsHubStarted {
|
||||
return errors.New("websocket service not started")
|
||||
}
|
||||
|
||||
data, err := common.JSONEncode(evt)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user