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:
Ryan O'Hara-Reid
2018-10-24 14:22:41 +11:00
committed by Adrian Gallagher
parent 7315e6604c
commit d3c2800fe0
99 changed files with 6515 additions and 3031 deletions

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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")
}

View File

@@ -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)
}
}
}

View File

@@ -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 {

View File

@@ -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")
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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)
}
}
}

View File

@@ -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")
}

View File

@@ -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
}

View File

@@ -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)
// }
// }

View File

@@ -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
}

View File

@@ -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)
// }
// }

View File

@@ -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)
}
}
}

View File

@@ -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")
}

View File

@@ -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)
}
}
}

View File

@@ -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")
}

View File

@@ -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)
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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
}

View File

@@ -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)
}
}
}

View File

@@ -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")
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}
}
}

View File

@@ -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")
}

View File

@@ -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")
}

View File

@@ -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&params=", 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)
}

View File

@@ -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)
// }
// }

View File

@@ -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"`
}

View File

@@ -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, &currencyResponse)
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
}

View File

@@ -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
}

View File

@@ -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)
}
}
}

View File

@@ -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")
}

View File

@@ -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)
}
}
}

View File

@@ -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"`
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}
}
}

View File

@@ -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")
}

View File

@@ -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"`
}

View File

@@ -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")
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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)

View 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
}

View 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)
}
}

View File

@@ -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)
}
}
}

View File

@@ -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")
}

View File

@@ -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)
}
}
}

View File

@@ -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")
}

View File

@@ -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)
}
}
}

View File

@@ -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")
}

View File

@@ -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)
}
}
}

View File

@@ -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"`
}

View File

@@ -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
}

View File

@@ -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)
}
}
}

View File

@@ -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"`
}
}

View File

@@ -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
}

View File

@@ -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)
}
}
}

View File

@@ -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")
}

View File

@@ -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)
}
}
}

View File

@@ -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")
}

View File

@@ -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)
}
}
}

View File

@@ -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")
}

View File

@@ -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)
}
}
}

View File

@@ -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")
}

View File

@@ -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)
}
}
}

View File

@@ -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")
}

View File

@@ -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)
}
}
}

View File

@@ -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")
}

View File

@@ -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)
}
}
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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"`

View File

@@ -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"`
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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)
}
}
}

View File

@@ -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
}

View File

@@ -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",
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}
}
}

View File

@@ -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")
}

View File

@@ -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)
}
}
}

View File

@@ -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")
}

View File

@@ -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)
}
}
}

View File

@@ -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
View File

@@ -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()
}

View File

@@ -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
}
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -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