github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/exchange-scrapers/BinanceScraperUS.go (about)

     1  package scrapers
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"strconv"
     7  	"sync"
     8  	"time"
     9  
    10  	"github.com/cryptwire/go-binance/v2"
    11  	"github.com/diadata-org/diadata/pkg/dia"
    12  	models "github.com/diadata-org/diadata/pkg/model"
    13  	utils "github.com/diadata-org/diadata/pkg/utils"
    14  	"github.com/zekroTJA/timedmap"
    15  )
    16  
    17  const (
    18  	BinanceUSWsURL = "wss://stream.binance.us:9443/ws"
    19  )
    20  
    21  type BinanceUSPairScraperSet map[*BinanceUSPairScraper]nothing
    22  
    23  // BinanceScraperUS is a Scraper for collecting trades from the Binance websocket API
    24  type BinanceScraperUS struct {
    25  	client *binance.Client
    26  	// signaling channels for session initialization and finishing
    27  	initDone     chan nothing
    28  	shutdown     chan nothing
    29  	shutdownDone chan nothing
    30  	// error handling; to read error or closed, first acquire read lock
    31  	// only cleanup method should hold write lock
    32  	errorLock sync.RWMutex
    33  	error     error
    34  	closed    bool
    35  	// used to keep track of trading pairs that we subscribed to
    36  	// use sync.Maps to concurrently handle multiple pairs
    37  	pairScrapers sync.Map // dia.ExchangePair -> BinanceUSPairScraperSet
    38  	// pairSubscriptions sync.Map // dia.ExchangePair -> string (subscription ID)
    39  	// pairLocks         sync.Map // dia.ExchangePair -> sync.Mutex
    40  	exchangeName string
    41  	chanTrades   chan *dia.Trade
    42  	db           *models.RelDB
    43  }
    44  
    45  // NewBinanceScraperUS returns a new BinanceScraperUS for the given pair
    46  func NewBinanceScraperUS(apiKey string, secretKey string, exchange dia.Exchange, scrape bool, relDB *models.RelDB) *BinanceScraperUS {
    47  	binance.BaseWsMainURL = BinanceUSWsURL
    48  
    49  	s := &BinanceScraperUS{
    50  		client:       binance.NewClient(apiKey, secretKey),
    51  		initDone:     make(chan nothing),
    52  		shutdown:     make(chan nothing),
    53  		shutdownDone: make(chan nothing),
    54  		exchangeName: exchange.Name,
    55  		error:        nil,
    56  		chanTrades:   make(chan *dia.Trade),
    57  		db:           relDB,
    58  	}
    59  
    60  	// establish connection in the background
    61  	if scrape {
    62  		go s.mainLoop()
    63  	}
    64  	return s
    65  }
    66  
    67  func (up *BinanceScraperUS) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) {
    68  	if pair.Symbol == "MIOTA" {
    69  		pair.ForeignName = "M" + pair.ForeignName
    70  	}
    71  	if pair.Symbol == "YOYOW" {
    72  		pair.ForeignName = "YOYOW" + pair.ForeignName[4:]
    73  	}
    74  	if pair.Symbol == "ETHOS" {
    75  		pair.ForeignName = "ETHOS" + pair.ForeignName[3:]
    76  	}
    77  	if pair.Symbol == "WNXM" {
    78  		pair.Symbol = "wNXM"
    79  		pair.ForeignName = "wNXM" + pair.ForeignName[4:]
    80  	}
    81  	return pair, nil
    82  }
    83  
    84  // runs in a goroutine until s is closed
    85  func (s *BinanceScraperUS) mainLoop() {
    86  	close(s.initDone)
    87  	for range s.shutdown { // user requested shutdown
    88  		log.Println("BinanceScraperUS shutting down")
    89  		s.cleanup()
    90  		return
    91  	}
    92  	select {}
    93  }
    94  
    95  func (s *BinanceScraperUS) FillSymbolData(symbol string) (dia.Asset, error) {
    96  	// TO DO
    97  	return dia.Asset{Symbol: symbol}, nil
    98  }
    99  
   100  // closes all connected PairScrapers
   101  // must only be called from mainLoop
   102  func (s *BinanceScraperUS) cleanup() {
   103  	s.errorLock.Lock()
   104  	defer s.errorLock.Unlock()
   105  	// close all channels of PairScraper children
   106  	s.pairScrapers.Range(func(k, v interface{}) bool {
   107  		for ps := range v.(BinanceUSPairScraperSet) {
   108  			ps.closed = true
   109  		}
   110  		s.pairScrapers.Delete(k)
   111  		return true
   112  	})
   113  
   114  	s.closed = true
   115  	close(s.shutdownDone) // signal that shutdown is complete
   116  }
   117  
   118  // Close closes any existing API connections, as well as channels of
   119  // PairScrapers from calls to ScrapePair
   120  func (s *BinanceScraperUS) Close() error {
   121  	if s.closed {
   122  		return errors.New("BinanceScraperUS: Already closed")
   123  	}
   124  	close(s.shutdown)
   125  	<-s.shutdownDone
   126  	s.errorLock.RLock()
   127  	defer s.errorLock.RUnlock()
   128  	return s.error
   129  }
   130  
   131  // ScrapePair returns a PairScraper that can be used to get trades for a single pair from
   132  // this APIScraper
   133  func (s *BinanceScraperUS) ScrapePair(pair dia.ExchangePair) (PairScraper, error) {
   134  	<-s.initDone // wait until client is connected
   135  
   136  	tmFalseDuplicateTrades := timedmap.New(duplicateTradesScanFrequency)
   137  	tmDuplicateTrades := timedmap.New(duplicateTradesScanFrequency)
   138  
   139  	if s.closed {
   140  		return nil, errors.New("BinanceScraperUS: Call ScrapePair on closed scraper")
   141  	}
   142  
   143  	ps := &BinanceUSPairScraper{
   144  		parent: s,
   145  		pair:   pair,
   146  	}
   147  
   148  	wsAggTradeHandler := func(event *binance.WsAggTradeEvent) {
   149  		var exchangepair dia.ExchangePair
   150  
   151  		volume, err := strconv.ParseFloat(event.Quantity, 64)
   152  		price, err2 := strconv.ParseFloat(event.Price, 64)
   153  
   154  		if err == nil && err2 == nil && event.Event == "aggTrade" {
   155  			if !event.IsBuyerMaker {
   156  				volume = -volume
   157  			}
   158  			pairNormalized, _ := s.NormalizePair(pair)
   159  			exchangepair, err = s.db.GetExchangePairCache(s.exchangeName, pair.ForeignName)
   160  			if err != nil {
   161  				log.Error(err)
   162  			}
   163  			t := &dia.Trade{
   164  				Symbol:         pairNormalized.Symbol,
   165  				Pair:           pairNormalized.ForeignName,
   166  				Price:          price,
   167  				Volume:         volume,
   168  				Time:           time.Unix(event.TradeTime/1000, (event.TradeTime%1000)*int64(time.Millisecond)),
   169  				ForeignTradeID: strconv.FormatInt(event.AggTradeID, 16),
   170  				Source:         s.exchangeName,
   171  				VerifiedPair:   exchangepair.Verified,
   172  				BaseToken:      exchangepair.UnderlyingPair.BaseToken,
   173  				QuoteToken:     exchangepair.UnderlyingPair.QuoteToken,
   174  			}
   175  			if exchangepair.Verified {
   176  				log.Infoln("Got verified trade", t)
   177  			}
   178  
   179  			// Handle duplicate trades.
   180  			discardTrade := t.IdentifyDuplicateFull(tmFalseDuplicateTrades, duplicateTradesMemory)
   181  			if !discardTrade {
   182  				t.IdentifyDuplicateTagset(tmDuplicateTrades, duplicateTradesMemory)
   183  				ps.parent.chanTrades <- t
   184  			}
   185  
   186  		} else {
   187  			log.Println("ignoring event ", event, err, err2)
   188  		}
   189  	}
   190  	errHandler := func(err error) {
   191  		log.Error(err)
   192  	}
   193  
   194  	_, _, err := binance.WsAggTradeServe(pair.ForeignName, wsAggTradeHandler, errHandler)
   195  	if err != nil {
   196  		log.Errorf("serving pair %s", pair.ForeignName)
   197  	}
   198  
   199  	return ps, err
   200  }
   201  func (s *BinanceScraperUS) normalizeSymbol(p dia.ExchangePair, foreignName string, params ...string) (pair dia.ExchangePair, err error) {
   202  	pair = p
   203  	return pair, nil
   204  }
   205  
   206  // FetchAvailablePairs returns a list with all available trade pairs
   207  func (s *BinanceScraperUS) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) {
   208  
   209  	data, _, err := utils.GetRequest("https://api.binance.us/api/v1/exchangeInfo")
   210  
   211  	if err != nil {
   212  		return
   213  	}
   214  	var ar binance.ExchangeInfo
   215  	err = json.Unmarshal(data, &ar)
   216  	if err == nil {
   217  		for _, p := range ar.Symbols {
   218  			pairToNormalise := dia.ExchangePair{
   219  				Symbol:      p.BaseAsset,
   220  				ForeignName: p.Symbol,
   221  				Exchange:    s.exchangeName,
   222  			}
   223  			pair, serr := s.normalizeSymbol(pairToNormalise, p.BaseAsset, p.Status)
   224  			if serr == nil {
   225  				pairs = append(pairs, pair)
   226  			} else {
   227  				log.Error(serr)
   228  			}
   229  		}
   230  	}
   231  	return
   232  }
   233  
   234  // BinanceUSPairScraper implements PairScraper for Binance
   235  type BinanceUSPairScraper struct {
   236  	parent *BinanceScraperUS
   237  	pair   dia.ExchangePair
   238  	closed bool
   239  }
   240  
   241  // Close stops listening for trades of the pair associated with s
   242  func (ps *BinanceUSPairScraper) Close() error {
   243  	var err error
   244  	s := ps.parent
   245  	// if parent already errored, return early
   246  	s.errorLock.RLock()
   247  	defer s.errorLock.RUnlock()
   248  	if s.error != nil {
   249  		return s.error
   250  	}
   251  	if ps.closed {
   252  		return errors.New("BinanceUSPairScraper: Already closed")
   253  	}
   254  
   255  	// TODO stop collection for the pair
   256  
   257  	ps.closed = true
   258  	return err
   259  }
   260  
   261  // Channel returns a channel that can be used to receive trades
   262  func (ps *BinanceScraperUS) Channel() chan *dia.Trade {
   263  	return ps.chanTrades
   264  }
   265  
   266  // Error returns an error when the channel Channel() is closed
   267  // and nil otherwise
   268  func (ps *BinanceUSPairScraper) Error() error {
   269  	s := ps.parent
   270  	s.errorLock.RLock()
   271  	defer s.errorLock.RUnlock()
   272  	return s.error
   273  }
   274  
   275  // Pair returns the pair this scraper is subscribed to
   276  func (ps *BinanceUSPairScraper) Pair() dia.ExchangePair {
   277  	return ps.pair
   278  }