Files
gocryptotrader/exchanges/btse/btse_websocket.go
Ryan O'Hara-Reid eb0571cc9b exchange: binance orderbook fix (#599)
* port orderbook binance management from draft singular asset (spot) processing add additional updates to buffer management

* integrate port

* shifted burden of proof to exchange and remove repairing techniques that obfuscate issues and could caause artifacts

* WIP

* Update exchanges, update tests, update configuration so we can default off on buffer util.

* Add buffer enabled switching to all exchanges and some that are missing, default to off.

* lbtc set not aggregate books

* Addr linter issues

* EOD wip

* optimization and bug fix pass

* clean before test and benchmarking

* add testing/benchmarks to sorting/reversing functions, dropped pointer to slice as we aren't changing slice len or cap

* Add tests and removed ptr for main book as we just ammend amount

* addr exchange test issues

* ci issues

* addr glorious issues

* Addr MCB nits, fixed funding rate book for bitfinex and fixed potential panic on nil book return

* addr linter issues

* updated mistakes

* Fix more tests

* revert bypass

* Addr mcb nits

* fix zero price bug caused by exchange. Filted out bid result rather then unsubscribing. Updated orderbook to L2 so there is no aggregation.

* Allow for zero bid and ask books to be loaded and warn if found.

* remove authentication subscription conflicts as they do not have a channel ID return

* WIP - Batching outbound requests for kraken as they do not give you the partial if you subscribe to do many things.

* finalised outbound request for kraken

* filter zero value due to invalid returned data from exchange, add in max subscription amount and increased outbound batch limit

* expand to max allowed book length & fix issue where they were sending a zero length ask side when we sent a depth of zero

* Updated function comments and added in more realistic book sizing for sort cases

* change map ordering

* amalgamate maps in buffer

* Rm ln

* fix kraken linter issues

* add in buffer initialisation

* increase timout by 30seconds

* Coinbene: Add websocket orderbook length check.

* Engine: Improve switch statement for orderbook summary dissplay.

* Binance: Added tests, remove deadlock

* Exchanges: Change orderbook field -> IsFundingRate

* Orderbook Buffer: Added method to orderbookHolder

* Kraken: removed superfluous integer for sleep

* Bitmex: fixed error return

* cmd/gctcli: force 8 decimal place usage for orderbook streaming

* Kraken: Add checksum and fix bug where we were dropping returned data which was causing artifacts

* Kraken: As per orderbook documentation added in maxdepth field to update to filter depth that goes beyond current scope

* Bitfinex: Tracking down bug on margin-funding, added sequence and checksum validation websocket config on connect (WIP)

* Bitfinex: Complete implementation of checksum

* Bitfinex: Fix funding book insertion and checksum - Dropped updates and deleting items not on book are continuously occuring from stream

* Bitfinex: Fix linter issues

* Bitfinex: Fix even more linter issues.

* Bitmex: Populate orderbook base identification fields to be passed back when error occurrs

* OkGroup: Populate orderbook base identification fields to be passed back when error occurrs

* BTSE: Change string check to 'connect success' to capture multiple user successful strings

* Bitfinex: Updated handling of funding tickers

* Bitfinex: Fix undocumented alignment bug for funding rates

* Bitfinex: Updated error return with more information

* Bitfinex: Change REST fetching to Raw book to keep it in line with websocket implementation. Fix woopsy.

* Localbitcoins: Had to impose a rate limiter to stop errors, fixed return for easier error identification.

* Exchanges: Update failing tests

* LocalBitcoins: Addr nit and bumped time by 1 second for fetching books

* Kraken: Dynamically scale precision based on str return for checksum calculations

* Kraken: Add pair and asset type to validateCRC32 error reponse

* BTSE: Filter out zero amount orderbook price levels in websocket return

* Exchanges: Update orderbook functions to return orderbook base to differentiate errors.

* BTSE: Fix spelling

* Bitmex: Fix error return string

* BTSE: Add orderbook filtering function

* Coinbene: Change wording

* BTSE: Add test for filtering

* Binance: Addr nits, added in variables for buffers and worker amounts and fixed error log messages

* GolangCI: Remove excess 0

* Binance: Reduces double ups on asset and pair in errors

* Binance: Fix error checking
2021-01-04 17:19:55 +11:00

366 lines
10 KiB
Go

package btse
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/stream"
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
)
const (
btseWebsocket = "wss://ws.btse.com/spotWS"
btseWebsocketTimer = time.Second * 57
)
// WsConnect connects the websocket client
func (b *BTSE) WsConnect() error {
if !b.Websocket.IsEnabled() || !b.IsEnabled() {
return errors.New(stream.WebsocketNotEnabled)
}
var dialer websocket.Dialer
err := b.Websocket.Conn.Dial(&dialer, http.Header{})
if err != nil {
return err
}
b.Websocket.Conn.SetupPingHandler(stream.PingHandler{
MessageType: websocket.PingMessage,
Delay: btseWebsocketTimer,
})
go b.wsReadData()
if b.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
err = b.WsAuthenticate()
if err != nil {
b.Websocket.DataHandler <- err
b.Websocket.SetCanUseAuthenticatedEndpoints(false)
}
}
return nil
}
// WsAuthenticate Send an authentication message to receive auth data
func (b *BTSE) WsAuthenticate() error {
nonce := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10)
path := "/spotWS" + nonce
hmac := crypto.GetHMAC(crypto.HashSHA512_384,
[]byte((path)),
[]byte(b.API.Credentials.Secret),
)
sign := crypto.HexEncodeToString(hmac)
req := wsSub{
Operation: "authKeyExpires",
Arguments: []string{b.API.Credentials.Key, nonce, sign},
}
return b.Websocket.Conn.SendJSONMessage(req)
}
func stringToOrderStatus(status string) (order.Status, error) {
switch status {
case "ORDER_INSERTED", "TRIGGER_INSERTED":
return order.New, nil
case "ORDER_CANCELLED":
return order.Cancelled, nil
case "ORDER_FULL_TRANSACTED":
return order.Filled, nil
case "ORDER_PARTIALLY_TRANSACTED":
return order.PartiallyFilled, nil
case "TRIGGER_ACTIVATED":
return order.Active, nil
case "INSUFFICIENT_BALANCE":
return order.InsufficientBalance, nil
case "MARKET_UNAVAILABLE":
return order.MarketUnavailable, nil
default:
return order.UnknownStatus, errors.New(status + " not recognised as order status")
}
}
// wsReadData receives and passes on websocket messages for processing
func (b *BTSE) wsReadData() {
b.Websocket.Wg.Add(1)
defer b.Websocket.Wg.Done()
for {
resp := b.Websocket.Conn.ReadMessage()
if resp.Raw == nil {
return
}
err := b.wsHandleData(resp.Raw)
if err != nil {
b.Websocket.DataHandler <- err
}
}
}
func (b *BTSE) wsHandleData(respRaw []byte) error {
type Result map[string]interface{}
var result Result
err := json.Unmarshal(respRaw, &result)
if err != nil {
if strings.Contains(string(respRaw), "connect success") ||
strings.Contains(string(respRaw), "authenticated successfully") {
return nil
} else if strings.Contains(string(respRaw), "AUTHENTICATE ERROR") {
b.Websocket.SetCanUseAuthenticatedEndpoints(false)
return errors.New("authentication failure")
}
return err
}
switch {
case result["topic"] == "notificationApi":
var notification wsNotification
err = json.Unmarshal(respRaw, &notification)
if err != nil {
return err
}
for i := range notification.Data {
var oType order.Type
var oSide order.Side
var oStatus order.Status
oType, err = order.StringToOrderType(notification.Data[i].Type)
if err != nil {
b.Websocket.DataHandler <- order.ClassificationError{
Exchange: b.Name,
OrderID: notification.Data[i].OrderID,
Err: err,
}
}
oSide, err = order.StringToOrderSide(notification.Data[i].OrderMode)
if err != nil {
b.Websocket.DataHandler <- order.ClassificationError{
Exchange: b.Name,
OrderID: notification.Data[i].OrderID,
Err: err,
}
}
oStatus, err = stringToOrderStatus(notification.Data[i].Status)
if err != nil {
b.Websocket.DataHandler <- order.ClassificationError{
Exchange: b.Name,
OrderID: notification.Data[i].OrderID,
Err: err,
}
}
var p currency.Pair
p, err = currency.NewPairFromString(notification.Data[i].Symbol)
if err != nil {
return err
}
var a asset.Item
a, err = b.GetPairAssetType(p)
if err != nil {
return err
}
b.Websocket.DataHandler <- &order.Detail{
Price: notification.Data[i].Price,
Amount: notification.Data[i].Size,
TriggerPrice: notification.Data[i].TriggerPrice,
Exchange: b.Name,
ID: notification.Data[i].OrderID,
Type: oType,
Side: oSide,
Status: oStatus,
AssetType: a,
Date: time.Unix(0, notification.Data[i].Timestamp*int64(time.Millisecond)),
Pair: p,
}
}
case strings.Contains(result["topic"].(string), "tradeHistory"):
if !b.IsSaveTradeDataEnabled() {
return nil
}
var tradeHistory wsTradeHistory
err = json.Unmarshal(respRaw, &tradeHistory)
if err != nil {
return err
}
var trades []trade.Data
for x := range tradeHistory.Data {
side := order.Buy
if tradeHistory.Data[x].Gain == -1 {
side = order.Sell
}
var p currency.Pair
p, err = currency.NewPairFromString(strings.Replace(tradeHistory.Topic,
"tradeHistory:",
"",
1))
if err != nil {
return err
}
var a asset.Item
a, err = b.GetPairAssetType(p)
if err != nil {
return err
}
trades = append(trades, trade.Data{
Timestamp: time.Unix(0, tradeHistory.Data[x].TransactionTime*int64(time.Millisecond)),
CurrencyPair: p,
AssetType: a,
Exchange: b.Name,
Price: tradeHistory.Data[x].Price,
Amount: tradeHistory.Data[x].Amount,
Side: side,
TID: strconv.FormatInt(tradeHistory.Data[x].ID, 10),
})
}
return trade.AddTradesToBuffer(b.Name, trades...)
case strings.Contains(result["topic"].(string), "orderBookL2Api"):
var t wsOrderBook
err = json.Unmarshal(respRaw, &t)
if err != nil {
return err
}
var newOB orderbook.Base
var price, amount float64
for i := range t.Data.SellQuote {
p := strings.Replace(t.Data.SellQuote[i].Price, ",", "", -1)
price, err = strconv.ParseFloat(p, 64)
if err != nil {
return err
}
a := strings.Replace(t.Data.SellQuote[i].Size, ",", "", -1)
amount, err = strconv.ParseFloat(a, 64)
if err != nil {
return err
}
if b.orderbookFilter(price, amount) {
continue
}
newOB.Asks = append(newOB.Asks, orderbook.Item{
Price: price,
Amount: amount,
})
}
for j := range t.Data.BuyQuote {
p := strings.Replace(t.Data.BuyQuote[j].Price, ",", "", -1)
price, err = strconv.ParseFloat(p, 64)
if err != nil {
return err
}
a := strings.Replace(t.Data.BuyQuote[j].Size, ",", "", -1)
amount, err = strconv.ParseFloat(a, 64)
if err != nil {
return err
}
if b.orderbookFilter(price, amount) {
continue
}
newOB.Bids = append(newOB.Bids, orderbook.Item{
Price: price,
Amount: amount,
})
}
p, err := currency.NewPairFromString(t.Topic[strings.Index(t.Topic, ":")+1 : strings.Index(t.Topic, currency.UnderscoreDelimiter)])
if err != nil {
return err
}
var a asset.Item
a, err = b.GetPairAssetType(p)
if err != nil {
return err
}
newOB.Pair = p
newOB.AssetType = a
newOB.ExchangeName = b.Name
orderbook.Reverse(newOB.Asks) // Reverse asks for correct alignment
err = b.Websocket.Orderbook.LoadSnapshot(&newOB)
if err != nil {
return err
}
default:
b.Websocket.DataHandler <- stream.UnhandledMessageWarning{Message: b.Name + stream.UnhandledMessage + string(respRaw)}
return nil
}
return nil
}
// orderbookFilter is needed on book levels from this exchange as their data
// is incorrect
func (b *BTSE) orderbookFilter(price, amount float64) bool {
// Amount filtering occurs when the amount exceeds the decimal returned.
// e.g. {"price":"1.37","size":"0.00"} currency: SFI-ETH
// Opted to not round up to 0.01 as this might skew calculations
// more than removing from the books completely.
// Price filtering occurs when we are deep in the bid book and there are
// prices that are less than 4 decimal places
// e.g. {"price":"0.0000","size":"14219"} currency: TRX-PAX
// We cannot load a zero price and this will ruin calculations
return price == 0 || amount == 0
}
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
func (b *BTSE) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
var channels = []string{"orderBookL2Api:%s_0", "tradeHistory:%s"}
pairs, err := b.GetEnabledPairs(asset.Spot)
if err != nil {
return nil, err
}
var subscriptions []stream.ChannelSubscription
if b.Websocket.CanUseAuthenticatedEndpoints() {
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: "notificationApi",
})
}
for i := range channels {
for j := range pairs {
subscriptions = append(subscriptions, stream.ChannelSubscription{
Channel: fmt.Sprintf(channels[i], pairs[j]),
Currency: pairs[j],
Asset: asset.Spot,
})
}
}
return subscriptions, nil
}
// Subscribe sends a websocket message to receive data from the channel
func (b *BTSE) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
var sub wsSub
sub.Operation = "subscribe"
for i := range channelsToSubscribe {
sub.Arguments = append(sub.Arguments, channelsToSubscribe[i].Channel)
}
err := b.Websocket.Conn.SendJSONMessage(sub)
if err != nil {
return err
}
b.Websocket.AddSuccessfulSubscriptions(channelsToSubscribe...)
return nil
}
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (b *BTSE) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
var unSub wsSub
unSub.Operation = "unsubscribe"
for i := range channelsToUnsubscribe {
unSub.Arguments = append(unSub.Arguments,
channelsToUnsubscribe[i].Channel)
}
err := b.Websocket.Conn.SendJSONMessage(unSub)
if err != nil {
return err
}
b.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe...)
return nil
}