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  }