mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-06-04 23:16:54 +00:00
* Renames func. Creates new func to setup pinghander either via gorilla style or our own
* Cleans up all ping pong handlers.......
* Clears up issues, makes naming a bit better
* Adds tests
* Adds ping support to binance
* Cleans up ping pongs and adds a comment
* Cleans up waitgroup stuff.
* DISCREETLY cleans up woeful function
* Fixes Kraken ping message type. Removes unnecessary test property. Adds `if err == websocket.ErrCloseSent {` to ping func
* +1 for +v
536 lines
15 KiB
Go
536 lines
15 KiB
Go
package poloniex
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
"github.com/thrasher-corp/gocryptotrader/common/crypto"
|
|
"github.com/thrasher-corp/gocryptotrader/currency"
|
|
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wsorderbook"
|
|
log "github.com/thrasher-corp/gocryptotrader/logger"
|
|
)
|
|
|
|
const (
|
|
poloniexWebsocketAddress = "wss://api2.poloniex.com"
|
|
wsAccountNotificationID = 1000
|
|
wsTickerDataID = 1002
|
|
ws24HourExchangeVolumeID = 1003
|
|
wsHeartbeat = 1010
|
|
delimiterUnderscore = "_"
|
|
)
|
|
|
|
var (
|
|
// currencyIDMap stores a map of currencies associated with their ID
|
|
currencyIDMap map[int]string
|
|
)
|
|
|
|
// WsConnect initiates a websocket connection
|
|
func (p *Poloniex) WsConnect() error {
|
|
if !p.Websocket.IsEnabled() || !p.IsEnabled() {
|
|
return errors.New(wshandler.WebsocketNotEnabled)
|
|
}
|
|
var dialer websocket.Dialer
|
|
err := p.WebsocketConn.Dial(&dialer, http.Header{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if currencyIDMap == nil {
|
|
currencyIDMap = make(map[int]string)
|
|
resp, err := p.GetTicker()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for k, v := range resp {
|
|
currencyIDMap[v.ID] = k
|
|
}
|
|
}
|
|
|
|
go p.WsHandleData()
|
|
p.GenerateDefaultSubscriptions()
|
|
|
|
return nil
|
|
}
|
|
|
|
func getWSDataType(data interface{}) string {
|
|
subData := data.([]interface{})
|
|
dataType := subData[0].(string)
|
|
return dataType
|
|
}
|
|
|
|
func checkSubscriptionSuccess(data []interface{}) bool {
|
|
return data[1].(float64) == 1
|
|
}
|
|
|
|
// WsHandleData handles data from the websocket connection
|
|
func (p *Poloniex) WsHandleData() {
|
|
p.Websocket.Wg.Add(1)
|
|
|
|
defer func() {
|
|
p.Websocket.Wg.Done()
|
|
}()
|
|
|
|
for {
|
|
select {
|
|
case <-p.Websocket.ShutdownC:
|
|
return
|
|
|
|
default:
|
|
resp, err := p.WebsocketConn.ReadMessage()
|
|
if err != nil {
|
|
p.Websocket.ReadMessageErrors <- err
|
|
return
|
|
}
|
|
p.Websocket.TrafficAlert <- struct{}{}
|
|
var result interface{}
|
|
err = json.Unmarshal(resp.Raw, &result)
|
|
if err != nil {
|
|
p.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
switch data := result.(type) {
|
|
case map[string]interface{}:
|
|
// subscription error
|
|
p.Websocket.DataHandler <- errors.New(data["error"].(string))
|
|
case []interface{}:
|
|
chanID := int(data[0].(float64))
|
|
if len(data) == 2 && chanID != wsHeartbeat {
|
|
if checkSubscriptionSuccess(data) {
|
|
if p.Verbose {
|
|
log.Debugf(log.ExchangeSys,
|
|
"%s websocket subscribed to channel successfully. %d",
|
|
p.Name,
|
|
chanID)
|
|
}
|
|
} else {
|
|
p.Websocket.DataHandler <- fmt.Errorf("%s websocket subscription to channel failed. %d",
|
|
p.Name,
|
|
chanID)
|
|
}
|
|
continue
|
|
}
|
|
|
|
switch chanID {
|
|
case wsAccountNotificationID:
|
|
p.wsHandleAccountData(data[2].([][]interface{}))
|
|
case wsTickerDataID:
|
|
p.wsHandleTickerData(data)
|
|
case ws24HourExchangeVolumeID:
|
|
case wsHeartbeat:
|
|
default:
|
|
if len(data) > 2 {
|
|
subData := data[2].([]interface{})
|
|
|
|
for x := range subData {
|
|
dataL2 := subData[x]
|
|
dataL3 := dataL2.([]interface{})
|
|
|
|
switch getWSDataType(dataL2) {
|
|
case "i":
|
|
dataL3map := dataL3[1].(map[string]interface{})
|
|
currencyPair, ok := dataL3map["currencyPair"].(string)
|
|
if !ok {
|
|
p.Websocket.DataHandler <- fmt.Errorf("%s websocket could not find currency pair in map",
|
|
p.Name)
|
|
continue
|
|
}
|
|
|
|
orderbookData, ok := dataL3map["orderBook"].([]interface{})
|
|
if !ok {
|
|
p.Websocket.DataHandler <- fmt.Errorf("%s websocket could not find orderbook data in map",
|
|
p.Name)
|
|
continue
|
|
}
|
|
|
|
err = p.WsProcessOrderbookSnapshot(orderbookData,
|
|
currencyPair)
|
|
if err != nil {
|
|
p.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
|
|
p.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{
|
|
Exchange: p.Name,
|
|
Asset: asset.Spot,
|
|
Pair: currency.NewPairFromString(currencyPair),
|
|
}
|
|
case "o":
|
|
currencyPair := currencyIDMap[chanID]
|
|
err = p.WsProcessOrderbookUpdate(int64(data[1].(float64)),
|
|
dataL3,
|
|
currencyPair)
|
|
if err != nil {
|
|
p.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
|
|
p.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{
|
|
Exchange: p.Name,
|
|
Asset: asset.Spot,
|
|
Pair: currency.NewPairFromString(currencyPair),
|
|
}
|
|
case "t":
|
|
currencyPair := currencyIDMap[chanID]
|
|
var trade WsTrade
|
|
trade.Symbol = currencyIDMap[chanID]
|
|
trade.TradeID, _ = strconv.ParseInt(dataL3[1].(string), 10, 64)
|
|
// 1 for buy 0 for sell
|
|
side := order.Buy
|
|
if dataL3[2].(float64) != 1 {
|
|
side = order.Sell
|
|
}
|
|
trade.Side = side.Lower()
|
|
trade.Volume, err = strconv.ParseFloat(dataL3[3].(string), 64)
|
|
if err != nil {
|
|
p.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
trade.Price, err = strconv.ParseFloat(dataL3[4].(string), 64)
|
|
if err != nil {
|
|
p.Websocket.DataHandler <- err
|
|
continue
|
|
}
|
|
trade.Timestamp = int64(dataL3[5].(float64))
|
|
|
|
p.Websocket.DataHandler <- wshandler.TradeData{
|
|
Timestamp: time.Unix(trade.Timestamp, 0),
|
|
CurrencyPair: currency.NewPairFromString(currencyPair),
|
|
Side: trade.Side,
|
|
Amount: trade.Volume,
|
|
Price: trade.Price,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *Poloniex) wsHandleTickerData(data []interface{}) {
|
|
tickerData := data[2].([]interface{})
|
|
var t WsTicker
|
|
currencyPair := currency.NewPairDelimiter(currencyIDMap[int(tickerData[0].(float64))], delimiterUnderscore)
|
|
if !p.GetEnabledPairs(asset.Spot).Contains(currencyPair, true) {
|
|
return
|
|
}
|
|
|
|
var err error
|
|
t.LastPrice, err = strconv.ParseFloat(tickerData[1].(string), 64)
|
|
if err != nil {
|
|
p.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
|
|
t.LowestAsk, err = strconv.ParseFloat(tickerData[2].(string), 64)
|
|
if err != nil {
|
|
p.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
|
|
t.HighestBid, err = strconv.ParseFloat(tickerData[3].(string), 64)
|
|
if err != nil {
|
|
p.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
|
|
t.PercentageChange, err = strconv.ParseFloat(tickerData[4].(string), 64)
|
|
if err != nil {
|
|
p.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
|
|
t.BaseCurrencyVolume24H, err = strconv.ParseFloat(tickerData[5].(string), 64)
|
|
if err != nil {
|
|
p.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
|
|
t.QuoteCurrencyVolume24H, err = strconv.ParseFloat(tickerData[6].(string), 64)
|
|
if err != nil {
|
|
p.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
|
|
t.IsFrozen = tickerData[7].(float64) == 1
|
|
t.HighestTradeIn24H, err = strconv.ParseFloat(tickerData[8].(string), 64)
|
|
if err != nil {
|
|
p.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
|
|
t.LowestTradePrice24H, err = strconv.ParseFloat(tickerData[9].(string), 64)
|
|
if err != nil {
|
|
p.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
|
|
p.Websocket.DataHandler <- &ticker.Price{
|
|
ExchangeName: p.Name,
|
|
Volume: t.BaseCurrencyVolume24H,
|
|
QuoteVolume: t.QuoteCurrencyVolume24H,
|
|
High: t.HighestBid,
|
|
Low: t.LowestAsk,
|
|
Bid: t.HighestBid,
|
|
Ask: t.LowestAsk,
|
|
Last: t.LastPrice,
|
|
AssetType: asset.Spot,
|
|
Pair: currencyPair,
|
|
}
|
|
}
|
|
|
|
// wsHandleAccountData Parses account data and sends to datahandler
|
|
func (p *Poloniex) wsHandleAccountData(accountData [][]interface{}) {
|
|
for i := range accountData {
|
|
switch accountData[i][0].(string) {
|
|
case "b":
|
|
amount, err := strconv.ParseFloat(accountData[i][3].(string), 64)
|
|
if err != nil {
|
|
p.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
|
|
response := WsAccountBalanceUpdateResponse{
|
|
currencyID: accountData[i][1].(float64),
|
|
wallet: accountData[i][2].(string),
|
|
amount: amount,
|
|
}
|
|
p.Websocket.DataHandler <- response
|
|
case "n":
|
|
timeParse, err := time.Parse("2006-01-02 15:04:05", accountData[i][6].(string))
|
|
if err != nil {
|
|
p.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
|
|
rate, err := strconv.ParseFloat(accountData[i][4].(string), 64)
|
|
if err != nil {
|
|
p.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
|
|
amount, err := strconv.ParseFloat(accountData[i][5].(string), 64)
|
|
if err != nil {
|
|
p.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
|
|
response := WsNewLimitOrderResponse{
|
|
currencyID: accountData[i][1].(float64),
|
|
orderNumber: accountData[i][2].(float64),
|
|
orderType: accountData[i][3].(float64),
|
|
rate: rate,
|
|
amount: amount,
|
|
date: timeParse,
|
|
}
|
|
p.Websocket.DataHandler <- response
|
|
case "o":
|
|
response := WsOrderUpdateResponse{
|
|
OrderNumber: accountData[i][1].(float64),
|
|
NewAmount: accountData[i][2].(string),
|
|
}
|
|
p.Websocket.DataHandler <- response
|
|
case "t":
|
|
timeParse, err := time.Parse("2006-01-02 15:04:05", accountData[i][8].(string))
|
|
if err != nil {
|
|
p.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
|
|
rate, err := strconv.ParseFloat(accountData[i][2].(string), 64)
|
|
if err != nil {
|
|
p.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
|
|
amount, err := strconv.ParseFloat(accountData[i][3].(string), 64)
|
|
if err != nil {
|
|
p.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
|
|
feeMultiplier, err := strconv.ParseFloat(accountData[i][4].(string), 64)
|
|
if err != nil {
|
|
p.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
|
|
totalFee, err := strconv.ParseFloat(accountData[i][7].(string), 64)
|
|
if err != nil {
|
|
p.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
|
|
response := WsTradeNotificationResponse{
|
|
TradeID: accountData[i][1].(float64),
|
|
Rate: rate,
|
|
Amount: amount,
|
|
FeeMultiplier: feeMultiplier,
|
|
FundingType: accountData[i][5].(float64),
|
|
OrderNumber: accountData[i][6].(float64),
|
|
TotalFee: totalFee,
|
|
Date: timeParse,
|
|
}
|
|
p.Websocket.DataHandler <- response
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 = asset.Spot
|
|
newOrderBook.Pair = currency.NewPairFromString(symbol)
|
|
newOrderBook.ExchangeName = p.Name
|
|
|
|
return p.Websocket.Orderbook.LoadSnapshot(&newOrderBook)
|
|
}
|
|
|
|
// WsProcessOrderbookUpdate processes new orderbook updates
|
|
func (p *Poloniex) WsProcessOrderbookUpdate(sequenceNumber int64, target []interface{}, symbol string) error {
|
|
cP := currency.NewPairFromString(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
|
|
}
|
|
update := &wsorderbook.WebsocketOrderbookUpdate{
|
|
Pair: cP,
|
|
Asset: asset.Spot,
|
|
UpdateID: sequenceNumber,
|
|
}
|
|
if target[1].(float64) == 1 {
|
|
update.Bids = []orderbook.Item{{Price: price, Amount: volume}}
|
|
} else {
|
|
update.Asks = []orderbook.Item{{Price: price, Amount: volume}}
|
|
}
|
|
return p.Websocket.Orderbook.Update(update)
|
|
}
|
|
|
|
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
|
|
func (p *Poloniex) GenerateDefaultSubscriptions() {
|
|
var subscriptions []wshandler.WebsocketChannelSubscription
|
|
subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{
|
|
Channel: strconv.FormatInt(wsTickerDataID, 10),
|
|
})
|
|
|
|
if p.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
|
|
subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{
|
|
Channel: strconv.FormatInt(wsAccountNotificationID, 10),
|
|
})
|
|
}
|
|
|
|
enabledCurrencies := p.GetEnabledPairs(asset.Spot)
|
|
for j := range enabledCurrencies {
|
|
enabledCurrencies[j].Delimiter = delimiterUnderscore
|
|
subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{
|
|
Channel: "orderbook",
|
|
Currency: enabledCurrencies[j],
|
|
})
|
|
}
|
|
p.Websocket.SubscribeToChannels(subscriptions)
|
|
}
|
|
|
|
// Subscribe sends a websocket message to receive data from the channel
|
|
func (p *Poloniex) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error {
|
|
subscriptionRequest := WsCommand{
|
|
Command: "subscribe",
|
|
}
|
|
switch {
|
|
case strings.EqualFold(strconv.FormatInt(wsAccountNotificationID, 10), channelToSubscribe.Channel):
|
|
return p.wsSendAuthorisedCommand("subscribe")
|
|
case strings.EqualFold(strconv.FormatInt(wsTickerDataID, 10), channelToSubscribe.Channel):
|
|
subscriptionRequest.Channel = wsTickerDataID
|
|
default:
|
|
subscriptionRequest.Channel = channelToSubscribe.Currency.String()
|
|
}
|
|
return p.WebsocketConn.SendJSONMessage(subscriptionRequest)
|
|
}
|
|
|
|
// Unsubscribe sends a websocket message to stop receiving data from the channel
|
|
func (p *Poloniex) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error {
|
|
unsubscriptionRequest := WsCommand{
|
|
Command: "unsubscribe",
|
|
}
|
|
switch {
|
|
case strings.EqualFold(strconv.FormatInt(wsAccountNotificationID, 10), channelToSubscribe.Channel):
|
|
return p.wsSendAuthorisedCommand("unsubscribe")
|
|
case strings.EqualFold(strconv.FormatInt(wsTickerDataID, 10), channelToSubscribe.Channel):
|
|
unsubscriptionRequest.Channel = wsTickerDataID
|
|
default:
|
|
unsubscriptionRequest.Channel = channelToSubscribe.Currency.String()
|
|
}
|
|
return p.WebsocketConn.SendJSONMessage(unsubscriptionRequest)
|
|
}
|
|
|
|
func (p *Poloniex) wsSendAuthorisedCommand(command string) error {
|
|
nonce := fmt.Sprintf("nonce=%v", time.Now().UnixNano())
|
|
hmac := crypto.GetHMAC(crypto.HashSHA512, []byte(nonce), []byte(p.API.Credentials.Secret))
|
|
request := WsAuthorisationRequest{
|
|
Command: command,
|
|
Channel: 1000,
|
|
Sign: crypto.HexEncodeToString(hmac),
|
|
Key: p.API.Credentials.Key,
|
|
Payload: nonce,
|
|
}
|
|
return p.WebsocketConn.SendJSONMessage(request)
|
|
}
|