mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-18 15:10:03 +00:00
* orderbook/buffer: data integrity and resubscription pass * btcmarkets: REMOVE THAT LIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIINE!!!!!!!!!!!!!!!!! * buffer: reinstate publish, refaactor, invalidate more and comments * buffer/orderbook: improve update and snapshot performance. Move Update type to orderbook package to util. pointer through entire function calls. (cleanup). Change action string to uint8 for easier comparison. Add parsing helper. Update current test benchmark comments. * dispatch: change publish func to variadic id param * dispatch: remove sender receiver wait time as this adds overhead and complexity. update tests. * dispatch: don't create pointers for every job container * rpcserver: fix assertion issues with data publishing change * linter: fixes * glorious: nits addr * depth: change validation handling to incorporate and store err * linter: fix more issues * dispatch: fix race * travis: update before fetching * depth: wrap and return wrapped error in invalidate call and fix tests * btcmarkets: fix commenting * workflow: check * workflow: check * orderbook: check error * buffer/depth: return invalidation error and fix tests * gctcli: display errors on orderbook streams * buffer: remove unused types * orderbook/bitmex: shift function to bitmex * orderbook: Add specific comments to unexported functions that don't have locking require locking. * orderbook: restrict published data functionality to orderbook.Outbound interface * common: add assertion failure helper for error * dispatch: remove atomics, add mutex protection, remove add/remove worker, redo main tests * dispatch: export function * engine: revert and change sub logger to manager * engine: remove old test * dispatch: add common variable ;) * btcmarket: don't overflow int in tests on 32bit systems * ci: force 1.17.7 usage for go * Revert "ci: force 1.17.7 usage for go" This reverts commit af2f95563bf218cf2b9f36a9fcf3258e2c6a2d91. * golangci: bump version add and remove linter items * Revert "golangci: bump version add and remove linter items" This reverts commit 3c98bffc9d030e39faca0387ea40c151df2ab06b. * dispatch: remove unsused mutex from mux * order: slight optimizations * nits: glorious * dispatch: fix regression on uuid generation and input inline with master * linter: fix * linter: fix * glorious: nit - rm slice segration * account: fix test after merge * coinbasepro: revert change * account: close channel instead of needing a receiver, push alert in routine to prepare for waiter. Co-authored-by: Ryan O'Hara-Reid <ryan.oharareid@thrasher.io>
1962 lines
59 KiB
Go
1962 lines
59 KiB
Go
package bitfinex
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"hash/crc32"
|
|
"net/http"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
"github.com/thrasher-corp/gocryptotrader/common"
|
|
"github.com/thrasher-corp/gocryptotrader/common/convert"
|
|
"github.com/thrasher-corp/gocryptotrader/common/crypto"
|
|
"github.com/thrasher-corp/gocryptotrader/currency"
|
|
"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/ticker"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
|
|
"github.com/thrasher-corp/gocryptotrader/log"
|
|
)
|
|
|
|
var comms = make(chan stream.Response)
|
|
|
|
type checksum struct {
|
|
Token int
|
|
Sequence int64
|
|
}
|
|
|
|
// checksumStore quick global for now
|
|
var checksumStore = make(map[int]*checksum)
|
|
var cMtx sync.Mutex
|
|
|
|
// WsConnect starts a new websocket connection
|
|
func (b *Bitfinex) 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 fmt.Errorf("%v unable to connect to Websocket. Error: %s",
|
|
b.Name,
|
|
err)
|
|
}
|
|
|
|
b.Websocket.Wg.Add(1)
|
|
go b.wsReadData(b.Websocket.Conn)
|
|
|
|
if b.Websocket.CanUseAuthenticatedEndpoints() {
|
|
err = b.Websocket.AuthConn.Dial(&dialer, http.Header{})
|
|
if err != nil {
|
|
log.Errorf(log.ExchangeSys,
|
|
"%v unable to connect to authenticated Websocket. Error: %s",
|
|
b.Name,
|
|
err)
|
|
b.Websocket.SetCanUseAuthenticatedEndpoints(false)
|
|
}
|
|
b.Websocket.Wg.Add(1)
|
|
go b.wsReadData(b.Websocket.AuthConn)
|
|
err = b.WsSendAuth(context.TODO())
|
|
if err != nil {
|
|
log.Errorf(log.ExchangeSys,
|
|
"%v - authentication failed: %v\n",
|
|
b.Name,
|
|
err)
|
|
b.Websocket.SetCanUseAuthenticatedEndpoints(false)
|
|
}
|
|
}
|
|
|
|
b.Websocket.Wg.Add(1)
|
|
go b.WsDataHandler()
|
|
return nil
|
|
}
|
|
|
|
// wsReadData receives and passes on websocket messages for processing
|
|
func (b *Bitfinex) wsReadData(ws stream.Connection) {
|
|
defer b.Websocket.Wg.Done()
|
|
for {
|
|
resp := ws.ReadMessage()
|
|
if resp.Raw == nil {
|
|
return
|
|
}
|
|
comms <- resp
|
|
}
|
|
}
|
|
|
|
// WsDataHandler handles data from wsReadData
|
|
func (b *Bitfinex) WsDataHandler() {
|
|
defer b.Websocket.Wg.Done()
|
|
for {
|
|
select {
|
|
case <-b.Websocket.ShutdownC:
|
|
select {
|
|
case resp := <-comms:
|
|
err := b.wsHandleData(resp.Raw)
|
|
if err != nil {
|
|
select {
|
|
case b.Websocket.DataHandler <- err:
|
|
default:
|
|
log.Errorf(log.WebsocketMgr,
|
|
"%s websocket handle data error: %v",
|
|
b.Name,
|
|
err)
|
|
}
|
|
}
|
|
default:
|
|
}
|
|
return
|
|
case resp := <-comms:
|
|
if resp.Type != websocket.TextMessage {
|
|
continue
|
|
}
|
|
err := b.wsHandleData(resp.Raw)
|
|
if err != nil {
|
|
b.Websocket.DataHandler <- err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (b *Bitfinex) wsHandleData(respRaw []byte) error {
|
|
var result interface{}
|
|
err := json.Unmarshal(respRaw, &result)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch d := result.(type) {
|
|
case map[string]interface{}:
|
|
event := d["event"]
|
|
switch event {
|
|
case "subscribed":
|
|
chanID, ok := d["chanId"].(float64)
|
|
if !ok {
|
|
return errors.New("unable to type assert chanId")
|
|
}
|
|
channel, ok := d["channel"].(string)
|
|
if !ok {
|
|
return errors.New("unable to type assert channel")
|
|
}
|
|
if symbol, ok := d["symbol"].(string); ok {
|
|
b.WsAddSubscriptionChannel(int(chanID),
|
|
channel,
|
|
symbol,
|
|
)
|
|
} else if key, ok := d["key"].(string); ok {
|
|
// Capture trading subscriptions
|
|
if contents := strings.Split(key, ":"); len(contents) > 3 {
|
|
// Edge case to parse margin strings.
|
|
// map[chanId:139136 channel:candles event:subscribed key:trade:1m:tXAUTF0:USTF0]
|
|
if contents[2][0] == 't' {
|
|
key = contents[2] + ":" + contents[3]
|
|
}
|
|
}
|
|
b.WsAddSubscriptionChannel(int(chanID),
|
|
channel,
|
|
key,
|
|
)
|
|
}
|
|
case "auth":
|
|
status, ok := d["status"].(string)
|
|
if !ok {
|
|
return errors.New("unable to type assert status")
|
|
}
|
|
if status == "OK" {
|
|
b.Websocket.DataHandler <- d
|
|
b.WsAddSubscriptionChannel(0, "account", "N/A")
|
|
} else if status == "fail" {
|
|
if code, ok := d["code"].(string); ok {
|
|
return fmt.Errorf("websocket unable to AUTH. Error code: %s",
|
|
code)
|
|
}
|
|
return errors.New("websocket unable to auth")
|
|
}
|
|
}
|
|
case []interface{}:
|
|
chanF, ok := d[0].(float64)
|
|
if !ok {
|
|
return errors.New("channel ID type assertion failure")
|
|
}
|
|
|
|
chanID := int(chanF)
|
|
var datum string
|
|
if datum, ok = d[1].(string); ok {
|
|
// Capturing heart beat
|
|
if datum == "hb" {
|
|
return nil
|
|
}
|
|
|
|
// Capturing checksum and storing value
|
|
if datum == "cs" {
|
|
var tokenF float64
|
|
tokenF, ok = d[2].(float64)
|
|
if !ok {
|
|
return errors.New("checksum token type assertion failure")
|
|
}
|
|
var seqNoF float64
|
|
seqNoF, ok = d[3].(float64)
|
|
if !ok {
|
|
return errors.New("sequence number type assertion failure")
|
|
}
|
|
|
|
cMtx.Lock()
|
|
checksumStore[chanID] = &checksum{
|
|
Token: int(tokenF),
|
|
Sequence: int64(seqNoF),
|
|
}
|
|
cMtx.Unlock()
|
|
return nil
|
|
}
|
|
}
|
|
|
|
chanInfo, ok := b.WebsocketSubdChannels[chanID]
|
|
if !ok && chanID != 0 {
|
|
return fmt.Errorf("unable to locate chanID: %d",
|
|
chanID)
|
|
}
|
|
|
|
var chanAsset = asset.Spot
|
|
var pair currency.Pair
|
|
pairInfo := strings.Split(chanInfo.Pair, ":")
|
|
switch {
|
|
case len(pairInfo) >= 3:
|
|
newPair := pairInfo[2]
|
|
if newPair[0] == 'f' {
|
|
chanAsset = asset.MarginFunding
|
|
}
|
|
|
|
pair, err = currency.NewPairFromString(newPair[1:])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case len(pairInfo) == 1:
|
|
newPair := pairInfo[0]
|
|
if newPair[0] == 'f' {
|
|
chanAsset = asset.MarginFunding
|
|
}
|
|
|
|
pair, err = currency.NewPairFromString(newPair[1:])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case chanInfo.Pair != "":
|
|
if strings.Contains(chanInfo.Pair, ":") {
|
|
chanAsset = asset.Margin
|
|
}
|
|
|
|
pair, err = currency.NewPairFromString(chanInfo.Pair[1:])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
switch chanInfo.Channel {
|
|
case wsBook:
|
|
var newOrderbook []WebsocketBook
|
|
obSnapBundle, ok := d[1].([]interface{})
|
|
if !ok {
|
|
return errors.New("orderbook interface cast failed")
|
|
}
|
|
if len(obSnapBundle) == 0 {
|
|
return errors.New("no data within orderbook snapshot")
|
|
}
|
|
|
|
sequenceNo, ok := d[2].(float64)
|
|
if !ok {
|
|
return errors.New("type assertion failure")
|
|
}
|
|
|
|
var fundingRate bool
|
|
switch id := obSnapBundle[0].(type) {
|
|
case []interface{}:
|
|
for i := range obSnapBundle {
|
|
data, ok := obSnapBundle[i].([]interface{})
|
|
if !ok {
|
|
return errors.New("type assertion failed for orderbok item data")
|
|
}
|
|
id, okAssert := data[0].(float64)
|
|
if !okAssert {
|
|
return errors.New("type assertion failed for orderbook id data")
|
|
}
|
|
pricePeriod, okAssert := data[1].(float64)
|
|
if !okAssert {
|
|
return errors.New("type assertion failed for orderbook price data")
|
|
}
|
|
rateAmount, okAssert := data[2].(float64)
|
|
if !okAssert {
|
|
return errors.New("type assertion failed for orderbook rate data")
|
|
}
|
|
if len(data) == 4 {
|
|
fundingRate = true
|
|
amount, okFunding := data[3].(float64)
|
|
if !okFunding {
|
|
return errors.New("type assertion failed for orderbook funding data")
|
|
}
|
|
newOrderbook = append(newOrderbook, WebsocketBook{
|
|
ID: int64(id),
|
|
Period: int64(pricePeriod),
|
|
Price: rateAmount,
|
|
Amount: amount})
|
|
} else {
|
|
newOrderbook = append(newOrderbook, WebsocketBook{
|
|
ID: int64(id),
|
|
Price: pricePeriod,
|
|
Amount: rateAmount})
|
|
}
|
|
}
|
|
if err = b.WsInsertSnapshot(pair, chanAsset, newOrderbook, fundingRate); err != nil {
|
|
return fmt.Errorf("inserting snapshot error: %s",
|
|
err)
|
|
}
|
|
case float64:
|
|
pricePeriod, okSnap := obSnapBundle[1].(float64)
|
|
if !okSnap {
|
|
return errors.New("type assertion failed for orderbook price snapshot data")
|
|
}
|
|
amountRate, okSnap := obSnapBundle[2].(float64)
|
|
if !okSnap {
|
|
return errors.New("type assertion failed for orderbook amount snapshot data")
|
|
}
|
|
if len(obSnapBundle) == 4 {
|
|
fundingRate = true
|
|
var amount float64
|
|
amount, okSnap = obSnapBundle[3].(float64)
|
|
if !okSnap {
|
|
return errors.New("type assertion failed for orderbook amount snapshot data")
|
|
}
|
|
newOrderbook = append(newOrderbook, WebsocketBook{
|
|
ID: int64(id),
|
|
Period: int64(pricePeriod),
|
|
Price: amountRate,
|
|
Amount: amount})
|
|
} else {
|
|
newOrderbook = append(newOrderbook, WebsocketBook{
|
|
ID: int64(id),
|
|
Price: pricePeriod,
|
|
Amount: amountRate})
|
|
}
|
|
|
|
if err = b.WsUpdateOrderbook(pair, chanAsset, newOrderbook, chanID, int64(sequenceNo), fundingRate); err != nil {
|
|
return fmt.Errorf("updating orderbook error: %s",
|
|
err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
case wsCandles:
|
|
if candleBundle, ok := d[1].([]interface{}); ok {
|
|
if len(candleBundle) == 0 {
|
|
return nil
|
|
}
|
|
|
|
switch candleData := candleBundle[0].(type) {
|
|
case []interface{}:
|
|
for i := range candleBundle {
|
|
var element []interface{}
|
|
element, ok = candleBundle[i].([]interface{})
|
|
if !ok {
|
|
return errors.New("candle type assertion for element data")
|
|
}
|
|
if len(element) < 6 {
|
|
return errors.New("invalid candleBundle length")
|
|
}
|
|
var klineData stream.KlineData
|
|
if klineData.Timestamp, err = convert.TimeFromUnixTimestampFloat(element[0]); err != nil {
|
|
return fmt.Errorf("unable to convert candle timestamp: %w", err)
|
|
}
|
|
if klineData.OpenPrice, ok = element[1].(float64); !ok {
|
|
return errors.New("unable to type assert candle open price")
|
|
}
|
|
if klineData.ClosePrice, ok = element[2].(float64); !ok {
|
|
return errors.New("unable to type assert candle close price")
|
|
}
|
|
if klineData.HighPrice, ok = element[3].(float64); !ok {
|
|
return errors.New("unable to type assert candle high price")
|
|
}
|
|
if klineData.LowPrice, ok = element[4].(float64); !ok {
|
|
return errors.New("unable to type assert candle low price")
|
|
}
|
|
if klineData.Volume, ok = element[5].(float64); !ok {
|
|
return errors.New("unable to type assert candle volume")
|
|
}
|
|
klineData.Exchange = b.Name
|
|
klineData.AssetType = chanAsset
|
|
klineData.Pair = pair
|
|
b.Websocket.DataHandler <- klineData
|
|
}
|
|
case float64:
|
|
if len(candleBundle) < 6 {
|
|
return errors.New("invalid candleBundle length")
|
|
}
|
|
var klineData stream.KlineData
|
|
if klineData.Timestamp, err = convert.TimeFromUnixTimestampFloat(candleData); err != nil {
|
|
return fmt.Errorf("unable to convert candle timestamp: %w", err)
|
|
}
|
|
if klineData.OpenPrice, ok = candleBundle[1].(float64); !ok {
|
|
return errors.New("unable to type assert candle open price")
|
|
}
|
|
if klineData.ClosePrice, ok = candleBundle[2].(float64); !ok {
|
|
return errors.New("unable to type assert candle close price")
|
|
}
|
|
if klineData.HighPrice, ok = candleBundle[3].(float64); !ok {
|
|
return errors.New("unable to type assert candle high price")
|
|
}
|
|
if klineData.LowPrice, ok = candleBundle[4].(float64); !ok {
|
|
return errors.New("unable to type assert candle low price")
|
|
}
|
|
if klineData.Volume, ok = candleBundle[5].(float64); !ok {
|
|
return errors.New("unable to type assert candle volume")
|
|
}
|
|
klineData.Exchange = b.Name
|
|
klineData.AssetType = chanAsset
|
|
klineData.Pair = pair
|
|
b.Websocket.DataHandler <- klineData
|
|
}
|
|
}
|
|
return nil
|
|
case wsTicker:
|
|
tickerData, ok := d[1].([]interface{})
|
|
if !ok {
|
|
return errors.New("type assertion for tickerData")
|
|
}
|
|
|
|
t := &ticker.Price{
|
|
AssetType: chanAsset,
|
|
Pair: pair,
|
|
ExchangeName: b.Name,
|
|
}
|
|
|
|
if len(tickerData) == 10 {
|
|
if t.Bid, ok = tickerData[0].(float64); !ok {
|
|
return errors.New("unable to type assert ticker bid")
|
|
}
|
|
if t.Ask, ok = tickerData[2].(float64); !ok {
|
|
return errors.New("unable to type assert ticker ask")
|
|
}
|
|
if t.Last, ok = tickerData[6].(float64); !ok {
|
|
return errors.New("unable to type assert ticker last")
|
|
}
|
|
if t.Volume, ok = tickerData[7].(float64); !ok {
|
|
return errors.New("unable to type assert ticker volume")
|
|
}
|
|
if t.High, ok = tickerData[8].(float64); !ok {
|
|
return errors.New("unable to type assert ticker high")
|
|
}
|
|
if t.Low, ok = tickerData[9].(float64); !ok {
|
|
return errors.New("unable to type assert ticker low")
|
|
}
|
|
} else {
|
|
if t.FlashReturnRate, ok = tickerData[0].(float64); !ok {
|
|
return errors.New("unable to type assert ticker flash return rate")
|
|
}
|
|
if t.Bid, ok = tickerData[1].(float64); !ok {
|
|
return errors.New("unable to type assert ticker bid")
|
|
}
|
|
if t.BidPeriod, ok = tickerData[2].(float64); !ok {
|
|
return errors.New("unable to type assert ticker bid period")
|
|
}
|
|
if t.BidSize, ok = tickerData[3].(float64); !ok {
|
|
return errors.New("unable to type assert ticker bid size")
|
|
}
|
|
if t.Ask, ok = tickerData[4].(float64); !ok {
|
|
return errors.New("unable to type assert ticker ask")
|
|
}
|
|
if t.AskPeriod, ok = tickerData[5].(float64); !ok {
|
|
return errors.New("unable to type assert ticker ask period")
|
|
}
|
|
if t.AskSize, ok = tickerData[6].(float64); !ok {
|
|
return errors.New("unable to type assert ticker ask size")
|
|
}
|
|
if t.Last, ok = tickerData[9].(float64); !ok {
|
|
return errors.New("unable to type assert ticker last")
|
|
}
|
|
if t.Volume, ok = tickerData[10].(float64); !ok {
|
|
return errors.New("unable to type assert ticker volume")
|
|
}
|
|
if t.High, ok = tickerData[11].(float64); !ok {
|
|
return errors.New("unable to type assert ticker high")
|
|
}
|
|
if t.Low, ok = tickerData[12].(float64); !ok {
|
|
return errors.New("unable to type assert ticker low")
|
|
}
|
|
if t.FlashReturnRateAmount, ok = tickerData[15].(float64); !ok {
|
|
return errors.New("unable to type assert ticker flash return rate")
|
|
}
|
|
}
|
|
b.Websocket.DataHandler <- t
|
|
return nil
|
|
case wsTrades:
|
|
if !b.IsSaveTradeDataEnabled() {
|
|
return nil
|
|
}
|
|
if chanAsset == asset.MarginFunding {
|
|
return nil
|
|
}
|
|
var tradeHolder []WebsocketTrade
|
|
switch len(d) {
|
|
case 2:
|
|
snapshot, ok := d[1].([]interface{})
|
|
if !ok {
|
|
return errors.New("unable to type assert trade snapshot data")
|
|
}
|
|
for i := range snapshot {
|
|
elem, ok := snapshot[i].([]interface{})
|
|
if !ok {
|
|
return errors.New("unable to type assert trade snapshot element data")
|
|
}
|
|
tradeID, ok := elem[0].(float64)
|
|
if !ok {
|
|
return errors.New("unable to type assert trade ID")
|
|
}
|
|
timestamp, ok := elem[1].(float64)
|
|
if !ok {
|
|
return errors.New("unable to type assert trade timestamp")
|
|
}
|
|
amount, ok := elem[2].(float64)
|
|
if !ok {
|
|
return errors.New("unable to type assert trade amount")
|
|
}
|
|
wsTrade := WebsocketTrade{
|
|
ID: int64(tradeID),
|
|
Timestamp: int64(timestamp),
|
|
Amount: amount,
|
|
}
|
|
if len(elem) == 5 {
|
|
rate, ok := elem[3].(float64)
|
|
if !ok {
|
|
return errors.New("unable to type assert trade rate")
|
|
}
|
|
wsTrade.Rate = rate
|
|
period, ok := elem[4].(float64)
|
|
if !ok {
|
|
return errors.New("unable to type assert trade period")
|
|
}
|
|
wsTrade.Period = int64(period)
|
|
} else {
|
|
price, ok := elem[3].(float64)
|
|
if !ok {
|
|
return errors.New("unable to type assert trade price")
|
|
}
|
|
wsTrade.Rate = price
|
|
}
|
|
tradeHolder = append(tradeHolder, wsTrade)
|
|
}
|
|
case 3:
|
|
event, ok := d[1].(string)
|
|
if !ok {
|
|
return errors.New("unable to type assert data event")
|
|
}
|
|
if event != wsFundingTradeUpdate &&
|
|
event != wsTradeExecutionUpdate {
|
|
return nil
|
|
}
|
|
data, ok := d[2].([]interface{})
|
|
if !ok {
|
|
return errors.New("trade data type assertion error")
|
|
}
|
|
|
|
tradeID, ok := data[0].(float64)
|
|
if !ok {
|
|
return errors.New("unable to type assert trade ID")
|
|
}
|
|
timestamp, ok := data[1].(float64)
|
|
if !ok {
|
|
return errors.New("unable to type assert trade timestamp")
|
|
}
|
|
amount, ok := data[2].(float64)
|
|
if !ok {
|
|
return errors.New("unable to type assert trade amount")
|
|
}
|
|
wsTrade := WebsocketTrade{
|
|
ID: int64(tradeID),
|
|
Timestamp: int64(timestamp),
|
|
Amount: amount,
|
|
}
|
|
if len(data) == 5 {
|
|
rate, ok := data[3].(float64)
|
|
if !ok {
|
|
return errors.New("unable to type assert trade rate")
|
|
}
|
|
period, ok := data[4].(float64)
|
|
if !ok {
|
|
return errors.New("unable to type assert trade period")
|
|
}
|
|
wsTrade.Rate = rate
|
|
wsTrade.Period = int64(period)
|
|
} else {
|
|
price, ok := data[3].(float64)
|
|
if !ok {
|
|
return errors.New("unable to type assert trade price")
|
|
}
|
|
wsTrade.Price = price
|
|
}
|
|
tradeHolder = append(tradeHolder, wsTrade)
|
|
}
|
|
trades := make([]trade.Data, len(tradeHolder))
|
|
for i := range tradeHolder {
|
|
side := order.Buy
|
|
newAmount := tradeHolder[i].Amount
|
|
if newAmount < 0 {
|
|
side = order.Sell
|
|
newAmount *= -1
|
|
}
|
|
price := tradeHolder[i].Price
|
|
if price == 0 && tradeHolder[i].Rate > 0 {
|
|
price = tradeHolder[i].Rate
|
|
}
|
|
trades[i] = trade.Data{
|
|
TID: strconv.FormatInt(tradeHolder[i].ID, 10),
|
|
CurrencyPair: pair,
|
|
Timestamp: time.UnixMilli(tradeHolder[i].Timestamp),
|
|
Price: price,
|
|
Amount: newAmount,
|
|
Exchange: b.Name,
|
|
AssetType: chanAsset,
|
|
Side: side,
|
|
}
|
|
}
|
|
|
|
return b.AddTradesToBuffer(trades...)
|
|
}
|
|
|
|
if authResp, ok := d[1].(string); ok {
|
|
switch authResp {
|
|
case wsHeartbeat, pong:
|
|
return nil
|
|
case wsNotification:
|
|
notification, ok := d[2].([]interface{})
|
|
if !ok {
|
|
return errors.New("unable to type assert notification data")
|
|
}
|
|
if data, ok := notification[4].([]interface{}); ok {
|
|
channelName, ok := notification[1].(string)
|
|
if !ok {
|
|
return errors.New("unable to type assert channelName")
|
|
}
|
|
switch {
|
|
case strings.Contains(channelName, wsFundingOfferNewRequest),
|
|
strings.Contains(channelName, wsFundingOfferUpdateRequest),
|
|
strings.Contains(channelName, wsFundingOfferCancelRequest):
|
|
if data[0] != nil {
|
|
if id, ok := data[0].(float64); ok && id > 0 {
|
|
if b.Websocket.Match.IncomingWithData(int64(id), respRaw) {
|
|
return nil
|
|
}
|
|
offer, err := wsHandleFundingOffer(data, true /* include rate real */)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b.Websocket.DataHandler <- offer
|
|
}
|
|
}
|
|
case strings.Contains(channelName, wsOrderNewRequest),
|
|
strings.Contains(channelName, wsOrderUpdateRequest),
|
|
strings.Contains(channelName, wsOrderCancelRequest):
|
|
if data[2] != nil {
|
|
if id, ok := data[2].(float64); ok && id > 0 {
|
|
if b.Websocket.Match.IncomingWithData(int64(id), respRaw) {
|
|
return nil
|
|
}
|
|
b.wsHandleOrder(data)
|
|
}
|
|
}
|
|
default:
|
|
return fmt.Errorf("%s - Unexpected data returned %s",
|
|
b.Name,
|
|
respRaw)
|
|
}
|
|
}
|
|
if notification[5] != nil {
|
|
if wsErr, ok := notification[5].(string); ok {
|
|
if strings.EqualFold(wsErr, wsError) {
|
|
if errMsg, ok := notification[6].(string); ok {
|
|
return fmt.Errorf("%s - Error %s",
|
|
b.Name,
|
|
errMsg)
|
|
}
|
|
return fmt.Errorf("%s - unhandled error message: %v", b.Name,
|
|
notification[6])
|
|
}
|
|
}
|
|
}
|
|
case wsOrderSnapshot:
|
|
if snapBundle, ok := d[2].([]interface{}); ok && len(snapBundle) > 0 {
|
|
if _, ok := snapBundle[0].([]interface{}); ok {
|
|
for i := range snapBundle {
|
|
if positionData, ok := snapBundle[i].([]interface{}); ok {
|
|
b.wsHandleOrder(positionData)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
case wsOrderCancel, wsOrderNew, wsOrderUpdate:
|
|
if oData, ok := d[2].([]interface{}); ok && len(oData) > 0 {
|
|
b.wsHandleOrder(oData)
|
|
}
|
|
case wsPositionSnapshot:
|
|
if snapBundle, ok := d[2].([]interface{}); ok && len(snapBundle) > 0 {
|
|
if _, ok := snapBundle[0].([]interface{}); ok {
|
|
snapshot := make([]WebsocketPosition, len(snapBundle))
|
|
for i := range snapBundle {
|
|
positionData, ok := snapBundle[i].([]interface{})
|
|
if !ok {
|
|
return errors.New("unable to type assert wsPositionSnapshot positionData")
|
|
}
|
|
var position WebsocketPosition
|
|
if position.Pair, ok = positionData[0].(string); !ok {
|
|
return errors.New("unable to type assert position snapshot pair")
|
|
}
|
|
if position.Status, ok = positionData[1].(string); !ok {
|
|
return errors.New("unable to type assert position snapshot status")
|
|
}
|
|
if position.Amount, ok = positionData[2].(float64); !ok {
|
|
return errors.New("unable to type assert position snapshot amount")
|
|
}
|
|
if position.Price, ok = positionData[3].(float64); !ok {
|
|
return errors.New("unable to type assert position snapshot price")
|
|
}
|
|
if position.MarginFunding, ok = positionData[4].(float64); !ok {
|
|
return errors.New("unable to type assert position snapshot margin funding")
|
|
}
|
|
marginFundingType, ok := positionData[5].(float64)
|
|
if !ok {
|
|
return errors.New("unable to type assert position snapshot margin funding type")
|
|
}
|
|
position.MarginFundingType = int64(marginFundingType)
|
|
if position.ProfitLoss, ok = positionData[6].(float64); !ok {
|
|
return errors.New("unable to type assert position snapshot profit loss")
|
|
}
|
|
if position.ProfitLossPercent, ok = positionData[7].(float64); !ok {
|
|
return errors.New("unable to type assert position snapshot profit loss percent")
|
|
}
|
|
if position.LiquidationPrice, ok = positionData[8].(float64); !ok {
|
|
return errors.New("unable to type assert position snapshot liquidation price")
|
|
}
|
|
if position.Leverage, ok = positionData[9].(float64); !ok {
|
|
return errors.New("unable to type assert position snapshot leverage")
|
|
}
|
|
snapshot[i] = position
|
|
}
|
|
b.Websocket.DataHandler <- snapshot
|
|
}
|
|
}
|
|
case wsPositionNew, wsPositionUpdate, wsPositionClose:
|
|
if positionData, ok := d[2].([]interface{}); ok && len(positionData) > 0 {
|
|
var position WebsocketPosition
|
|
if position.Pair, ok = positionData[0].(string); !ok {
|
|
return errors.New("unable to type assert position pair")
|
|
}
|
|
if position.Status, ok = positionData[1].(string); !ok {
|
|
return errors.New("unable to type assert position status")
|
|
}
|
|
if position.Amount, ok = positionData[2].(float64); !ok {
|
|
return errors.New("unable to type assert position amount")
|
|
}
|
|
if position.Price, ok = positionData[3].(float64); !ok {
|
|
return errors.New("unable to type assert position price")
|
|
}
|
|
if position.MarginFunding, ok = positionData[4].(float64); !ok {
|
|
return errors.New("unable to type assert margin position funding")
|
|
}
|
|
marginFundingType, ok := positionData[5].(float64)
|
|
if !ok {
|
|
return errors.New("unable to type assert position margin funding type")
|
|
}
|
|
position.MarginFundingType = int64(marginFundingType)
|
|
if position.ProfitLoss, ok = positionData[6].(float64); !ok {
|
|
return errors.New("unable to type assert position profit loss")
|
|
}
|
|
if position.ProfitLossPercent, ok = positionData[7].(float64); !ok {
|
|
return errors.New("unable to type assert position profit loss percent")
|
|
}
|
|
if position.LiquidationPrice, ok = positionData[8].(float64); !ok {
|
|
return errors.New("unable to type assert position liquidation price")
|
|
}
|
|
if position.Leverage, ok = positionData[9].(float64); !ok {
|
|
return errors.New("unable to type assert position leverage")
|
|
}
|
|
b.Websocket.DataHandler <- position
|
|
}
|
|
case wsTradeExecuted, wsTradeExecutionUpdate:
|
|
if tradeData, ok := d[2].([]interface{}); ok && len(tradeData) > 4 {
|
|
var tData WebsocketTradeData
|
|
var tradeID float64
|
|
if tradeID, ok = tradeData[0].(float64); !ok {
|
|
return errors.New("unable to type assert trade ID")
|
|
}
|
|
tData.TradeID = int64(tradeID)
|
|
if tData.Pair, ok = tradeData[1].(string); !ok {
|
|
return errors.New("unable to type assert trade pair")
|
|
}
|
|
var timestamp float64
|
|
if timestamp, ok = tradeData[2].(float64); !ok {
|
|
return errors.New("unable to type assert trade timestamp")
|
|
}
|
|
tData.Timestamp = int64(timestamp)
|
|
var orderID float64
|
|
if orderID, ok = tradeData[3].(float64); !ok {
|
|
return errors.New("unable to type assert trade order ID")
|
|
}
|
|
tData.OrderID = int64(orderID)
|
|
if tData.AmountExecuted, ok = tradeData[4].(float64); !ok {
|
|
return errors.New("unable to type assert trade amount executed")
|
|
}
|
|
if tData.PriceExecuted, ok = tradeData[5].(float64); !ok {
|
|
return errors.New("unable to type assert trade price executed")
|
|
}
|
|
if tData.OrderType, ok = tradeData[6].(string); !ok {
|
|
return errors.New("unable to type assert trade order type")
|
|
}
|
|
if tData.OrderPrice, ok = tradeData[7].(float64); !ok {
|
|
return errors.New("unable to type assert trade order type")
|
|
}
|
|
var maker float64
|
|
if maker, ok = tradeData[8].(float64); !ok {
|
|
return errors.New("unable to type assert trade maker")
|
|
}
|
|
tData.Maker = maker == 1
|
|
if tData.Fee, ok = tradeData[9].(float64); !ok {
|
|
return errors.New("unable to type assert trade fee")
|
|
}
|
|
if tData.FeeCurrency, ok = tradeData[10].(string); !ok {
|
|
return errors.New("unable to type assert trade fee currency")
|
|
}
|
|
b.Websocket.DataHandler <- tData
|
|
}
|
|
case wsFundingOfferSnapshot:
|
|
if snapBundle, ok := d[2].([]interface{}); ok && len(snapBundle) > 0 {
|
|
if _, ok := snapBundle[0].([]interface{}); ok {
|
|
snapshot := make([]*WsFundingOffer, len(snapBundle))
|
|
for i := range snapBundle {
|
|
data, ok := snapBundle[i].([]interface{})
|
|
if !ok {
|
|
return errors.New("unable to type assert wsFundingOrderSnapshot snapBundle data")
|
|
}
|
|
offer, err := wsHandleFundingOffer(data, false /* include rate real */)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
snapshot[i] = offer
|
|
}
|
|
b.Websocket.DataHandler <- snapshot
|
|
}
|
|
}
|
|
case wsFundingOfferNew, wsFundingOfferUpdate, wsFundingOfferCancel:
|
|
if data, ok := d[2].([]interface{}); ok && len(data) > 0 {
|
|
offer, err := wsHandleFundingOffer(data, true /* include rate real */)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b.Websocket.DataHandler <- offer
|
|
}
|
|
case wsFundingCreditSnapshot:
|
|
if snapBundle, ok := d[2].([]interface{}); ok && len(snapBundle) > 0 {
|
|
if _, ok := snapBundle[0].([]interface{}); ok {
|
|
snapshot := make([]*WsCredit, len(snapBundle))
|
|
for i := range snapBundle {
|
|
data, ok := snapBundle[i].([]interface{})
|
|
if !ok {
|
|
return errors.New("unable to type assert wsFundingCreditSnapshot snapBundle data")
|
|
}
|
|
fundingCredit, err := wsHandleFundingCreditLoanData(data, true /* include position pair */)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
snapshot[i] = fundingCredit
|
|
}
|
|
b.Websocket.DataHandler <- snapshot
|
|
}
|
|
}
|
|
case wsFundingCreditNew, wsFundingCreditUpdate, wsFundingCreditCancel:
|
|
if data, ok := d[2].([]interface{}); ok && len(data) > 0 {
|
|
fundingCredit, err := wsHandleFundingCreditLoanData(data, true /* include position pair */)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b.Websocket.DataHandler <- fundingCredit
|
|
}
|
|
case wsFundingLoanSnapshot:
|
|
if snapBundle, ok := d[2].([]interface{}); ok && len(snapBundle) > 0 {
|
|
if _, ok := snapBundle[0].([]interface{}); ok {
|
|
snapshot := make([]*WsCredit, len(snapBundle))
|
|
for i := range snapBundle {
|
|
data, ok := snapBundle[i].([]interface{})
|
|
if !ok {
|
|
return errors.New("unable to type assert wsFundingLoanSnapshot snapBundle data")
|
|
}
|
|
fundingLoanSnapshot, err := wsHandleFundingCreditLoanData(data, false /* include position pair */)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
snapshot[i] = fundingLoanSnapshot
|
|
}
|
|
b.Websocket.DataHandler <- snapshot
|
|
}
|
|
}
|
|
case wsFundingLoanNew, wsFundingLoanUpdate, wsFundingLoanCancel:
|
|
if data, ok := d[2].([]interface{}); ok && len(data) > 0 {
|
|
fundingData, err := wsHandleFundingCreditLoanData(data, false /* include position pair */)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b.Websocket.DataHandler <- fundingData
|
|
}
|
|
case wsWalletSnapshot:
|
|
if snapBundle, ok := d[2].([]interface{}); ok && len(snapBundle) > 0 {
|
|
if _, ok := snapBundle[0].([]interface{}); ok {
|
|
snapshot := make([]WsWallet, len(snapBundle))
|
|
for i := range snapBundle {
|
|
data, ok := snapBundle[i].([]interface{})
|
|
if !ok {
|
|
return errors.New("unable to type assert wsWalletSnapshot snapBundle data")
|
|
}
|
|
var wallet WsWallet
|
|
if wallet.Type, ok = data[0].(string); !ok {
|
|
return errors.New("unable to type assert wallet snapshot type")
|
|
}
|
|
if wallet.Currency, ok = data[1].(string); !ok {
|
|
return errors.New("unable to type assert wallet snapshot currency")
|
|
}
|
|
if wallet.Balance, ok = data[2].(float64); !ok {
|
|
return errors.New("unable to type assert wallet snapshot balance")
|
|
}
|
|
if wallet.UnsettledInterest, ok = data[3].(float64); !ok {
|
|
return errors.New("unable to type assert wallet snapshot unsettled interest")
|
|
}
|
|
if data[4] != nil {
|
|
if wallet.BalanceAvailable, ok = data[4].(float64); !ok {
|
|
return errors.New("unable to type assert wallet snapshot balance available")
|
|
}
|
|
}
|
|
snapshot[i] = wallet
|
|
}
|
|
b.Websocket.DataHandler <- snapshot
|
|
}
|
|
}
|
|
case wsWalletUpdate:
|
|
if data, ok := d[2].([]interface{}); ok && len(data) > 0 {
|
|
var wallet WsWallet
|
|
if wallet.Type, ok = data[0].(string); !ok {
|
|
return errors.New("unable to type assert wallet snapshot type")
|
|
}
|
|
if wallet.Currency, ok = data[1].(string); !ok {
|
|
return errors.New("unable to type assert wallet snapshot currency")
|
|
}
|
|
if wallet.Balance, ok = data[2].(float64); !ok {
|
|
return errors.New("unable to type assert wallet snapshot balance")
|
|
}
|
|
if wallet.UnsettledInterest, ok = data[3].(float64); !ok {
|
|
return errors.New("unable to type assert wallet snapshot unsettled interest")
|
|
}
|
|
if data[4] != nil {
|
|
if wallet.BalanceAvailable, ok = data[4].(float64); !ok {
|
|
return errors.New("unable to type assert wallet snapshot balance available")
|
|
}
|
|
}
|
|
b.Websocket.DataHandler <- wallet
|
|
}
|
|
case wsBalanceUpdate:
|
|
if data, ok := d[2].([]interface{}); ok && len(data) > 0 {
|
|
var balance WsBalanceInfo
|
|
if balance.TotalAssetsUnderManagement, ok = data[0].(float64); !ok {
|
|
return errors.New("unable to type assert balance total assets under management")
|
|
}
|
|
if balance.NetAssetsUnderManagement, ok = data[1].(float64); !ok {
|
|
return errors.New("unable to type assert balance net assets under management")
|
|
}
|
|
b.Websocket.DataHandler <- balance
|
|
}
|
|
case wsMarginInfoUpdate:
|
|
if data, ok := d[2].([]interface{}); ok && len(data) > 0 {
|
|
if eventType, ok := data[0].(string); ok && eventType == "base" {
|
|
baseData, ok := data[1].([]interface{})
|
|
if !ok {
|
|
return errors.New("unable to type assert wsMarginInfoUpdate baseData")
|
|
}
|
|
var marginInfoBase WsMarginInfoBase
|
|
if marginInfoBase.UserProfitLoss, ok = baseData[0].(float64); !ok {
|
|
return errors.New("unable to type assert margin info user profit loss")
|
|
}
|
|
if marginInfoBase.UserSwaps, ok = baseData[1].(float64); !ok {
|
|
return errors.New("unable to type assert margin info user swaps")
|
|
}
|
|
if marginInfoBase.MarginBalance, ok = baseData[2].(float64); !ok {
|
|
return errors.New("unable to type assert margin info balance")
|
|
}
|
|
if marginInfoBase.MarginNet, ok = baseData[3].(float64); !ok {
|
|
return errors.New("unable to type assert margin info net")
|
|
}
|
|
if marginInfoBase.MarginRequired, ok = baseData[4].(float64); !ok {
|
|
return errors.New("unable to type assert margin info required")
|
|
}
|
|
b.Websocket.DataHandler <- marginInfoBase
|
|
}
|
|
}
|
|
case wsFundingInfoUpdate:
|
|
if data, ok := d[2].([]interface{}); ok && len(data) > 0 {
|
|
if fundingType, ok := data[0].(string); ok && fundingType == "sym" {
|
|
symbolData, ok := data[2].([]interface{})
|
|
if !ok {
|
|
return errors.New("unable to type assert wsFundingInfoUpdate symbolData")
|
|
}
|
|
var fundingInfo WsFundingInfo
|
|
if fundingInfo.Symbol, ok = data[1].(string); !ok {
|
|
return errors.New("unable to type assert symbol")
|
|
}
|
|
if fundingInfo.YieldLoan, ok = symbolData[0].(float64); !ok {
|
|
return errors.New("unable to type assert funding info update yield loan")
|
|
}
|
|
if fundingInfo.YieldLend, ok = symbolData[1].(float64); !ok {
|
|
return errors.New("unable to type assert funding info update yield lend")
|
|
}
|
|
if fundingInfo.DurationLoan, ok = symbolData[2].(float64); !ok {
|
|
return errors.New("unable to type assert funding info update duration loan")
|
|
}
|
|
if fundingInfo.DurationLend, ok = symbolData[3].(float64); !ok {
|
|
return errors.New("unable to type assert funding info update duration lend")
|
|
}
|
|
b.Websocket.DataHandler <- fundingInfo
|
|
}
|
|
}
|
|
case wsFundingTradeExecuted, wsFundingTradeUpdate:
|
|
if data, ok := d[2].([]interface{}); ok && len(data) > 0 {
|
|
var wsFundingTrade WsFundingTrade
|
|
tradeID, ok := data[0].(float64)
|
|
if !ok {
|
|
return errors.New("unable to type assert funding trade ID")
|
|
}
|
|
wsFundingTrade.ID = int64(tradeID)
|
|
if wsFundingTrade.Symbol, ok = data[1].(string); !ok {
|
|
return errors.New("unable to type assert funding trade symbol")
|
|
}
|
|
created, ok := data[2].(float64)
|
|
if !ok {
|
|
return errors.New("unable to type assert funding trade created")
|
|
}
|
|
wsFundingTrade.MTSCreated = time.UnixMilli(int64(created))
|
|
offerID, ok := data[3].(float64)
|
|
if !ok {
|
|
return errors.New("unable to type assert funding trade offer ID")
|
|
}
|
|
wsFundingTrade.OfferID = int64(offerID)
|
|
if wsFundingTrade.Amount, ok = data[4].(float64); !ok {
|
|
return errors.New("unable to type assert funding trade amount")
|
|
}
|
|
if wsFundingTrade.Rate, ok = data[5].(float64); !ok {
|
|
return errors.New("unable to type assert funding trade rate")
|
|
}
|
|
period, ok := data[6].(float64)
|
|
if !ok {
|
|
return errors.New("unable to type assert funding trade period")
|
|
}
|
|
wsFundingTrade.Period = int64(period)
|
|
wsFundingTrade.Maker = data[7] != nil
|
|
b.Websocket.DataHandler <- wsFundingTrade
|
|
}
|
|
default:
|
|
b.Websocket.DataHandler <- stream.UnhandledMessageWarning{
|
|
Message: b.Name + stream.UnhandledMessage + string(respRaw),
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func wsHandleFundingOffer(data []interface{}, includeRateReal bool) (*WsFundingOffer, error) {
|
|
var offer WsFundingOffer
|
|
var ok bool
|
|
if data[0] != nil {
|
|
var offerID float64
|
|
if offerID, ok = data[0].(float64); !ok {
|
|
return nil, errors.New("unable to type assert funding offer ID")
|
|
}
|
|
offer.ID = int64(offerID)
|
|
}
|
|
if data[1] != nil {
|
|
if offer.Symbol, ok = data[1].(string); !ok {
|
|
return nil, errors.New("unable to type assert funding offer symbol")
|
|
}
|
|
}
|
|
if data[2] != nil {
|
|
var created float64
|
|
if created, ok = data[2].(float64); !ok {
|
|
return nil, errors.New("unable to type assert funding offer created")
|
|
}
|
|
offer.Created = time.UnixMilli(int64(created))
|
|
}
|
|
if data[3] != nil {
|
|
var updated float64
|
|
if updated, ok = data[3].(float64); !ok {
|
|
return nil, errors.New("unable to type assert funding offer updated")
|
|
}
|
|
offer.Updated = time.UnixMilli(int64(updated))
|
|
}
|
|
if data[4] != nil {
|
|
if offer.Amount, ok = data[4].(float64); !ok {
|
|
return nil, errors.New("unable to type assert funding offer amount")
|
|
}
|
|
}
|
|
if data[5] != nil {
|
|
if offer.OriginalAmount, ok = data[5].(float64); !ok {
|
|
return nil, errors.New("unable to type assert funding offer original amount")
|
|
}
|
|
}
|
|
if data[6] != nil {
|
|
if offer.Type, ok = data[6].(string); !ok {
|
|
return nil, errors.New("unable to type assert funding offer type")
|
|
}
|
|
}
|
|
if data[9] != nil {
|
|
if offer.Flags, ok = data[9].(float64); !ok {
|
|
return nil, errors.New("unable to type assert funding offer flags")
|
|
}
|
|
}
|
|
if data[10] != nil {
|
|
if offer.Status, ok = data[10].(string); !ok {
|
|
return nil, errors.New("unable to type assert funding offer status")
|
|
}
|
|
}
|
|
if data[14] != nil {
|
|
if offer.Rate, ok = data[14].(float64); !ok {
|
|
return nil, errors.New("unable to type assert funding offer rate")
|
|
}
|
|
}
|
|
if data[15] != nil {
|
|
var period float64
|
|
if period, ok = data[15].(float64); !ok {
|
|
return nil, errors.New("unable to type assert funding offer period")
|
|
}
|
|
offer.Period = int64(period)
|
|
}
|
|
if data[16] != nil {
|
|
var notify float64
|
|
if notify, ok = data[16].(float64); !ok {
|
|
return nil, errors.New("unable to type assert funding offer notify")
|
|
}
|
|
offer.Notify = notify == 1
|
|
}
|
|
if data[17] != nil {
|
|
var hidden float64
|
|
if hidden, ok = data[17].(float64); !ok {
|
|
return nil, errors.New("unable to type assert funding offer hidden")
|
|
}
|
|
offer.Hidden = hidden == 1
|
|
}
|
|
if data[19] != nil {
|
|
var renew float64
|
|
if renew, ok = data[19].(float64); !ok {
|
|
return nil, errors.New("unable to type assert funding offer renew")
|
|
}
|
|
offer.Renew = renew == 1
|
|
}
|
|
if includeRateReal && data[20] != nil {
|
|
if offer.RateReal, ok = data[20].(float64); !ok {
|
|
return nil, errors.New("unable to type assert funding offer rate real")
|
|
}
|
|
}
|
|
return &offer, nil
|
|
}
|
|
|
|
func wsHandleFundingCreditLoanData(data []interface{}, includePositionPair bool) (*WsCredit, error) {
|
|
var credit WsCredit
|
|
var ok bool
|
|
if data[0] != nil {
|
|
var id float64
|
|
if id, ok = data[0].(float64); !ok {
|
|
return nil, errors.New("unable to type assert funding credit ID")
|
|
}
|
|
credit.ID = int64(id)
|
|
}
|
|
if data[1] != nil {
|
|
if credit.Symbol, ok = data[1].(string); !ok {
|
|
return nil, errors.New("unable to type assert funding credit symbol")
|
|
}
|
|
}
|
|
if data[2] != nil {
|
|
var side float64
|
|
if side, ok = data[2].(float64); !ok {
|
|
return nil, errors.New("unable to type assert funding credit side")
|
|
}
|
|
credit.Side = int8(side)
|
|
}
|
|
if data[3] != nil {
|
|
var created float64
|
|
if created, ok = data[3].(float64); !ok {
|
|
return nil, errors.New("unable to type assert funding credit created")
|
|
}
|
|
credit.Created = time.UnixMilli(int64(created))
|
|
}
|
|
if data[4] != nil {
|
|
var updated float64
|
|
if updated, ok = data[4].(float64); !ok {
|
|
return nil, errors.New("unable to type assert funding credit updated")
|
|
}
|
|
credit.Updated = time.UnixMilli(int64(updated))
|
|
}
|
|
if data[5] != nil {
|
|
if credit.Amount, ok = data[5].(float64); !ok {
|
|
return nil, errors.New("unable to type assert funding credit amount")
|
|
}
|
|
}
|
|
if data[6] != nil {
|
|
credit.Flags = data[6]
|
|
}
|
|
if data[7] != nil {
|
|
if credit.Status, ok = data[7].(string); !ok {
|
|
return nil, errors.New("unable to type assert funding credit status")
|
|
}
|
|
}
|
|
if data[11] != nil {
|
|
if credit.Rate, ok = data[11].(float64); !ok {
|
|
return nil, errors.New("unable to type assert funding credit rate")
|
|
}
|
|
}
|
|
if data[12] != nil {
|
|
var period float64
|
|
if period, ok = data[12].(float64); !ok {
|
|
return nil, errors.New("unable to type assert funding credit period")
|
|
}
|
|
credit.Period = int64(period)
|
|
}
|
|
if data[13] != nil {
|
|
var opened float64
|
|
if opened, ok = data[13].(float64); !ok {
|
|
return nil, errors.New("unable to type assert funding credit opened")
|
|
}
|
|
credit.Opened = time.UnixMilli(int64(opened))
|
|
}
|
|
if data[14] != nil {
|
|
var lastPayout float64
|
|
if lastPayout, ok = data[14].(float64); !ok {
|
|
return nil, errors.New("unable to type assert last funding credit payout")
|
|
}
|
|
credit.LastPayout = time.UnixMilli(int64(lastPayout))
|
|
}
|
|
if data[15] != nil {
|
|
var notify float64
|
|
if notify, ok = data[15].(float64); !ok {
|
|
return nil, errors.New("unable to type assert funding credit notify")
|
|
}
|
|
credit.Notify = notify == 1
|
|
}
|
|
if data[16] != nil {
|
|
var hidden float64
|
|
if hidden, ok = data[16].(float64); !ok {
|
|
return nil, errors.New("unable to type assert funding credit hidden")
|
|
}
|
|
credit.Hidden = hidden == 1
|
|
}
|
|
if data[18] != nil {
|
|
var renew float64
|
|
if renew, ok = data[18].(float64); !ok {
|
|
return nil, errors.New("unable to type assert funding credit renew")
|
|
}
|
|
credit.Renew = renew == 1
|
|
}
|
|
if data[19] != nil {
|
|
if credit.RateReal, ok = data[19].(float64); !ok {
|
|
return nil, errors.New("unable to type assert rate funding credit real")
|
|
}
|
|
}
|
|
if data[20] != nil {
|
|
var noClose float64
|
|
if noClose, ok = data[20].(float64); !ok {
|
|
return nil, errors.New("unable to type assert no funding credit close")
|
|
}
|
|
credit.NoClose = noClose == 1
|
|
}
|
|
if includePositionPair {
|
|
if data[21] != nil {
|
|
if credit.PositionPair, ok = data[21].(string); !ok {
|
|
return nil, errors.New("unable to type assert funding credit position pair")
|
|
}
|
|
}
|
|
}
|
|
return &credit, nil
|
|
}
|
|
|
|
func (b *Bitfinex) wsHandleOrder(data []interface{}) {
|
|
var od order.Detail
|
|
var err error
|
|
od.Exchange = b.Name
|
|
if data[0] != nil {
|
|
if id, ok := data[0].(float64); ok {
|
|
od.ID = strconv.FormatFloat(id, 'f', -1, 64)
|
|
}
|
|
}
|
|
if data[16] != nil {
|
|
if price, ok := data[16].(float64); ok {
|
|
od.Price = price
|
|
}
|
|
}
|
|
if data[7] != nil {
|
|
if amount, ok := data[7].(float64); ok {
|
|
od.Amount = amount
|
|
}
|
|
}
|
|
if data[6] != nil {
|
|
if remainingAmount, ok := data[6].(float64); ok {
|
|
od.RemainingAmount = remainingAmount
|
|
}
|
|
}
|
|
if data[7] != nil && data[6] != nil {
|
|
if executedAmount, ok := data[7].(float64); ok {
|
|
od.ExecutedAmount = executedAmount - od.RemainingAmount
|
|
}
|
|
}
|
|
if data[4] != nil {
|
|
if date, ok := data[4].(float64); ok {
|
|
od.Date = time.Unix(int64(date)*1000, 0)
|
|
}
|
|
}
|
|
if data[5] != nil {
|
|
if lastUpdated, ok := data[5].(float64); ok {
|
|
od.LastUpdated = time.Unix(int64(lastUpdated)*1000, 0)
|
|
}
|
|
}
|
|
if data[2] != nil {
|
|
if p, ok := data[3].(string); ok {
|
|
od.Pair, od.AssetType, err = b.GetRequestFormattedPairAndAssetType(p[1:])
|
|
if err != nil {
|
|
b.Websocket.DataHandler <- err
|
|
return
|
|
}
|
|
}
|
|
}
|
|
if data[8] != nil {
|
|
if ordType, ok := data[8].(string); ok {
|
|
oType, err := order.StringToOrderType(ordType)
|
|
if err != nil {
|
|
b.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: b.Name,
|
|
OrderID: od.ID,
|
|
Err: err,
|
|
}
|
|
}
|
|
od.Type = oType
|
|
}
|
|
}
|
|
if data[13] != nil {
|
|
if ordStatus, ok := data[13].(string); ok {
|
|
oStatus, err := order.StringToOrderStatus(ordStatus)
|
|
if err != nil {
|
|
b.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: b.Name,
|
|
OrderID: od.ID,
|
|
Err: err,
|
|
}
|
|
}
|
|
od.Status = oStatus
|
|
}
|
|
}
|
|
b.Websocket.DataHandler <- &od
|
|
}
|
|
|
|
// WsInsertSnapshot add the initial orderbook snapshot when subscribed to a
|
|
// channel
|
|
func (b *Bitfinex) WsInsertSnapshot(p currency.Pair, assetType asset.Item, books []WebsocketBook, fundingRate bool) error {
|
|
if len(books) == 0 {
|
|
return errors.New("no orderbooks submitted")
|
|
}
|
|
var book orderbook.Base
|
|
book.Bids = make(orderbook.Items, 0, len(books))
|
|
book.Asks = make(orderbook.Items, 0, len(books))
|
|
for i := range books {
|
|
item := orderbook.Item{
|
|
ID: books[i].ID,
|
|
Amount: books[i].Amount,
|
|
Price: books[i].Price,
|
|
Period: books[i].Period,
|
|
}
|
|
if fundingRate {
|
|
if item.Amount < 0 {
|
|
item.Amount *= -1
|
|
book.Bids = append(book.Bids, item)
|
|
} else {
|
|
book.Asks = append(book.Asks, item)
|
|
}
|
|
} else {
|
|
if books[i].Amount > 0 {
|
|
book.Bids = append(book.Bids, item)
|
|
} else {
|
|
item.Amount *= -1
|
|
book.Asks = append(book.Asks, item)
|
|
}
|
|
}
|
|
}
|
|
|
|
book.Asset = assetType
|
|
book.Pair = p
|
|
book.Exchange = b.Name
|
|
book.PriceDuplication = true
|
|
book.IsFundingRate = fundingRate
|
|
book.VerifyOrderbook = b.CanVerifyOrderbook
|
|
return b.Websocket.Orderbook.LoadSnapshot(&book)
|
|
}
|
|
|
|
// WsUpdateOrderbook updates the orderbook list, removing and adding to the
|
|
// orderbook sides
|
|
func (b *Bitfinex) WsUpdateOrderbook(p currency.Pair, assetType asset.Item, book []WebsocketBook, channelID int, sequenceNo int64, fundingRate bool) error {
|
|
orderbookUpdate := orderbook.Update{
|
|
Asset: assetType,
|
|
Pair: p,
|
|
Bids: make([]orderbook.Item, 0, len(book)),
|
|
Asks: make([]orderbook.Item, 0, len(book)),
|
|
}
|
|
|
|
for i := range book {
|
|
item := orderbook.Item{
|
|
ID: book[i].ID,
|
|
Amount: book[i].Amount,
|
|
Price: book[i].Price,
|
|
Period: book[i].Period,
|
|
}
|
|
|
|
if book[i].Price > 0 {
|
|
orderbookUpdate.Action = orderbook.UpdateInsert
|
|
if fundingRate {
|
|
if book[i].Amount < 0 {
|
|
item.Amount *= -1
|
|
orderbookUpdate.Bids = append(orderbookUpdate.Bids, item)
|
|
} else {
|
|
orderbookUpdate.Asks = append(orderbookUpdate.Asks, item)
|
|
}
|
|
} else {
|
|
if book[i].Amount > 0 {
|
|
orderbookUpdate.Bids = append(orderbookUpdate.Bids, item)
|
|
} else {
|
|
item.Amount *= -1
|
|
orderbookUpdate.Asks = append(orderbookUpdate.Asks, item)
|
|
}
|
|
}
|
|
} else {
|
|
orderbookUpdate.Action = orderbook.Delete
|
|
if fundingRate {
|
|
if book[i].Amount == 1 {
|
|
// delete bid
|
|
orderbookUpdate.Asks = append(orderbookUpdate.Asks, item)
|
|
} else {
|
|
// delete ask
|
|
orderbookUpdate.Bids = append(orderbookUpdate.Bids, item)
|
|
}
|
|
} else {
|
|
if book[i].Amount == 1 {
|
|
// delete bid
|
|
orderbookUpdate.Bids = append(orderbookUpdate.Bids, item)
|
|
} else {
|
|
// delete ask
|
|
orderbookUpdate.Asks = append(orderbookUpdate.Asks, item)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
cMtx.Lock()
|
|
checkme := checksumStore[channelID]
|
|
if checkme == nil {
|
|
cMtx.Unlock()
|
|
return b.Websocket.Orderbook.Update(&orderbookUpdate)
|
|
}
|
|
checksumStore[channelID] = nil
|
|
cMtx.Unlock()
|
|
|
|
if checkme.Sequence+1 == sequenceNo {
|
|
// Sequence numbers get dropped, if checksum is not in line with
|
|
// sequence, do not check.
|
|
ob, err := b.Websocket.Orderbook.GetOrderbook(p, assetType)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot calculate websocket checksum: book not found for %s %s %w",
|
|
p,
|
|
assetType,
|
|
err)
|
|
}
|
|
|
|
err = validateCRC32(ob, checkme.Token)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return b.Websocket.Orderbook.Update(&orderbookUpdate)
|
|
}
|
|
|
|
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
|
|
func (b *Bitfinex) GenerateDefaultSubscriptions() ([]stream.ChannelSubscription, error) {
|
|
var channels = []string{
|
|
wsBook,
|
|
wsTrades,
|
|
wsTicker,
|
|
wsCandles,
|
|
}
|
|
|
|
var subscriptions []stream.ChannelSubscription
|
|
assets := b.GetAssetTypes(true)
|
|
for i := range assets {
|
|
enabledPairs, err := b.GetEnabledPairs(assets[i])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for j := range channels {
|
|
for k := range enabledPairs {
|
|
params := make(map[string]interface{})
|
|
if channels[j] == wsBook {
|
|
params["prec"] = "R0"
|
|
params["len"] = "100"
|
|
}
|
|
|
|
if channels[j] == wsCandles {
|
|
// TODO: Add ability to select timescale && funding period
|
|
var fundingPeriod string
|
|
prefix := "t"
|
|
if assets[i] == asset.MarginFunding {
|
|
prefix = "f"
|
|
fundingPeriod = ":p30"
|
|
}
|
|
params["key"] = "trade:1m:" + prefix + enabledPairs[k].String() + fundingPeriod
|
|
} else {
|
|
params["symbol"] = enabledPairs[k].String()
|
|
}
|
|
|
|
subscriptions = append(subscriptions, stream.ChannelSubscription{
|
|
Channel: channels[j],
|
|
Currency: enabledPairs[k],
|
|
Params: params,
|
|
Asset: assets[i],
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return subscriptions, nil
|
|
}
|
|
|
|
// Subscribe sends a websocket message to receive data from the channel
|
|
func (b *Bitfinex) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
|
|
var errs common.Errors
|
|
checksum := make(map[string]interface{})
|
|
checksum["event"] = "conf"
|
|
checksum["flags"] = bitfinexChecksumFlag + bitfinexWsSequenceFlag
|
|
err := b.Websocket.Conn.SendJSONMessage(checksum)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for i := range channelsToSubscribe {
|
|
req := make(map[string]interface{})
|
|
req["event"] = "subscribe"
|
|
req["channel"] = channelsToSubscribe[i].Channel
|
|
|
|
for k, v := range channelsToSubscribe[i].Params {
|
|
req[k] = v
|
|
}
|
|
|
|
err := b.Websocket.Conn.SendJSONMessage(req)
|
|
if err != nil {
|
|
errs = append(errs, err)
|
|
continue
|
|
}
|
|
b.Websocket.AddSuccessfulSubscriptions(channelsToSubscribe[i])
|
|
}
|
|
if errs != nil {
|
|
return errs
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Unsubscribe sends a websocket message to stop receiving data from the channel
|
|
func (b *Bitfinex) Unsubscribe(channelsToUnsubscribe []stream.ChannelSubscription) error {
|
|
var errs common.Errors
|
|
for i := range channelsToUnsubscribe {
|
|
req := make(map[string]interface{})
|
|
req["event"] = "unsubscribe"
|
|
req["channel"] = channelsToUnsubscribe[i].Channel
|
|
|
|
for k, v := range channelsToUnsubscribe[i].Params {
|
|
req[k] = v
|
|
}
|
|
|
|
err := b.Websocket.Conn.SendJSONMessage(req)
|
|
if err != nil {
|
|
errs = append(errs, err)
|
|
continue
|
|
}
|
|
b.Websocket.RemoveSuccessfulUnsubscriptions(channelsToUnsubscribe[i])
|
|
}
|
|
if errs != nil {
|
|
return errs
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WsSendAuth sends a autheticated event payload
|
|
func (b *Bitfinex) WsSendAuth(ctx context.Context) error {
|
|
creds, err := b.GetCredentials(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
nonce := strconv.FormatInt(time.Now().Unix(), 10)
|
|
payload := "AUTH" + nonce
|
|
|
|
hmac, err := crypto.GetHMAC(crypto.HashSHA512_384,
|
|
[]byte(payload),
|
|
[]byte(creds.Secret))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
request := WsAuthRequest{
|
|
Event: "auth",
|
|
APIKey: creds.Key,
|
|
AuthPayload: payload,
|
|
AuthSig: crypto.HexEncodeToString(hmac),
|
|
AuthNonce: nonce,
|
|
DeadManSwitch: 0,
|
|
}
|
|
err = b.Websocket.AuthConn.SendJSONMessage(request)
|
|
if err != nil {
|
|
b.Websocket.SetCanUseAuthenticatedEndpoints(false)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WsAddSubscriptionChannel adds a new subscription channel to the
|
|
// WebsocketSubdChannels map in bitfinex.go (Bitfinex struct)
|
|
func (b *Bitfinex) WsAddSubscriptionChannel(chanID int, channel, pair string) {
|
|
chanInfo := WebsocketChanInfo{Pair: pair, Channel: channel}
|
|
b.WebsocketSubdChannels[chanID] = chanInfo
|
|
|
|
if b.Verbose {
|
|
log.Debugf(log.ExchangeSys,
|
|
"%s Subscribed to Channel: %s Pair: %s ChannelID: %d\n",
|
|
b.Name,
|
|
channel,
|
|
pair,
|
|
chanID)
|
|
}
|
|
}
|
|
|
|
// WsNewOrder authenticated new order request
|
|
func (b *Bitfinex) WsNewOrder(data *WsNewOrderRequest) (string, error) {
|
|
data.CustomID = b.Websocket.AuthConn.GenerateMessageID(false)
|
|
request := makeRequestInterface(wsOrderNew, data)
|
|
resp, err := b.Websocket.AuthConn.SendMessageReturnResponse(data.CustomID, request)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if resp == nil {
|
|
return "", errors.New(b.Name + " - Order message not returned")
|
|
}
|
|
var respData []interface{}
|
|
err = json.Unmarshal(resp, &respData)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if len(respData) < 3 {
|
|
return "", errors.New("unexpected respData length")
|
|
}
|
|
responseDataDetail, ok := respData[2].([]interface{})
|
|
if !ok {
|
|
return "", errors.New("unable to type assert respData")
|
|
}
|
|
|
|
if len(responseDataDetail) < 4 {
|
|
return "", errors.New("invalid responseDataDetail length")
|
|
}
|
|
|
|
responseOrderDetail, ok := responseDataDetail[4].([]interface{})
|
|
if !ok {
|
|
return "", errors.New("unable to type assert responseOrderDetail")
|
|
}
|
|
var orderID string
|
|
if responseOrderDetail[0] != nil {
|
|
if ordID, ordOK := responseOrderDetail[0].(float64); ordOK && ordID > 0 {
|
|
orderID = strconv.FormatFloat(ordID, 'f', -1, 64)
|
|
}
|
|
}
|
|
var errorMessage, errCode string
|
|
if len(responseDataDetail) > 6 {
|
|
errCode, ok = responseDataDetail[6].(string)
|
|
if !ok {
|
|
return "", errors.New("unable to type assert errCode")
|
|
}
|
|
}
|
|
if len(responseDataDetail) > 7 {
|
|
errorMessage, ok = responseDataDetail[7].(string)
|
|
if !ok {
|
|
return "", errors.New("unable to type assert errorMessage")
|
|
}
|
|
}
|
|
if strings.EqualFold(errCode, wsError) {
|
|
return orderID, errors.New(b.Name + " - " + errCode + ": " + errorMessage)
|
|
}
|
|
return orderID, nil
|
|
}
|
|
|
|
// WsModifyOrder authenticated modify order request
|
|
func (b *Bitfinex) WsModifyOrder(data *WsUpdateOrderRequest) error {
|
|
request := makeRequestInterface(wsOrderUpdate, data)
|
|
resp, err := b.Websocket.AuthConn.SendMessageReturnResponse(data.OrderID, request)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resp == nil {
|
|
return errors.New(b.Name + " - Order message not returned")
|
|
}
|
|
|
|
var responseData []interface{}
|
|
err = json.Unmarshal(resp, &responseData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(responseData) < 3 {
|
|
return errors.New("unexpected responseData length")
|
|
}
|
|
responseOrderData, ok := responseData[2].([]interface{})
|
|
if !ok {
|
|
return errors.New("unable to type assert responseOrderData")
|
|
}
|
|
var errorMessage, errCode string
|
|
if len(responseOrderData) > 6 {
|
|
errCode, ok = responseOrderData[6].(string)
|
|
if !ok {
|
|
return errors.New("unable to type assert errCode")
|
|
}
|
|
}
|
|
if len(responseOrderData) > 7 {
|
|
errorMessage, ok = responseOrderData[7].(string)
|
|
if !ok {
|
|
return errors.New("unable to type assert errorMessage")
|
|
}
|
|
}
|
|
if strings.EqualFold(errCode, wsError) {
|
|
return errors.New(b.Name + " - " + errCode + ": " + errorMessage)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WsCancelMultiOrders authenticated cancel multi order request
|
|
func (b *Bitfinex) WsCancelMultiOrders(orderIDs []int64) error {
|
|
cancel := WsCancelGroupOrdersRequest{
|
|
OrderID: orderIDs,
|
|
}
|
|
request := makeRequestInterface(wsCancelMultipleOrders, cancel)
|
|
return b.Websocket.AuthConn.SendJSONMessage(request)
|
|
}
|
|
|
|
// WsCancelOrder authenticated cancel order request
|
|
func (b *Bitfinex) WsCancelOrder(orderID int64) error {
|
|
cancel := WsCancelOrderRequest{
|
|
OrderID: orderID,
|
|
}
|
|
request := makeRequestInterface(wsOrderCancel, cancel)
|
|
resp, err := b.Websocket.AuthConn.SendMessageReturnResponse(orderID, request)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resp == nil {
|
|
return fmt.Errorf("%v - Order %v failed to cancel", b.Name, orderID)
|
|
}
|
|
var responseData []interface{}
|
|
err = json.Unmarshal(resp, &responseData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(responseData) < 3 {
|
|
return errors.New("unexpected responseData length")
|
|
}
|
|
responseOrderData, ok := responseData[2].([]interface{})
|
|
if !ok {
|
|
return errors.New("unable to type assert responseOrderData")
|
|
}
|
|
var errorMessage, errCode string
|
|
if len(responseOrderData) > 6 {
|
|
errCode, ok = responseOrderData[6].(string)
|
|
if !ok {
|
|
return errors.New("unable to type assert errCode")
|
|
}
|
|
}
|
|
if len(responseOrderData) > 7 {
|
|
errorMessage, ok = responseOrderData[7].(string)
|
|
if !ok {
|
|
return errors.New("unable to type assert errorMessage")
|
|
}
|
|
}
|
|
if strings.EqualFold(errCode, wsError) {
|
|
return errors.New(b.Name + " - " + errCode + ": " + errorMessage)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WsCancelAllOrders authenticated cancel all orders request
|
|
func (b *Bitfinex) WsCancelAllOrders() error {
|
|
cancelAll := WsCancelAllOrdersRequest{All: 1}
|
|
request := makeRequestInterface(wsCancelMultipleOrders, cancelAll)
|
|
return b.Websocket.AuthConn.SendJSONMessage(request)
|
|
}
|
|
|
|
// WsNewOffer authenticated new offer request
|
|
func (b *Bitfinex) WsNewOffer(data *WsNewOfferRequest) error {
|
|
request := makeRequestInterface(wsFundingOfferNew, data)
|
|
return b.Websocket.AuthConn.SendJSONMessage(request)
|
|
}
|
|
|
|
// WsCancelOffer authenticated cancel offer request
|
|
func (b *Bitfinex) WsCancelOffer(orderID int64) error {
|
|
cancel := WsCancelOrderRequest{
|
|
OrderID: orderID,
|
|
}
|
|
request := makeRequestInterface(wsFundingOfferCancel, cancel)
|
|
resp, err := b.Websocket.AuthConn.SendMessageReturnResponse(orderID, request)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resp == nil {
|
|
return fmt.Errorf("%v - Order %v failed to cancel", b.Name, orderID)
|
|
}
|
|
var responseData []interface{}
|
|
err = json.Unmarshal(resp, &responseData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(responseData) < 3 {
|
|
return errors.New("unexpected responseData length")
|
|
}
|
|
responseOrderData, ok := responseData[2].([]interface{})
|
|
if !ok {
|
|
return errors.New("unable to type assert responseOrderData")
|
|
}
|
|
var errorMessage, errCode string
|
|
if len(responseOrderData) > 6 {
|
|
errCode, ok = responseOrderData[6].(string)
|
|
if !ok {
|
|
return errors.New("unable to type assert errCode")
|
|
}
|
|
}
|
|
if len(responseOrderData) > 7 {
|
|
errorMessage, ok = responseOrderData[7].(string)
|
|
if !ok {
|
|
return errors.New("unable to type assert errorMessage")
|
|
}
|
|
}
|
|
if strings.EqualFold(errCode, wsError) {
|
|
return errors.New(b.Name + " - " + errCode + ": " + errorMessage)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func makeRequestInterface(channelName string, data interface{}) []interface{} {
|
|
return []interface{}{0, channelName, nil, data}
|
|
}
|
|
|
|
func validateCRC32(book *orderbook.Base, token int) error {
|
|
// Order ID's need to be sub-sorted in ascending order, this needs to be
|
|
// done on the main book to ensure that we do not cut price levels out below
|
|
reOrderByID(book.Bids)
|
|
reOrderByID(book.Asks)
|
|
|
|
// RO precision calculation is based on order ID's and amount values
|
|
var bids, asks []orderbook.Item
|
|
for i := 0; i < 25; i++ {
|
|
if i < len(book.Bids) {
|
|
bids = append(bids, book.Bids[i])
|
|
}
|
|
if i < len(book.Asks) {
|
|
asks = append(asks, book.Asks[i])
|
|
}
|
|
}
|
|
|
|
// ensure '-' (negative amount) is passed back to string buffer as
|
|
// this is needed for calcs - These get swapped if funding rate
|
|
bidmod := float64(1)
|
|
if book.IsFundingRate {
|
|
bidmod = -1
|
|
}
|
|
|
|
askMod := float64(-1)
|
|
if book.IsFundingRate {
|
|
askMod = 1
|
|
}
|
|
|
|
var check strings.Builder
|
|
for i := 0; i < 25; i++ {
|
|
if i < len(bids) {
|
|
check.WriteString(strconv.FormatInt(bids[i].ID, 10))
|
|
check.WriteString(":")
|
|
check.WriteString(strconv.FormatFloat(bidmod*bids[i].Amount, 'f', -1, 64))
|
|
check.WriteString(":")
|
|
}
|
|
|
|
if i < len(asks) {
|
|
check.WriteString(strconv.FormatInt(asks[i].ID, 10))
|
|
check.WriteString(":")
|
|
check.WriteString(strconv.FormatFloat(askMod*asks[i].Amount, 'f', -1, 64))
|
|
check.WriteString(":")
|
|
}
|
|
}
|
|
|
|
checksumStr := strings.TrimSuffix(check.String(), ":")
|
|
checksum := crc32.ChecksumIEEE([]byte(checksumStr))
|
|
if checksum == uint32(token) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("invalid checksum for %s %s: calculated [%d] does not match [%d]",
|
|
book.Asset,
|
|
book.Pair,
|
|
checksum,
|
|
uint32(token))
|
|
}
|
|
|
|
// reOrderByID sub sorts orderbook items by its corresponding ID when price
|
|
// levels are the same. TODO: Deprecate and shift to buffer level insertion
|
|
// based off ascending ID.
|
|
func reOrderByID(depth []orderbook.Item) {
|
|
subSort:
|
|
for x := 0; x < len(depth); {
|
|
var subset []orderbook.Item
|
|
// Traverse forward elements
|
|
for y := x + 1; y < len(depth); y++ {
|
|
if depth[x].Price == depth[y].Price &&
|
|
// Period matching is for funding rates, this was undocumented
|
|
// but these need to be matched with price for the correct ID
|
|
// alignment
|
|
depth[x].Period == depth[y].Period {
|
|
// Append element to subset when price match occurs
|
|
subset = append(subset, depth[y])
|
|
// Traverse next
|
|
continue
|
|
}
|
|
if len(subset) != 0 {
|
|
// Append root element
|
|
subset = append(subset, depth[x])
|
|
// Sort IDs by ascending
|
|
sort.Slice(subset, func(i, j int) bool {
|
|
return subset[i].ID < subset[j].ID
|
|
})
|
|
// Re-align elements with sorted ID subset
|
|
for z := range subset {
|
|
depth[x+z] = subset[z]
|
|
}
|
|
}
|
|
// When price is not matching change checked element to root
|
|
x = y
|
|
continue subSort
|
|
}
|
|
break
|
|
}
|
|
}
|