diff --git a/bitfinexhttp.go b/bitfinexhttp.go index b68582b6..a4217a5a 100644 --- a/bitfinexhttp.go +++ b/bitfinexhttp.go @@ -179,6 +179,7 @@ type Bitfinex struct { AvailablePairs []string EnabledPairs []string WebsocketConn *websocket.Conn + WebsocketSubdChannels map[int]BitfinexWebsocketChanInfo } func (b *Bitfinex) SetDefaults() { @@ -187,6 +188,7 @@ func (b *Bitfinex) SetDefaults() { b.Verbose = false b.Websocket = false b.RESTPollingDelay = 10 + b.WebsocketSubdChannels = make(map[int]BitfinexWebsocketChanInfo) } func (b *Bitfinex) GetName() string { diff --git a/bitfinexwebsocket.go b/bitfinexwebsocket.go index dc647015..f16e1d6c 100644 --- a/bitfinexwebsocket.go +++ b/bitfinexwebsocket.go @@ -4,61 +4,106 @@ import ( "github.com/gorilla/websocket" "log" "net/http" + "reflect" + "strconv" + "time" ) -/* Implementation based off Bitfinex's bfws.bitfinex.com websocket service */ - const ( - BITFINEX_WEBSOCKET = "ws://websocket.bitfinex.com:8086/WSGateway/" - BTIFINEX_WEBSOCKET_TRADES = "SubscribeTrades" - BITFINEX_WEBSOCKET_ORDERBOOK = "SubscribeLevel2" + BITFINEX_WEBSOCKET = "wss://api2.bitfinex.com:3000/ws" + BITFINEX_WEBSOCKET_VERSION = "1.0" ) -type BitfinexWebsocketFrameRequest struct { - Type string `json:"type"` - M int `json:"m"` - I int `json:"i"` - Subscription string `json:"n"` - Params string `json:"o"` +type BitfinexWebsocketChanInfo struct { + Channel string + Pair string } -type BitfinexWebsocketResponse struct { - M int `json:"m"` - I int `json:"i"` - Subscription string `json:"n"` - Params string `json:"o"` +type BitfinexWebsocketBook struct { + Price float64 + Count int + Amount float64 } -func (b *Bitfinex) Subscribe(counter *int, subscription string, params map[string]interface{}) error { - msg := BitfinexWebsocketFrameRequest{} - msg.Type = "anws_frame" - msg.M = 0 - msg.I = *counter - msg.Subscription = subscription +type BitfinexWebsocketTrade struct { + ID int64 + Timestamp int64 + Price float64 + Amount float64 +} - paramsEncoded, err := JSONEncode(params) - if err != nil { - return err - } - msg.Params = string(paramsEncoded) +type BitfinexWebsocketTicker struct { + Bid float64 + BidSize float64 + Ask float64 + AskSize float64 + DailyChange float64 + DialyChangePerc float64 + LastPrice float64 + Volume float64 +} - msgEncoded, err := JSONEncode(msg) +func (b *Bitfinex) WebsocketPingHandler() error { + request := make(map[string]string) + request["event"] = "ping" + return b.WebsocketSend(request) +} + +func (b *Bitfinex) WebsocketSend(data interface{}) error { + json, err := JSONEncode(data) if err != nil { return err } - err = b.WebsocketConn.WriteMessage(websocket.TextMessage, msgEncoded) + err = b.WebsocketConn.WriteMessage(websocket.TextMessage, json) if err != nil { return err } - - *counter += 2 return nil } +func (b *Bitfinex) WebsocketSubscribe(channel string, params map[string]string) { + request := make(map[string]string) + request["event"] = "subscribe" + request["channel"] = channel + + if len(params) > 0 { + for k, v := range params { + request[k] = v + } + } + + b.WebsocketSend(request) +} + +func (b *Bitfinex) WebsocketSendAuth() error { + request := make(map[string]interface{}) + payload := "AUTH" + strconv.FormatInt(time.Now().UnixNano(), 10)[:13] + request["event"] = "auth" + request["apiKey"] = b.APIKey + request["authSig"] = HexEncodeToString(GetHMAC(HASH_SHA512_384, []byte(payload), []byte(b.APISecret))) + request["authPayload"] = payload + return b.WebsocketSend(request) +} + +func (b *Bitfinex) WebsocketSendUnauth() error { + request := make(map[string]string) + request["event"] = "unauth" + return b.WebsocketSend(request) +} + +func (b *Bitfinex) WebsocketAddSubscriptionChannel(chanID int, channel, pair string) { + chanInfo := BitfinexWebsocketChanInfo{Pair: pair, Channel: channel} + b.WebsocketSubdChannels[chanID] = chanInfo + + if b.Verbose { + log.Printf("%s Subscribed to Channel: %s Pair: %s ChannelID: %d\n", b.GetName(), channel, pair, chanID) + } +} + func (b *Bitfinex) WebsocketClient() { - msgCounter := 0 + channels := []string{"book", "trades", "ticker"} for b.Enabled && b.Websocket { var Dialer websocket.Dialer var err error @@ -69,24 +114,46 @@ func (b *Bitfinex) WebsocketClient() { continue } - if b.Verbose { - log.Printf("%s Connected to Websocket.\n", b.GetName()) + msgType, resp, err := b.WebsocketConn.ReadMessage() + if msgType != websocket.TextMessage { + continue } - err = b.Subscribe(&msgCounter, BITFINEX_WEBSOCKET_ORDERBOOK, map[string]interface{}{"ExchangeID": "0", "ProductPairID": "1", "Depth": 25, "RoundToDecimals": 2}) + type WebsocketHandshake struct { + Event string `json:"event"` + Code int64 `json:"code"` + Version int `json:"version"` + } + + hs := WebsocketHandshake{} + err = JSONDecode(resp, &hs) if err != nil { log.Println(err) continue } - err = b.Subscribe(&msgCounter, BTIFINEX_WEBSOCKET_TRADES, map[string]interface{}{"ExchangeID": "0", "ProductPairID": "1", "IncludeLastCount": "100"}) - if err != nil { - log.Println(err) - continue + if hs.Event == "info" { + if b.Verbose { + log.Printf("%s Connected to Websocket.\n", b.GetName()) + } } - if b.Verbose { - log.Printf("%s Subscribed to channels.\n", b.GetName()) + for _, x := range channels { + for _, y := range b.EnabledPairs { + params := make(map[string]string) + if x == "book" { + params["prec"] = "P0" + } + params["pair"] = y + b.WebsocketSubscribe(x, params) + } + } + + if b.AuthenticatedAPISupport { + err = b.WebsocketSendAuth() + if err != nil { + log.Println(err) + } } for b.Enabled && b.Websocket { @@ -98,14 +165,79 @@ func (b *Bitfinex) WebsocketClient() { switch msgType { case websocket.TextMessage: - msg := BitfinexWebsocketResponse{} - err := JSONDecode(resp, &msg) + var result interface{} + err := JSONDecode(resp, &result) if err != nil { log.Println(err) continue } - log.Println(string(resp)) - log.Println(msg) + + switch reflect.TypeOf(result).String() { + case "map[string]interface {}": + eventData := result.(map[string]interface{}) + event := eventData["event"] + + switch event { + case "subscribed": + b.WebsocketAddSubscriptionChannel(int(eventData["chanId"].(float64)), eventData["channel"].(string), eventData["pair"].(string)) + case "auth": + status := eventData["status"].(string) + + if status == "OK" { + b.WebsocketAddSubscriptionChannel(0, "account", "N/A") + } else if status == "fail" { + log.Printf("%s Websocket unable to AUTH. Error code: %s\n", b.GetName(), eventData["code"].(string)) + b.AuthenticatedAPISupport = false + } + } + case "[]interface {}": + chanData := result.([]interface{}) + chanID := int(chanData[0].(float64)) + chanInfo, ok := b.WebsocketSubdChannels[chanID] + + if !ok { + log.Println("Unable to locate chanID: %d", chanID) + } else { + switch chanInfo.Channel { + case "book": + orderbook := []BitfinexWebsocketBook{} + switch len(chanData) { + case 2: + data := chanData[1].([]interface{}) + for _, x := range data { + y := x.([]interface{}) + orderbook = append(orderbook, BitfinexWebsocketBook{Price: y[0].(float64), Count: int(y[1].(float64)), Amount: y[2].(float64)}) + } + case 4: + orderbook = append(orderbook, BitfinexWebsocketBook{Price: chanData[1].(float64), Count: int(chanData[2].(float64)), Amount: chanData[3].(float64)}) + } + case "ticker": + ticker := BitfinexWebsocketTicker{Bid: chanData[1].(float64), BidSize: chanData[2].(float64), Ask: chanData[3].(float64), AskSize: chanData[4].(float64), + DailyChange: chanData[5].(float64), DialyChangePerc: chanData[6].(float64), LastPrice: chanData[7].(float64), Volume: chanData[8].(float64)} + + log.Printf("Bitfinex %s Websocket Last %f Volume %f\n", chanInfo.Pair, ticker.LastPrice, ticker.Volume) + case "account": + //to-do + case "trades": + trades := []BitfinexWebsocketTrade{} + switch len(chanData) { + case 2: + data := chanData[1].([]interface{}) + for _, x := range data { + y := x.([]interface{}) + trades = append(trades, BitfinexWebsocketTrade{ID: int64(y[0].(float64)), Timestamp: int64(y[1].(float64)), Price: y[2].(float64), Amount: y[3].(float64)}) + } + case 5: + trade := BitfinexWebsocketTrade{ID: int64(chanData[1].(float64)), Timestamp: int64(chanData[2].(float64)), Price: chanData[3].(float64), Amount: chanData[4].(float64)} + trades = append(trades, trade) + + if b.Verbose { + log.Printf("Bitfinex %s Websocket Trade ID %d Timestamp %d Price %f Amount %f\n", chanInfo.Pair, trade.ID, trade.Timestamp, trade.Price, trade.Amount) + } + } + } + } + } } } b.WebsocketConn.Close()