package bitfinex import ( "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/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/stream/buffer" "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) } 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) } go b.wsReadData(b.Websocket.AuthConn) err = b.WsSendAuth() if err != nil { log.Errorf(log.ExchangeSys, "%v - authentication failed: %v\n", b.Name, err) b.Websocket.SetCanUseAuthenticatedEndpoints(false) } } go b.WsDataHandler() return nil } // wsReadData receives and passes on websocket messages for processing func (b *Bitfinex) wsReadData(ws stream.Connection) { b.Websocket.Wg.Add(1) 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() { b.Websocket.Wg.Add(1) defer b.Websocket.Wg.Done() for { select { case resp := <-comms: if resp.Type == websocket.TextMessage { err := b.wsHandleData(resp.Raw) if err != nil { b.Websocket.DataHandler <- err } } case <-b.Websocket.ShutdownC: return } } } 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": if symbol, ok := d["symbol"].(string); ok { b.WsAddSubscriptionChannel(int(d["chanId"].(float64)), d["channel"].(string), symbol, ) } else if key, ok := d["key"].(string); ok { // Capture trading subscriptions contents := strings.Split(d["key"].(string), ":") if 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(d["chanId"].(float64)), d["channel"].(string), key, ) } case "auth": status := d["status"].(string) if status == "OK" { b.Websocket.DataHandler <- d b.WsAddSubscriptionChannel(0, "account", "N/A") } else if status == "fail" { return fmt.Errorf("websocket unable to AUTH. Error code: %s", d["code"].(string)) } } 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 := obSnapBundle[i].([]interface{}) id, okAssert := data[0].(float64) if !okAssert { return errors.New("type assertion failed for orderbook item data") } pricePeriod, okAssert := data[1].(float64) if !okAssert { return errors.New("type assertion failed for orderbook item data") } rateAmount, okAssert := data[2].(float64) if !okAssert { return errors.New("type assertion failed for orderbook item data") } if len(data) == 4 { fundingRate = true amount, okFunding := data[3].(float64) if !okFunding { return errors.New("type assertion failed for orderbook item 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}) } } err := b.WsInsertSnapshot(pair, chanAsset, newOrderbook, fundingRate) if 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 snapshot data") } amountRate, okSnap := obSnapBundle[2].(float64) if !okSnap { return errors.New("type assertion failed for orderbook 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 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}) } err := b.WsUpdateOrderbook(pair, chanAsset, newOrderbook, chanID, int64(sequenceNo), fundingRate) if 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 { element := candleBundle[i].([]interface{}) b.Websocket.DataHandler <- stream.KlineData{ Timestamp: time.Unix(0, int64(element[0].(float64))*int64(time.Millisecond)), Exchange: b.Name, AssetType: chanAsset, Pair: pair, OpenPrice: element[1].(float64), ClosePrice: element[2].(float64), HighPrice: element[3].(float64), LowPrice: element[4].(float64), Volume: element[5].(float64), } } case float64: b.Websocket.DataHandler <- stream.KlineData{ Timestamp: time.Unix(0, int64(candleData)*int64(time.Millisecond)), Exchange: b.Name, AssetType: chanAsset, Pair: pair, OpenPrice: candleBundle[1].(float64), ClosePrice: candleBundle[2].(float64), HighPrice: candleBundle[3].(float64), LowPrice: candleBundle[4].(float64), Volume: candleBundle[5].(float64), } } } return nil case wsTicker: tickerData := d[1].([]interface{}) if len(tickerData) == 10 { b.Websocket.DataHandler <- &ticker.Price{ ExchangeName: b.Name, Bid: tickerData[0].(float64), Ask: tickerData[2].(float64), Last: tickerData[6].(float64), Volume: tickerData[7].(float64), High: tickerData[8].(float64), Low: tickerData[9].(float64), AssetType: chanAsset, Pair: pair, } } else { b.Websocket.DataHandler <- &ticker.Price{ ExchangeName: b.Name, FlashReturnRate: tickerData[0].(float64), Bid: tickerData[1].(float64), BidPeriod: tickerData[2].(float64), BidSize: tickerData[3].(float64), Ask: tickerData[4].(float64), AskPeriod: tickerData[5].(float64), AskSize: tickerData[6].(float64), Last: tickerData[9].(float64), Volume: tickerData[10].(float64), High: tickerData[11].(float64), Low: tickerData[12].(float64), FlashReturnRateAmount: tickerData[15].(float64), AssetType: chanAsset, Pair: pair, } } return nil case wsTrades: if !b.IsSaveTradeDataEnabled() { return nil } if chanAsset == asset.MarginFunding { return nil } var tradeHolder []WebsocketTrade switch len(d) { case 2: snapshot := d[1].([]interface{}) for i := range snapshot { elem := snapshot[i].([]interface{}) if len(elem) == 5 { tradeHolder = append(tradeHolder, WebsocketTrade{ ID: int64(elem[0].(float64)), Timestamp: int64(elem[1].(float64)), Amount: elem[2].(float64), Rate: elem[3].(float64), Period: int64(elem[4].(float64)), }) } else { tradeHolder = append(tradeHolder, WebsocketTrade{ ID: int64(elem[0].(float64)), Timestamp: int64(elem[1].(float64)), Amount: elem[2].(float64), Price: elem[3].(float64), }) } } case 3: if d[1].(string) != wsFundingTradeUpdate && d[1].(string) != wsTradeExecutionUpdate { return nil } data := d[2].([]interface{}) if len(data) == 5 { tradeHolder = append(tradeHolder, WebsocketTrade{ ID: int64(data[0].(float64)), Timestamp: int64(data[1].(float64)), Amount: data[2].(float64), Rate: data[3].(float64), Period: int64(data[4].(float64)), }) } else { tradeHolder = append(tradeHolder, WebsocketTrade{ ID: int64(data[0].(float64)), Timestamp: int64(data[1].(float64)), Amount: data[2].(float64), Price: data[3].(float64), }) } } var trades []trade.Data 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 = append(trades, trade.Data{ TID: strconv.FormatInt(tradeHolder[i].ID, 10), CurrencyPair: pair, Timestamp: time.Unix(0, tradeHolder[i].Timestamp*int64(time.Millisecond)), 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 := d[2].([]interface{}) if data, ok := notification[4].([]interface{}); ok { channelName := notification[1].(string) switch { case strings.Contains(channelName, wsFundingOrderNewRequest), strings.Contains(channelName, wsFundingOrderUpdateRequest), strings.Contains(channelName, wsFundingOrderCancelRequest): if data[0] != nil && data[0].(float64) > 0 { id := int64(data[0].(float64)) if b.Websocket.Match.IncomingWithData(id, respRaw) { return nil } b.wsHandleFundingOffer(data) } case strings.Contains(channelName, wsOrderNewRequest), strings.Contains(channelName, wsOrderUpdateRequest), strings.Contains(channelName, wsOrderCancelRequest): if data[2] != nil && data[2].(float64) > 0 { id := int64(data[2].(float64)) if b.Websocket.Match.IncomingWithData(id, respRaw) { return nil } b.wsHandleOrder(data) } default: return fmt.Errorf("%s - Unexpected data returned %s", b.Name, respRaw) } } if notification[5] != nil && strings.EqualFold(notification[5].(string), wsError) { return fmt.Errorf("%s - Error %s", b.Name, notification[6].(string)) } case wsOrderSnapshot: if snapBundle, ok := d[2].([]interface{}); ok && len(snapBundle) > 0 { if _, ok := snapBundle[0].([]interface{}); ok { for i := range snapBundle { positionData := snapBundle[i].([]interface{}) b.wsHandleOrder(positionData) } } } case wsOrderCancel, wsOrderNew, wsOrderUpdate: if oData, ok := d[2].([]interface{}); ok && len(oData) > 0 { b.wsHandleOrder(oData) } case wsPositionSnapshot: var snapshot []WebsocketPosition if snapBundle, ok := d[2].([]interface{}); ok && len(snapBundle) > 0 { if _, ok := snapBundle[0].([]interface{}); ok { for i := range snapBundle { positionData := snapBundle[i].([]interface{}) position := WebsocketPosition{ Pair: positionData[0].(string), Status: positionData[1].(string), Amount: positionData[2].(float64), Price: positionData[3].(float64), MarginFunding: positionData[4].(float64), MarginFundingType: int64(positionData[5].(float64)), ProfitLoss: positionData[6].(float64), ProfitLossPercent: positionData[7].(float64), LiquidationPrice: positionData[8].(float64), Leverage: positionData[9].(float64), } snapshot = append(snapshot, position) } b.Websocket.DataHandler <- snapshot } } case wsPositionNew, wsPositionUpdate, wsPositionClose: if positionData, ok := d[2].([]interface{}); ok && len(positionData) > 0 { position := WebsocketPosition{ Pair: positionData[0].(string), Status: positionData[1].(string), Amount: positionData[2].(float64), Price: positionData[3].(float64), MarginFunding: positionData[4].(float64), MarginFundingType: int64(positionData[5].(float64)), ProfitLoss: positionData[6].(float64), ProfitLossPercent: positionData[7].(float64), LiquidationPrice: positionData[8].(float64), Leverage: positionData[9].(float64), } b.Websocket.DataHandler <- position } case wsTradeExecuted, wsTradeExecutionUpdate: if tradeData, ok := d[2].([]interface{}); ok && len(tradeData) > 4 { b.Websocket.DataHandler <- WebsocketTradeData{ TradeID: int64(tradeData[0].(float64)), Pair: tradeData[1].(string), Timestamp: int64(tradeData[2].(float64)), OrderID: int64(tradeData[3].(float64)), AmountExecuted: tradeData[4].(float64), PriceExecuted: tradeData[5].(float64), OrderType: tradeData[6].(string), OrderPrice: tradeData[7].(float64), Maker: tradeData[8].(float64) == 1, Fee: tradeData[9].(float64), FeeCurrency: tradeData[10].(string), } } case wsFundingOrderSnapshot: var snapshot []WsFundingOffer if snapBundle, ok := d[2].([]interface{}); ok && len(snapBundle) > 0 { if _, ok := snapBundle[0].([]interface{}); ok { for i := range snapBundle { data := snapBundle[i].([]interface{}) offer := WsFundingOffer{ ID: int64(data[0].(float64)), Symbol: data[1].(string), Created: int64(data[2].(float64)), Updated: int64(data[3].(float64)), Amount: data[4].(float64), OriginalAmount: data[5].(float64), Type: data[6].(string), Flags: data[9].(float64), Status: data[10].(string), Rate: data[14].(float64), Period: int64(data[15].(float64)), Notify: data[16].(float64) == 1, Hidden: data[17].(float64) == 1, Insure: data[18].(float64) == 1, Renew: data[19].(float64) == 1, RateReal: data[20].(float64), } snapshot = append(snapshot, offer) } b.Websocket.DataHandler <- snapshot } } case wsFundingOrderNew, wsFundingOrderUpdate, wsFundingOrderCancel: if data, ok := d[2].([]interface{}); ok && len(data) > 0 { b.wsHandleFundingOffer(data) } case wsFundingCreditSnapshot: var snapshot []WsCredit if snapBundle, ok := d[2].([]interface{}); ok && len(snapBundle) > 0 { if _, ok := snapBundle[0].([]interface{}); ok { for i := range snapBundle { data := snapBundle[i].([]interface{}) credit := WsCredit{ ID: int64(data[0].(float64)), Symbol: data[1].(string), Side: data[2].(string), Created: int64(data[3].(float64)), Updated: int64(data[4].(float64)), Amount: data[5].(float64), Flags: data[6].(string), Status: data[7].(string), Rate: data[11].(float64), Period: int64(data[12].(float64)), Opened: int64(data[13].(float64)), LastPayout: int64(data[14].(float64)), Notify: data[15].(float64) == 1, Hidden: data[16].(float64) == 1, Insure: data[17].(float64) == 1, Renew: data[18].(float64) == 1, RateReal: data[19].(float64), NoClose: data[20].(float64) == 1, PositionPair: data[21].(string), } snapshot = append(snapshot, credit) } b.Websocket.DataHandler <- snapshot } } case wsFundingCreditNew, wsFundingCreditUpdate, wsFundingCreditCancel: if data, ok := d[2].([]interface{}); ok && len(data) > 0 { b.Websocket.DataHandler <- WsCredit{ ID: int64(data[0].(float64)), Symbol: data[1].(string), Side: data[2].(string), Created: int64(data[3].(float64)), Updated: int64(data[4].(float64)), Amount: data[5].(float64), Flags: data[6].(string), Status: data[7].(string), Rate: data[11].(float64), Period: int64(data[12].(float64)), Opened: int64(data[13].(float64)), LastPayout: int64(data[14].(float64)), Notify: data[15].(float64) == 1, Hidden: data[16].(float64) == 1, Insure: data[17].(float64) == 1, Renew: data[18].(float64) == 1, RateReal: data[19].(float64), NoClose: data[20].(float64) == 1, PositionPair: data[21].(string), } } case wsFundingLoanSnapshot: var snapshot []WsCredit if snapBundle, ok := d[2].([]interface{}); ok && len(snapBundle) > 0 { if _, ok := snapBundle[0].([]interface{}); ok { for i := range snapBundle { data := snapBundle[i].([]interface{}) credit := WsCredit{ ID: int64(data[0].(float64)), Symbol: data[1].(string), Side: data[2].(string), Created: int64(data[3].(float64)), Updated: int64(data[4].(float64)), Amount: data[5].(float64), Flags: data[6].(string), Status: data[7].(string), Rate: data[11].(float64), Period: int64(data[12].(float64)), Opened: int64(data[13].(float64)), LastPayout: int64(data[14].(float64)), Notify: data[15].(float64) == 1, Hidden: data[16].(float64) == 1, Insure: data[17].(float64) == 1, Renew: data[18].(float64) == 1, RateReal: data[19].(float64), NoClose: data[20].(float64) == 1, } snapshot = append(snapshot, credit) } b.Websocket.DataHandler <- snapshot } } case wsFundingLoanNew, wsFundingLoanUpdate, wsFundingLoanCancel: if data, ok := d[2].([]interface{}); ok && len(data) > 0 { b.Websocket.DataHandler <- WsCredit{ ID: int64(data[0].(float64)), Symbol: data[1].(string), Side: data[2].(string), Created: int64(data[3].(float64)), Updated: int64(data[4].(float64)), Amount: data[5].(float64), Flags: data[6].(string), Status: data[7].(string), Rate: data[11].(float64), Period: int64(data[12].(float64)), Opened: int64(data[13].(float64)), LastPayout: int64(data[14].(float64)), Notify: data[15].(float64) == 1, Hidden: data[16].(float64) == 1, Insure: data[17].(float64) == 1, Renew: data[18].(float64) == 1, RateReal: data[19].(float64), NoClose: data[20].(float64) == 1, } } case wsWalletSnapshot: var snapshot []WsWallet if snapBundle, ok := d[2].([]interface{}); ok && len(snapBundle) > 0 { if _, ok := snapBundle[0].([]interface{}); ok { for i := range snapBundle { data := snapBundle[i].([]interface{}) var balanceAvailable float64 if _, ok := data[4].(float64); ok { balanceAvailable = data[4].(float64) } wallet := WsWallet{ Type: data[0].(string), Currency: data[1].(string), Balance: data[2].(float64), UnsettledInterest: data[3].(float64), BalanceAvailable: balanceAvailable, } snapshot = append(snapshot, wallet) } b.Websocket.DataHandler <- snapshot } } case wsWalletUpdate: if data, ok := d[2].([]interface{}); ok && len(data) > 0 { var balanceAvailable float64 if _, ok := data[4].(float64); ok { balanceAvailable = data[4].(float64) } b.Websocket.DataHandler <- WsWallet{ Type: data[0].(string), Currency: data[1].(string), Balance: data[2].(float64), UnsettledInterest: data[3].(float64), BalanceAvailable: balanceAvailable, } } case wsBalanceUpdate: if data, ok := d[2].([]interface{}); ok && len(data) > 0 { b.Websocket.DataHandler <- WsBalanceInfo{ TotalAssetsUnderManagement: data[0].(float64), NetAssetsUnderManagement: data[1].(float64), } } case wsMarginInfoUpdate: if data, ok := d[2].([]interface{}); ok && len(data) > 0 { if data[0].(string) == "base" { if infoBase, ok := d[2].([]interface{}); ok && len(infoBase) > 0 { baseData := data[1].([]interface{}) b.Websocket.DataHandler <- WsMarginInfoBase{ UserProfitLoss: baseData[0].(float64), UserSwaps: baseData[1].(float64), MarginBalance: baseData[2].(float64), MarginNet: baseData[3].(float64), } } } } case wsFundingInfoUpdate: if data, ok := d[2].([]interface{}); ok && len(data) > 0 { if data[0].(string) == "sym" { symbolData := data[1].([]interface{}) b.Websocket.DataHandler <- WsFundingInfo{ YieldLoan: symbolData[0].(float64), YieldLend: symbolData[1].(float64), DurationLoan: symbolData[2].(float64), DurationLend: symbolData[3].(float64), } } } case wsFundingTradeExecuted, wsFundingTradeUpdate: if data, ok := d[2].([]interface{}); ok && len(data) > 0 { b.Websocket.DataHandler <- WsFundingTrade{ ID: int64(data[0].(float64)), Symbol: data[1].(string), MTSCreated: int64(data[2].(float64)), OfferID: int64(data[3].(float64)), Amount: data[4].(float64), Rate: data[5].(float64), Period: int64(data[6].(float64)), Maker: data[7].(float64) == 1, } } default: b.Websocket.DataHandler <- stream.UnhandledMessageWarning{ Message: b.Name + stream.UnhandledMessage + string(respRaw), } return nil } } } return nil } func (b *Bitfinex) wsHandleFundingOffer(data []interface{}) { var fo WsFundingOffer if data[0] != nil { fo.ID = int64(data[0].(float64)) } if data[1] != nil { fo.Symbol = data[1].(string)[1:] } if data[2] != nil { fo.Created = int64(data[2].(float64)) } if data[3] != nil { fo.Updated = int64(data[0].(float64)) } if data[15] != nil { fo.Period = int64(data[15].(float64)) } if data[4] != nil { fo.Amount = data[4].(float64) } if data[5] != nil { fo.OriginalAmount = data[5].(float64) } if data[6] != nil { fo.Type = data[6].(string) } if data[9] != nil { fo.Flags = data[9].(float64) } if data[9] != nil { fo.Status = data[10].(string) } if data[9] != nil { fo.Rate = data[14].(float64) } if data[16] != nil { fo.Notify = data[16].(float64) == 1 } if data[17] != nil { fo.Hidden = data[17].(float64) == 1 } if data[18] != nil { fo.Insure = data[18].(float64) == 1 } if data[19] != nil { fo.Renew = data[19].(float64) == 1 } if data[20] != nil { fo.RateReal = data[20].(float64) } b.Websocket.DataHandler <- fo } func (b *Bitfinex) wsHandleOrder(data []interface{}) { var od order.Detail var err error od.Exchange = b.Name if data[0] != nil { od.ID = strconv.FormatFloat(data[0].(float64), 'f', -1, 64) } if data[16] != nil { od.Price = data[16].(float64) } if data[7] != nil { od.Amount = data[7].(float64) } if data[6] != nil { od.RemainingAmount = data[6].(float64) } if data[7] != nil && data[6] != nil { od.ExecutedAmount = data[7].(float64) - data[6].(float64) } if data[4] != nil { od.Date = time.Unix(int64(data[4].(float64))*1000, 0) } if data[5] != nil { od.LastUpdated = time.Unix(int64(data[5].(float64))*1000, 0) } if data[2] != nil { od.Pair, od.AssetType, err = b.GetRequestFormattedPairAndAssetType(data[3].(string)[1:]) if err != nil { b.Websocket.DataHandler <- err return } } if data[8] != nil { oType, err := order.StringToOrderType(data[8].(string)) if err != nil { b.Websocket.DataHandler <- order.ClassificationError{ Exchange: b.Name, OrderID: od.ID, Err: err, } } od.Type = oType } if data[13] != nil { oStatus, err := order.StringToOrderStatus(data[13].(string)) 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 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 := buffer.Update{Asset: assetType, Pair: p} 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 = buffer.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 = buffer.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() 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() error { if !b.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", b.Name) } nonce := strconv.FormatInt(time.Now().Unix(), 10) payload := "AUTH" + nonce request := WsAuthRequest{ Event: "auth", APIKey: b.API.Credentials.Key, AuthPayload: payload, AuthSig: crypto.HexEncodeToString(crypto.GetHMAC(crypto.HashSHA512_384, []byte(payload), []byte(b.API.Credentials.Secret))), 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 } responseDataDetail := respData[2].([]interface{}) responseOrderDetail := responseDataDetail[4].([]interface{}) var orderID string if responseOrderDetail[0] != nil && responseOrderDetail[0].(float64) > 0 { orderID = strconv.FormatFloat(responseOrderDetail[0].(float64), 'f', -1, 64) } errCode := responseDataDetail[6].(string) errorMessage := responseDataDetail[7].(string) 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 } responseOrderData := responseData[2].([]interface{}) errCode := responseOrderData[6].(string) errorMessage := responseOrderData[7].(string) 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 } responseOrderData := responseData[2].([]interface{}) errCode := responseOrderData[6].(string) errorMessage := responseOrderData[7].(string) 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(wsFundingOrderNew, 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(wsFundingOrderCancel, 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 } responseOrderData := responseData[2].([]interface{}) errCode := responseOrderData[6].(string) var errorMessage string if responseOrderData[7] != nil { errorMessage = responseOrderData[7].(string) } 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 } }