github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/exchange-scrapers/BinanceScraper.go (about) 1 package scrapers 2 3 import ( 4 "encoding/json" 5 "errors" 6 "io/ioutil" 7 "net/http" 8 "net/url" 9 "os" 10 "strconv" 11 "strings" 12 "sync" 13 "time" 14 15 "github.com/diadata-org/diadata/pkg/dia" 16 "github.com/diadata-org/diadata/pkg/dia/helpers/configCollectors" 17 models "github.com/diadata-org/diadata/pkg/model" 18 utils "github.com/diadata-org/diadata/pkg/utils" 19 ws "github.com/gorilla/websocket" 20 "github.com/zekroTJA/timedmap" 21 ) 22 23 const ( 24 BINANCE_API_MAX_RETRIES = 5 25 ) 26 27 var ( 28 binanceWSBaseString = "wss://stream.binance.com:9443/ws" 29 ) 30 31 type binancePairScraperSet map[*BinancePairScraper]nothing 32 33 // BinanceScraper is a Scraper for collecting trades from the Binance websocket API 34 type BinanceScraper struct { 35 // signaling channels for session initialization and finishing 36 shutdown chan nothing 37 shutdownDone chan nothing 38 // error handling; to read error or closed, first acquire read lock 39 // only cleanup method should hold write lock 40 errorLock sync.RWMutex 41 error error 42 closed bool 43 // used to keep track of trading pairs that we subscribed to 44 // use sync.Maps to concurrently handle multiple pairs 45 pairScrapers sync.Map // dia.ExchangePair -> binancePairScraperSet 46 newPairScrapers map[string]*BinancePairScraper 47 proxyIndex int 48 exchangeName string 49 scraperName string 50 chanTrades chan *dia.Trade 51 db *models.RelDB 52 wsClient *ws.Conn 53 apiConnectRetries int 54 exchangepairCache map[string]dia.ExchangePair 55 } 56 57 type binanceWSResponse struct { 58 Timestamp int64 `json:"T"` 59 Price string `json:"p"` 60 Volume string `json:"q"` 61 ForeignTradeID int `json:"t"` 62 ForeignName string `json:"s"` 63 Type interface{} `json:"e"` 64 Buy bool `json:"m"` 65 Ignore bool `json:"M"` 66 } 67 68 // BinancePairScraper implements PairScraper for Binance 69 type BinancePairScraper struct { 70 parent *BinanceScraper 71 pair dia.ExchangePair 72 closed bool 73 } 74 75 // NewBinanceScraper returns a new BinanceScraper for the given pair 76 func NewBinanceScraper(apiKey string, secretKey string, exchange dia.Exchange, scraperName string, scrape bool, relDB *models.RelDB) *BinanceScraper { 77 78 s := &BinanceScraper{ 79 shutdown: make(chan nothing), 80 shutdownDone: make(chan nothing), 81 exchangeName: exchange.Name, 82 scraperName: scraperName, 83 error: nil, 84 chanTrades: make(chan *dia.Trade), 85 db: relDB, 86 proxyIndex: 0, 87 newPairScrapers: make(map[string]*BinancePairScraper), 88 exchangepairCache: make(map[string]dia.ExchangePair), 89 } 90 91 var err error 92 reverseBasetokens, err = getReverseTokensFromConfigFull("binance/reverse_tokens/" + s.exchangeName + "Basetoken") 93 if err != nil { 94 log.Error("error getting tokens for which pairs should be reversed: ", err) 95 } 96 log.Info("reverse basetokens: ", reverseBasetokens) 97 98 err = s.connectToAPI() 99 if err != nil { 100 log.Error("getting an error while connecting to api: ", err) 101 } else { 102 log.Println("Successfully connect to websocket server.") 103 } 104 105 //establish connection in the background 106 if scrape { 107 go s.mainLoop() 108 } 109 110 return s 111 } 112 113 func (scraper *BinanceScraper) subscribe(pair dia.ExchangePair, subscribe bool) error { 114 if scraper.closed { 115 return errors.New("binance Scraper: Call ScrapePair on closed scraper") 116 } 117 118 // Validate WebSocket connection exists 119 if scraper.wsClient == nil { 120 return errors.New("WebSocket connection not initialized") 121 } 122 123 // Determine subscription type (SUBSCRIBE/UNSUBSCRIBE) 124 subscribeType := "UNSUBSCRIBE" 125 if subscribe { 126 subscribeType = "SUBSCRIBE" 127 } 128 // Convert symbol+currency to lowercase (e.g., "btcusdt@trade") 129 pairTicker := strings.ToLower(pair.ForeignName) 130 131 subscribeMessage := map[string]interface{}{ 132 "method": subscribeType, 133 "params": []string{pairTicker + "@trade"}, //btcusdt@trade 134 "id": time.Now().UnixNano(), 135 } 136 log.Info("Subscribe Message: ", subscribeMessage) 137 138 if err := scraper.wsClient.WriteJSON(subscribeMessage); err != nil { 139 log.Errorf("Failed to send subscription request: %v", err) 140 return err 141 } 142 return nil 143 } 144 145 func (scraper *BinanceScraper) connectToAPI() error { 146 log.Info("Starting connect to API") 147 148 // Switch to alternative Proxy whenever too many retries on the first. 149 if scraper.apiConnectRetries > BINANCE_API_MAX_RETRIES { 150 log.Errorf("too many timeouts for Binance api connection with proxy %v. Switch to alternative proxy.", scraper.proxyIndex) 151 scraper.apiConnectRetries = 0 152 scraper.proxyIndex = (scraper.proxyIndex + 1) % 2 153 } 154 155 username := utils.Getenv("BINANCE_PROXY"+strconv.Itoa(scraper.proxyIndex)+"_USERNAME", "") 156 password := utils.Getenv("BINANCE_PROXY"+strconv.Itoa(scraper.proxyIndex)+"_PASSWORD", "") 157 user := url.UserPassword(username, password) 158 host := utils.Getenv("BINANCE_PROXY"+strconv.Itoa(scraper.proxyIndex)+"_HOST", "") 159 160 var d ws.Dialer 161 if host != "" { 162 d = ws.Dialer{ 163 Proxy: http.ProxyURL(&url.URL{ 164 Scheme: "http", // or "https" depending on your proxy 165 User: user, 166 Host: host, 167 Path: "/", 168 }, 169 ), 170 } 171 } 172 173 conn, _, err := d.Dial(binanceWSBaseString, nil) 174 if err != nil { 175 log.Errorf("Binance - Connect to API: %s.", err.Error()) 176 return err 177 } 178 scraper.wsClient = conn 179 180 return nil 181 } 182 183 func (up *BinanceScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) { 184 return pair, nil 185 } 186 187 func (s *BinanceScraper) mainLoop() { 188 189 defer func() { 190 log.Println("BinanceScraper main loop exiting") 191 s.cleanup() 192 }() 193 194 tmFalseDuplicateTrades := timedmap.New(duplicateTradesScanFrequency) 195 tmDuplicateTrades := timedmap.New(duplicateTradesScanFrequency) 196 var lock sync.RWMutex 197 198 for { 199 var message binanceWSResponse 200 err := s.wsClient.ReadJSON(&message) 201 if err != nil { 202 log.Error("JSON decode error: ", err) 203 continue 204 } 205 206 if message.Type != nil { 207 s.parseWSResponse(message, tmFalseDuplicateTrades, tmDuplicateTrades, &lock) 208 } else { 209 log.Warn("Skipping invalid trade data:", message) 210 } 211 } 212 } 213 214 func (s *BinanceScraper) parseWSResponse( 215 message binanceWSResponse, 216 tmFalseDuplicateTrades *timedmap.TimedMap, 217 tmDuplicateTrades *timedmap.TimedMap, 218 lock *sync.RWMutex, 219 ) { 220 221 var exchangepair dia.ExchangePair 222 var err error 223 224 ps := s.newPairScrapers[message.ForeignName] 225 pair := ps.pair 226 227 exchangepair, err = s.getExchangePair(message.ForeignName, lock) 228 if err != nil { 229 log.Errorf("getExchangePair %s: %v", message.ForeignName, err) 230 } 231 232 tradeTime := time.Unix(0, message.Timestamp*1000000) 233 tradePrice, err := strconv.ParseFloat(message.Price, 64) 234 if err != nil { 235 log.Errorf("Binance - Parse price: %v.", err) 236 } 237 tradeVolume, err := strconv.ParseFloat(message.Volume, 64) 238 if err != nil { 239 log.Errorf("Binance - Parse volume: %v.", err) 240 } 241 242 if !message.Buy { 243 tradeVolume = -tradeVolume 244 } 245 tradeForeignTradeID := strconv.Itoa(message.ForeignTradeID) 246 247 t := &dia.Trade{ 248 Symbol: pair.Symbol, 249 Pair: message.ForeignName, 250 Price: tradePrice, 251 Volume: tradeVolume, 252 Time: tradeTime, 253 ForeignTradeID: tradeForeignTradeID, 254 Source: s.exchangeName, 255 VerifiedPair: exchangepair.Verified, 256 BaseToken: exchangepair.UnderlyingPair.BaseToken, 257 QuoteToken: exchangepair.UnderlyingPair.QuoteToken, 258 } 259 260 if utils.Contains(reverseBasetokens, t.BaseToken.Identifier()) { 261 // If we need quotation of a base token, reverse pair 262 tSwapped, errSwap := dia.SwapTrade(*t) 263 if errSwap == nil { 264 t = &tSwapped 265 } 266 } 267 268 if exchangepair.Verified { 269 log.Infoln("Got verified trade", t) 270 } 271 272 // Handle duplicate trades. 273 discardTrade := t.IdentifyDuplicateFull(tmFalseDuplicateTrades, duplicateTradesMemory) 274 if !discardTrade { 275 t.IdentifyDuplicateTagset(tmDuplicateTrades, duplicateTradesMemory) 276 ps.parent.chanTrades <- t 277 } 278 279 } 280 281 // getExchangePair returns the exchangepair for @foreignname. It is taken from a local cache 282 // if existing, otherwise from Redis. 283 func (s *BinanceScraper) getExchangePair(foreignName string, lock *sync.RWMutex) (dia.ExchangePair, error) { 284 if ep, ok := s.exchangepairCache[s.scraperName+foreignName]; ok { 285 return ep, nil 286 } 287 ep, err := s.db.GetExchangePairCache(s.scraperName, foreignName) 288 if err != nil { 289 return dia.ExchangePair{}, err 290 } 291 lock.Lock() 292 s.exchangepairCache[s.scraperName+foreignName] = ep 293 lock.Unlock() 294 return ep, nil 295 } 296 297 func (s *BinanceScraper) FillSymbolData(symbol string) (dia.Asset, error) { 298 // TO DO 299 return dia.Asset{Symbol: symbol}, nil 300 } 301 302 // closes all connected PairScrapers 303 // must only be called from mainLoop 304 func (s *BinanceScraper) cleanup() { 305 s.errorLock.Lock() 306 defer s.errorLock.Unlock() 307 // close all channels of PairScraper children 308 s.pairScrapers.Range(func(k, v interface{}) bool { 309 for ps := range v.(binancePairScraperSet) { 310 ps.closed = true 311 } 312 s.pairScrapers.Delete(k) 313 return true 314 }) 315 316 s.closed = true 317 close(s.shutdownDone) // signal that shutdown is complete 318 } 319 320 // Close closes any existing API connections, as well as channels of 321 // PairScrapers from calls to ScrapePair 322 func (s *BinanceScraper) Close() error { 323 if s.closed { 324 return errors.New("BinanceScraper: Already closed") 325 } 326 close(s.shutdown) 327 <-s.shutdownDone 328 s.errorLock.RLock() 329 defer s.errorLock.RUnlock() 330 return s.error 331 } 332 333 // ScrapePair returns a PairScraper that can be used to get trades for a single pair from 334 // this APIScraper 335 func (s *BinanceScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) { 336 ps := &BinancePairScraper{ 337 parent: s, 338 pair: pair, 339 } 340 341 s.newPairScrapers[pair.ForeignName] = ps 342 343 if err := s.subscribe(pair, true); err != nil { 344 log.Error("Subscription failed:", err) 345 } 346 347 //ensure that no more than 5 requests are sent per second(as required by Binance). 348 time.Sleep(400 * time.Millisecond) 349 350 return ps, nil 351 } 352 func (s *BinanceScraper) normalizeSymbol(p dia.ExchangePair, foreignName string, params ...string) (pair dia.ExchangePair, err error) { 353 return pair, nil 354 } 355 356 // FetchAvailablePairs returns a list with all available trade pairs 357 func (s *BinanceScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) { 358 359 // data, _, err := utils.GetRequest("https://api.binance.com/api/v1/exchangeInfo") 360 361 // if err != nil { 362 // return 363 // } 364 // var ar binance.ExchangeInfo 365 // err = json.Unmarshal(data, &ar) 366 // if err == nil { 367 // for _, p := range ar.Symbols { 368 369 // pairToNormalise := dia.ExchangePair{ 370 // Symbol: p.Symbol, 371 // ForeignName: p.Symbol, 372 // Exchange: s.exchangeName, 373 // } 374 375 // pair, serr := s.normalizeSymbol(pairToNormalise, p.BaseAsset, p.Status) 376 // if serr == nil { 377 // pairs = append(pairs, pair) 378 // } else { 379 // log.Error(serr) 380 // } 381 // } 382 // } 383 return 384 } 385 386 // Close stops listening for trades of the pair associated with s 387 func (ps *BinancePairScraper) Close() error { 388 var err error 389 s := ps.parent 390 // if parent already errored, return early 391 s.errorLock.RLock() 392 defer s.errorLock.RUnlock() 393 if s.error != nil { 394 return s.error 395 } 396 if ps.closed { 397 return errors.New("BinancePairScraper: Already closed") 398 } 399 400 // TODO stop collection for the pair 401 402 ps.closed = true 403 return err 404 } 405 406 // Channel returns a channel that can be used to receive trades 407 func (ps *BinanceScraper) Channel() chan *dia.Trade { 408 return ps.chanTrades 409 } 410 411 // Error returns an error when the channel Channel() is closed 412 // and nil otherwise 413 func (ps *BinancePairScraper) Error() error { 414 s := ps.parent 415 s.errorLock.RLock() 416 defer s.errorLock.RUnlock() 417 return s.error 418 } 419 420 // Pair returns the pair this scraper is subscribed to 421 func (ps *BinancePairScraper) Pair() dia.ExchangePair { 422 return ps.pair 423 } 424 425 // getReverseTokensFromConfigFull returns a list of addresses from config file. 426 func getReverseTokensFromConfigFull(filename string) (*[]string, error) { 427 428 var reverseTokens []string 429 430 // Load file and read data 431 filehandle := configCollectors.ConfigFileConnectors(filename, ".json") 432 jsonFile, err := os.Open(filehandle) 433 if err != nil { 434 return &[]string{}, err 435 } 436 defer func() { 437 err = jsonFile.Close() 438 if err != nil { 439 log.Error(err) 440 } 441 }() 442 443 byteData, err := ioutil.ReadAll(jsonFile) 444 if err != nil { 445 return &[]string{}, err 446 } 447 448 type lockedAssetList struct { 449 AllAssets []dia.Asset `json:"Tokens"` 450 } 451 var allAssets lockedAssetList 452 err = json.Unmarshal(byteData, &allAssets) 453 if err != nil { 454 return &[]string{}, err 455 } 456 457 // Extract addresses 458 for _, token := range allAssets.AllAssets { 459 reverseTokens = append(reverseTokens, token.Identifier()) 460 } 461 462 return &reverseTokens, nil 463 }