github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/exchange-scrapers/BitfinexScraper.go (about) 1 package scrapers 2 3 import ( 4 "context" 5 "errors" 6 "os" 7 "strconv" 8 "strings" 9 "sync" 10 "time" 11 12 bitfinex "github.com/bitfinexcom/bitfinex-api-go/v2" 13 "github.com/bitfinexcom/bitfinex-api-go/v2/rest" 14 "github.com/bitfinexcom/bitfinex-api-go/v2/websocket" 15 "github.com/diadata-org/diadata/pkg/dia" 16 models "github.com/diadata-org/diadata/pkg/model" 17 utils "github.com/diadata-org/diadata/pkg/utils" 18 "github.com/op/go-logging" 19 "github.com/zekroTJA/timedmap" 20 ) 21 22 type pairScraperSet map[*BitfinexPairScraper]nothing 23 24 // BitfinexScraper is a Scraper for collecting trades from the Bitfinex websocket API 25 type BitfinexScraper struct { 26 // the websocket connection to the Bitfinex API 27 wsClient *websocket.Client 28 restClient *rest.Client 29 // signaling channels for session initialization and finishing 30 initDone chan nothing 31 shutdown chan nothing 32 shutdownDone chan nothing 33 // error handling; to read error or closed, first acquire read lock 34 // only cleanup method should hold write lock 35 errorLock sync.RWMutex 36 error error 37 closed bool 38 // used to keep track of trading pairs that we subscribed to 39 // use sync.Maps to concurrently handle multiple pairs 40 pairScrapers sync.Map // dia.ExchangePair -> pairScraperSet 41 pairSubscriptions sync.Map // dia.ExchangePair -> string (subscription ID) 42 symbols map[string]string // pair to symbol mapping 43 exchangeName string 44 chanTrades chan *dia.Trade 45 db *models.RelDB 46 } 47 48 // NewBitfinexScraper returns a new BitfinexScraper for the given pair 49 func NewBitfinexScraper(key string, secret string, exchange dia.Exchange, scrape bool, relDB *models.RelDB) *BitfinexScraper { 50 // we want to ensure there are no gaps in our stream 51 // -> close the returned channel on disconnect, forcing the caller to handle 52 // possible gaps 53 params := websocket.NewDefaultParameters() 54 //TODO: Set to false again because now we can have holes in our data stream 55 params.AutoReconnect = true 56 // params.HeartbeatTimeout = 5 * time.Second // used for testing 57 // Only info messages should be sent to log backend 58 loggerBackend := logging.AddModuleLevel(logging.NewLogBackend(os.Stdout, "", 0)) 59 loggerBackend.SetLevel(logging.INFO, "") 60 params.Logger = logging.MustGetLogger("scrapers") 61 params.Logger.SetBackend(loggerBackend) 62 63 s := &BitfinexScraper{ 64 wsClient: websocket.NewWithParams(params), 65 restClient: rest.NewClient().Credentials(key, secret), 66 initDone: make(chan nothing), 67 shutdown: make(chan nothing), 68 shutdownDone: make(chan nothing), 69 symbols: make(map[string]string), 70 exchangeName: exchange.Name, 71 error: nil, 72 chanTrades: make(chan *dia.Trade), 73 db: relDB, 74 } 75 76 // establish connection in the background 77 if scrape { 78 go s.mainLoop() 79 } 80 return s 81 } 82 83 // runs in a goroutine until s is closed 84 func (s *BitfinexScraper) mainLoop() { 85 err := s.wsClient.Connect() 86 listener := s.wsClient.Listen() 87 close(s.initDone) 88 if err != nil { 89 s.cleanup(err) 90 return 91 } 92 93 tmFalseDuplicateTrades := timedmap.New(duplicateTradesScanFrequency) 94 tmDuplicateTrades := timedmap.New(duplicateTradesScanFrequency) 95 96 for { 97 select { 98 case msg, ok := <-listener: 99 if ok { 100 var exchangepair dia.ExchangePair 101 // log.Printf("MSG RECV: %#v\n", msg) 102 // find out message type 103 switch m := msg.(type) { 104 case *bitfinex.Trade: 105 volume := m.Amount 106 if m.Side != bitfinex.Bid { 107 volume = -volume 108 } 109 110 exchangepair, err = s.db.GetExchangePairCache(s.exchangeName, m.Pair) 111 if err != nil { 112 log.Error(err) 113 } 114 // parse trade data structure 115 t := &dia.Trade{ 116 Symbol: s.symbols[m.Pair], 117 Pair: m.Pair, 118 Price: m.Price, 119 Volume: volume, 120 Time: time.Unix(m.MTS/1000, (m.MTS%1000)*int64(time.Millisecond)), 121 ForeignTradeID: strconv.FormatInt(m.ID, 16), 122 Source: s.exchangeName, 123 VerifiedPair: exchangepair.Verified, 124 BaseToken: exchangepair.UnderlyingPair.BaseToken, 125 QuoteToken: exchangepair.UnderlyingPair.QuoteToken, 126 } 127 if exchangepair.Verified { 128 log.Infoln("Got verified trade", t) 129 } 130 131 // Handle duplicate trades. 132 discardTrade := t.IdentifyDuplicateFull(tmFalseDuplicateTrades, duplicateTradesMemory) 133 if !discardTrade { 134 t.IdentifyDuplicateTagset(tmDuplicateTrades, duplicateTradesMemory) 135 s.chanTrades <- t 136 } 137 138 case error: 139 s.cleanup(m) 140 return 141 } 142 } else { 143 s.cleanup(errors.New("BitfinexScraper: Listener channel was closed unexpectedly")) 144 return 145 } 146 case <-s.shutdown: // user requested shutdown 147 log.Println("BitfinexScraper shutting down") 148 s.cleanup(nil) 149 return 150 } 151 } 152 } 153 154 // closes all connected PairScrapers 155 // must only be called from mainLoop 156 func (s *BitfinexScraper) cleanup(err error) { 157 s.errorLock.Lock() 158 defer s.errorLock.Unlock() 159 // close all channels of PairScraper children 160 s.pairScrapers.Range(func(k, v interface{}) bool { 161 for ps := range v.(pairScraperSet) { 162 ps.closed = true 163 } 164 s.pairScrapers.Delete(k) 165 return true 166 }) 167 if s.wsClient.IsConnected() { 168 s.wsClient.Close() 169 } 170 if err != nil { 171 s.error = err 172 } 173 s.closed = true 174 close(s.shutdownDone) // signal that shutdown is complete 175 } 176 177 // Close closes any existing API connections, as well as channels of 178 // PairScrapers from calls to ScrapePair 179 func (s *BitfinexScraper) Close() error { 180 if s.closed { 181 return errors.New("BitfinexScraper: Already closed") 182 } 183 close(s.shutdown) 184 <-s.shutdownDone 185 s.errorLock.RLock() 186 defer s.errorLock.RUnlock() 187 return s.error 188 } 189 190 // ScrapePair returns a PairScraper that can be used to get trades for a single pair from 191 // this APIScraper 192 func (s *BitfinexScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) { 193 <-s.initDone // wait until wsClient is connected 194 s.errorLock.RLock() 195 defer s.errorLock.RUnlock() 196 if s.error != nil { 197 return nil, s.error 198 } 199 if s.closed { 200 return nil, errors.New("BitfinexScraper: Call ScrapePair on closed scraper") 201 } 202 ps := &BitfinexPairScraper{ 203 parent: s, 204 pair: pair, 205 } 206 207 s.symbols[pair.ForeignName] = pair.Symbol 208 209 // initialize pairScraperSet for pair if not already done 210 pairScrapers, _ := s.pairScrapers.LoadOrStore(pair.ForeignName, pairScraperSet{}) 211 // register ps 212 pairScrapers.(pairScraperSet)[ps] = nothing{} 213 // subscribe to trading pair if we are the first scraper for this pair 214 if _, ok := s.pairSubscriptions.Load(pair.ForeignName); !ok { 215 ctx1, ctx1cancel := context.WithTimeout(context.Background(), 5*time.Second) 216 defer ctx1cancel() 217 id, err := s.wsClient.SubscribeTrades(ctx1, pair.ForeignName) 218 if err != nil { 219 // well that didn't work -> cleanup and return error 220 delete(pairScrapers.(pairScraperSet), ps) 221 return nil, err 222 } 223 s.pairSubscriptions.Store(pair.ForeignName, id) 224 } 225 return ps, nil 226 } 227 func (s *BitfinexScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) { 228 229 switch pair.Symbol { 230 case "IOT": 231 pair.Symbol = "MIOTA" 232 case "IOS": 233 pair.Symbol = "IOST" 234 case "QTM": 235 pair.Symbol = "QTUM" 236 case "QSH": 237 pair.Symbol = "QASH" 238 case "DSH": 239 pair.Symbol = "DASH" 240 } 241 return pair, nil 242 243 } 244 245 // func (s *BitfinexScraper) normalizeSymbol(pair dia.Pair) (dia.Pair, error) { 246 // pair.Symbol = strings.ToUpper(pair.ForeignName[0:3]) 247 // if helpers.NameForSymbol(pair.Symbol) == pair.Symbol { 248 // if !helpers.SymbolIsName(pair.Symbol) { 249 // pair, _ = s.NormalizePair(pair) 250 // return pair, errors.New("Foreign name can not be normalized:" + pair.ForeignName + " symbol:" + pair.Symbol) 251 // } 252 // } 253 // if helpers.SymbolIsBlackListed(pair.Symbol) { 254 // return pair, errors.New("Symbol is black listed:" + pair.Symbol) 255 // } 256 // return pair, nil 257 // } 258 259 func (s *BitfinexScraper) FillSymbolData(symbol string) (asset dia.Asset, err error) { 260 // TO DO 261 return dia.Asset{Symbol: symbol}, nil 262 } 263 264 // FetchAvailablePairs returns a list with all available trade pairs 265 func (s *BitfinexScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) { 266 267 data, _, err := utils.GetRequest("https://api.bitfinex.com/v1/symbols") 268 if err != nil { 269 return 270 } 271 ls := strings.Split(strings.Replace(string(data)[1:len(data)-1], "\"", "", -1), ",") 272 for _, p := range ls { 273 var pairToNormalize dia.ExchangePair 274 if len(p) == 6 { 275 pairToNormalize.Symbol = strings.ToUpper(p[0:3]) 276 } else { 277 pairToNormalize.Symbol = strings.ToUpper(strings.Split(p, ":")[0]) 278 } 279 pairToNormalize.ForeignName = strings.ToUpper(p) 280 pairToNormalize.Exchange = s.exchangeName 281 pair, serr := s.NormalizePair(pairToNormalize) 282 if serr == nil { 283 pairs = append(pairs, pair) 284 } else { 285 log.Error(serr) 286 } 287 } 288 return 289 } 290 291 // BitfinexPairScraper implements PairScraper for Bitfinex 292 type BitfinexPairScraper struct { 293 parent *BitfinexScraper 294 pair dia.ExchangePair 295 closed bool 296 } 297 298 // Close stops listening for trades of the pair associated with s 299 func (ps *BitfinexPairScraper) Close() error { 300 var err error 301 s := ps.parent 302 // if parent already errored, return early 303 s.errorLock.RLock() 304 defer s.errorLock.RUnlock() 305 if s.error != nil { 306 return s.error 307 } 308 if ps.closed { 309 return errors.New("BitfinexPairScraper: Already closed") 310 } 311 pairScrapers, ok := s.pairScrapers.Load(ps.pair.Symbol) 312 if !ok { // should never happen 313 panic("BitfinexPairScraper: pairScraperSet not found") 314 } 315 // deregister and close channel 316 delete(pairScrapers.(pairScraperSet), ps) 317 // if we're the last one for this pair -> unsubscribe 318 if len(pairScrapers.(pairScraperSet)) == 0 { 319 id, ok := s.pairSubscriptions.Load(ps.pair.Symbol) 320 if !ok { // should never happen 321 panic("BitfinexPairScraper: Subscription ID not found") 322 } 323 ctx1, ctx1cancel := context.WithTimeout(context.Background(), 5*time.Second) 324 defer ctx1cancel() 325 err = s.wsClient.Unsubscribe(ctx1, id.(string)) 326 } 327 ps.closed = true 328 return err 329 } 330 331 // Channel returns a channel that can be used to receive trades 332 func (ps *BitfinexScraper) Channel() chan *dia.Trade { 333 return ps.chanTrades 334 } 335 336 // Error returns an error when the channel Channel() is closed 337 // and nil otherwise 338 func (ps *BitfinexPairScraper) Error() error { 339 s := ps.parent 340 s.errorLock.RLock() 341 defer s.errorLock.RUnlock() 342 return s.error 343 } 344 345 // Pair returns the pair this scraper is subscribed to 346 func (ps *BitfinexPairScraper) Pair() dia.ExchangePair { 347 return ps.pair 348 }