mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 23:16:45 +00:00
* 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
570 lines
12 KiB
Go
570 lines
12 KiB
Go
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-/gocryptotrader/currency/pair"
|
|
exchange "github.com/thrasher-/gocryptotrader/exchanges"
|
|
"github.com/thrasher-/gocryptotrader/exchanges/orderbook"
|
|
)
|
|
|
|
const (
|
|
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"
|
|
)
|
|
|
|
var (
|
|
mtx sync.Mutex
|
|
)
|
|
|
|
// WsConnect initiates a websocket client connection
|
|
func (b *BTCC) WsConnect() error {
|
|
if !b.Websocket.IsEnabled() || !b.IsEnabled() {
|
|
return errors.New(exchange.WebsocketNotEnabled)
|
|
}
|
|
|
|
var dialer websocket.Dialer
|
|
var err error
|
|
|
|
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,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// WsHandleData handles read data
|
|
func (b *BTCC) WsHandleData() {
|
|
b.Websocket.Wg.Add(1)
|
|
defer b.Websocket.Wg.Done()
|
|
|
|
for {
|
|
select {
|
|
case <-b.Websocket.ShutdownC:
|
|
return
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
return err
|
|
}
|
|
|
|
var currencyResponse WsResponseMain
|
|
for {
|
|
_, resp, err := b.Conn.ReadMessage()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
b.Websocket.TrafficAlert <- struct{}{}
|
|
|
|
err = common.JSONDecode(resp, ¤cyResponse)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch currencyResponse.MsgType {
|
|
case msgTypeAllTickers:
|
|
var tickers WsAllTickerData
|
|
err := common.JSONDecode(currencyResponse.Data, &tickers)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var availableTickers []string
|
|
for _, tickerData := range tickers {
|
|
availableTickers = append(availableTickers, tickerData.Symbol)
|
|
}
|
|
|
|
err = b.UpdateCurrencies(availableTickers, false, true)
|
|
if err != nil {
|
|
return fmt.Errorf("%s failed to update available currencies. %s",
|
|
b.Name,
|
|
err)
|
|
}
|
|
|
|
return b.WsUnSubscribeAllTickers()
|
|
|
|
case "Heartbeat":
|
|
|
|
default:
|
|
return fmt.Errorf("btcc_websocket.go error - Updating currency pairs resp incorrect: %s",
|
|
string(resp))
|
|
}
|
|
}
|
|
}
|
|
|
|
// WsSubscribeToOrderbook subscribes to an orderbook channel
|
|
func (b *BTCC) WsSubscribeToOrderbook() error {
|
|
mtx.Lock()
|
|
defer mtx.Unlock()
|
|
|
|
for _, pair := range b.GetEnabledCurrencies() {
|
|
formattedPair := exchange.FormatExchangeCurrency(b.GetName(), pair)
|
|
err := b.Conn.WriteJSON(WsOutgoing{
|
|
Action: "SubOrderBook",
|
|
Symbol: formattedPair.String(),
|
|
Len: 100})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WsSubcribeToTicker subscribes to a ticker channel
|
|
func (b *BTCC) WsSubcribeToTicker() error {
|
|
mtx.Lock()
|
|
defer mtx.Unlock()
|
|
|
|
for _, pair := range b.GetEnabledCurrencies() {
|
|
formattedPair := exchange.FormatExchangeCurrency(b.GetName(), pair)
|
|
err := b.Conn.WriteJSON(WsOutgoing{
|
|
Action: "Subscribe",
|
|
Symbol: formattedPair.String(),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WsSubcribeToTrades subscribes to a trade channel
|
|
func (b *BTCC) WsSubcribeToTrades() error {
|
|
mtx.Lock()
|
|
defer mtx.Unlock()
|
|
|
|
for _, pair := range b.GetEnabledCurrencies() {
|
|
formattedPair := exchange.FormatExchangeCurrency(b.GetName(), pair)
|
|
err := b.Conn.WriteJSON(WsOutgoing{
|
|
Action: "GetTrades",
|
|
Symbol: formattedPair.String(),
|
|
Count: 100,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WsProcessOrderbookSnapshot processes a new orderbook snapshot
|
|
func (b *BTCC) WsProcessOrderbookSnapshot(ob WsOrderbookSnapshot) error {
|
|
var asks, bids []orderbook.Item
|
|
for _, data := range ob.List {
|
|
var newSize float64
|
|
switch data.Size.(type) {
|
|
case float64:
|
|
newSize = data.Size.(float64)
|
|
case string:
|
|
var err error
|
|
newSize, err = strconv.ParseFloat(data.Size.(string), 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if data.Side == "1" {
|
|
asks = append(asks, orderbook.Item{Price: data.Price, Amount: newSize})
|
|
continue
|
|
}
|
|
|
|
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
|
|
}
|