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

     1  package scrapers
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"strings"
     9  	"sync"
    10  	"time"
    11  
    12  	ws "github.com/gorilla/websocket"
    13  	"github.com/zekroTJA/timedmap"
    14  	"go.uber.org/ratelimit"
    15  
    16  	"github.com/diadata-org/diadata/pkg/dia"
    17  	models "github.com/diadata-org/diadata/pkg/model"
    18  	"github.com/diadata-org/diadata/pkg/utils"
    19  )
    20  
    21  const (
    22  	bitMexAPIEndpoint = "https://www.bitMex.com/api/v1"
    23  	bitMexWSEndpoint  = "wss://ws.bitMex.com/realtime"
    24  
    25  	// bitMexWSRateLimitPerSec is a max request per second for sending websocket requests
    26  	bitMexWSRateLimitPerSec = 10
    27  
    28  	// bitMexTaskMaxRetry is a max retry value used when retrying subscribe/unsubscribe trades
    29  	bitMexTaskMaxRetry = 20
    30  
    31  	// bitMexConnMaxRetry is a max retry value used when retrying to create a new connection
    32  	bitMexConnMaxRetry = 50
    33  
    34  	// bitMexRateLimitError is a rate limit error code
    35  	bitMexRateLimitError = 429
    36  
    37  	// bitMexPingInterval is the number of seconds between ping messages
    38  	bitMexPingInterval = 25
    39  )
    40  
    41  // bitMexWSTask is a websocket task tracking subscription/unsubscription
    42  type bitMexWSTask struct {
    43  	Op         string
    44  	Args       []string
    45  	RetryCount int
    46  }
    47  
    48  func (c *bitMexWSTask) toString() string {
    49  	return fmt.Sprintf("op=%s, param=%s, retry=%d", c.Op, c.Args, c.RetryCount)
    50  }
    51  
    52  // bitMexWSRequest is a websocket request
    53  type bitMexWSRequest struct {
    54  	Op   string   `json:"op"`
    55  	Args []string `json:"args,omitempty"`
    56  }
    57  
    58  // bitMexSubscriptionResult is a subscription result coming from websocket
    59  type bitMexSubscriptionResult struct {
    60  	Success   bool            `json:"success"`
    61  	Subscribe string          `json:"subscribe"`
    62  	Error     string          `json:"error"`
    63  	Status    int             `json:"status"`
    64  	Request   json.RawMessage `json:"request"`
    65  	Table     string          `json:"table"`
    66  	Trades    []bitMexWSTrade `json:"data"`
    67  }
    68  
    69  // bitMexWSTrade is a trade result coming from websocket
    70  type bitMexWSTrade struct {
    71  	Timestamp       time.Time `json:"timestamp"`
    72  	Symbol          string    `json:"symbol"`
    73  	Side            string    `json:"side"`
    74  	Size            float64   `json:"size"`
    75  	Price           float64   `json:"price"`
    76  	TickDirection   string    `json:"tickDirection"`
    77  	TrdMatchID      string    `json:"trdMatchID"`
    78  	GrossValue      float64   `json:"grossValue"`
    79  	HomeNotional    float64   `json:"homeNotional"`
    80  	ForeignNotional float64   `json:"foreignNotional"`
    81  }
    82  
    83  // bitMexInstrument represents a trading pair
    84  type bitMexInstrument struct {
    85  	Symbol     string    `json:"symbol"`
    86  	RootSymbol string    `json:"rootSymbol"`
    87  	Expiry     time.Time `json:"expiry"`
    88  }
    89  
    90  // BitMexScraper is a scraper for bitmex.com
    91  type BitMexScraper struct {
    92  	ws *ws.Conn
    93  	rl ratelimit.Limiter
    94  
    95  	// signaling channels for session initialization and finishing
    96  	shutdown           chan nothing
    97  	shutdownDone       chan nothing
    98  	signalShutdown     sync.Once
    99  	signalShutdownDone sync.Once
   100  
   101  	// error handling; err should be read from error(), closed should be read from isClosed()
   102  	// those two methods implement RW lock
   103  	errMutex    sync.RWMutex
   104  	err         error
   105  	closedMutex sync.RWMutex
   106  	closed      bool
   107  	//consecutiveErrCount int
   108  
   109  	// used to keep track of trading pairs that we subscribed to
   110  	pairScrapers    sync.Map
   111  	exchangeName    string
   112  	chanTrades      chan *dia.Trade
   113  	db              *models.RelDB
   114  	tasks           sync.Map
   115  	pingTicker      *time.Ticker
   116  	stopPingRoutine chan bool
   117  
   118  	// used to handle connection retry
   119  	connMutex      sync.RWMutex
   120  	connRetryCount int
   121  }
   122  
   123  // NewBitMexScraper returns a new BitMex scraper
   124  func NewBitMexScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *BitMexScraper {
   125  	s := &BitMexScraper{
   126  		shutdown:        make(chan nothing),
   127  		shutdownDone:    make(chan nothing),
   128  		exchangeName:    exchange.Name,
   129  		err:             nil,
   130  		chanTrades:      make(chan *dia.Trade),
   131  		db:              relDB,
   132  		pingTicker:      time.NewTicker(bitMexPingInterval * time.Second),
   133  		stopPingRoutine: make(chan bool),
   134  	}
   135  
   136  	if err := s.newConn(); err != nil {
   137  		log.Error(err)
   138  
   139  		return nil
   140  	}
   141  
   142  	s.rl = ratelimit.New(bitMexWSRateLimitPerSec)
   143  
   144  	if scrape {
   145  		go s.mainLoop()
   146  	}
   147  
   148  	return s
   149  }
   150  
   151  // Close unsubscribes data and closes any existing WebSocket connections, as well as channels of BitMexScraper
   152  func (s *BitMexScraper) Close() error {
   153  	if s.isClosed() {
   154  		return errors.New("BitMexScraper: Already closed")
   155  	}
   156  
   157  	s.signalShutdown.Do(func() {
   158  		close(s.shutdown)
   159  	})
   160  
   161  	<-s.shutdownDone
   162  
   163  	return s.error()
   164  }
   165  
   166  // Channel returns a channel that can be used to receive trades
   167  func (s *BitMexScraper) Channel() chan *dia.Trade {
   168  	return s.chanTrades
   169  }
   170  
   171  // FetchAvailablePairs returns all traded pairs on BitMex
   172  func (s *BitMexScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) {
   173  
   174  	data, _, err := utils.GetRequest(bitMexAPIEndpoint + "/instrument")
   175  	if err != nil {
   176  		return nil, err
   177  	}
   178  
   179  	var res []bitMexInstrument
   180  	if err := json.Unmarshal(data, &res); err != nil {
   181  		return nil, err
   182  	}
   183  
   184  	for _, i := range res {
   185  
   186  		// fmt.Printf(`
   187  		// {
   188  		//     "Symbol": "%s",
   189  		//     "ForeignName": "%s",
   190  		//     "Exchange": "BitMex",
   191  		//     "Ignore": false
   192  		// },
   193  		// `, i.RootSymbol, i.RootSymbol+"_"+strings.TrimPrefix(i.Symbol, i.RootSymbol))
   194  
   195  		pairs = append(pairs, dia.ExchangePair{
   196  			Symbol:      i.RootSymbol,
   197  			ForeignName: i.RootSymbol + "_" + strings.TrimPrefix(i.Symbol, i.RootSymbol),
   198  			Exchange:    s.exchangeName,
   199  		})
   200  	}
   201  
   202  	return pairs, nil
   203  }
   204  
   205  // FillSymbolData adds the name to the asset underlying @symbol on BitMex
   206  func (s *BitMexScraper) FillSymbolData(symbol string) (dia.Asset, error) {
   207  	return dia.Asset{Symbol: symbol}, nil
   208  }
   209  
   210  func (s *BitMexScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) {
   211  	return pair, nil
   212  }
   213  
   214  // ScrapePair returns a PairScraper that can be used to get trades for a single pair from the BitMex scraper
   215  func (s *BitMexScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) {
   216  
   217  	if err := s.error(); err != nil {
   218  		return nil, err
   219  	}
   220  
   221  	if s.isClosed() {
   222  		return nil, errors.New("BitMexScraper: Call ScrapePair on closed scraper")
   223  	}
   224  
   225  	ps := &BitMexPairScraper{
   226  		parent: s,
   227  		pair:   pair,
   228  	}
   229  
   230  	if err := s.subscribe([]dia.ExchangePair{pair}); err != nil {
   231  		return nil, err
   232  	}
   233  
   234  	return ps, nil
   235  }
   236  
   237  func (s *BitMexScraper) startPing() {
   238  	for {
   239  		select {
   240  		case <-s.stopPingRoutine:
   241  			return
   242  		case <-s.pingTicker.C:
   243  			err := s.ping()
   244  			if err != nil {
   245  				log.Error("ping brought error ", err)
   246  			}
   247  		}
   248  	}
   249  }
   250  
   251  func (s *BitMexScraper) mainLoop() {
   252  	defer s.cleanup()
   253  
   254  	go s.startPing()
   255  
   256  	for {
   257  		select {
   258  		case <-s.shutdown:
   259  			log.Warn("BitMexScraper: Shutting down main loop")
   260  		default:
   261  		}
   262  
   263  		_, msg, err := s.wsConn().ReadMessage()
   264  		if err != nil {
   265  
   266  			log.Warnf("BitMexScraper: Creating a new connection caused by err=%s", err.Error())
   267  
   268  			if retryErr := s.retryConnection(); retryErr != nil {
   269  				s.setError(retryErr)
   270  				log.Errorf("BitMexScraper: Shutting down main loop after retrying to create a new connection, err=%s", retryErr.Error())
   271  			}
   272  
   273  			log.Info("BitMexScraper: Successfully created a new connection")
   274  			continue
   275  
   276  		}
   277  
   278  		if string(msg) == "pong" {
   279  			continue
   280  		}
   281  
   282  		var subResult bitMexSubscriptionResult
   283  		if err := json.Unmarshal(msg, &subResult); err == nil {
   284  
   285  			if subResult.Status == 400 {
   286  				log.Warning(bytes.NewBuffer(msg))
   287  			}
   288  			if subResult.Table == "trade" {
   289  				s.handleTrades(subResult)
   290  				continue
   291  			}
   292  
   293  			if subResult.Success {
   294  				// subscription Success
   295  				continue
   296  			}
   297  			if subResult.Status == bitMexRateLimitError {
   298  
   299  				var failedRequest bitMexWSRequest
   300  				if errUnmarshal := json.Unmarshal(subResult.Request, &failedRequest); errUnmarshal == nil {
   301  
   302  					task := bitMexWSTask{
   303  						Op:   failedRequest.Op,
   304  						Args: failedRequest.Args,
   305  					}
   306  					if errRetryTask := s.retryTask(s.getTaskID(task)); err != nil {
   307  						s.setError(errRetryTask)
   308  						log.Errorf("BitMexScraper: Shutting down main loop due to failing to retry a task, err=%s", errRetryTask.Error())
   309  
   310  					}
   311  
   312  				}
   313  
   314  				continue
   315  			}
   316  
   317  		} else {
   318  			log.Println(err)
   319  		}
   320  
   321  	}
   322  
   323  }
   324  
   325  func (s *BitMexScraper) handleTrades(tradesWsResponse bitMexSubscriptionResult) {
   326  	var pair dia.ExchangePair
   327  	tmFalseDuplicateTrades := timedmap.New(duplicateTradesScanFrequency)
   328  	tmDuplicateTrades := timedmap.New(duplicateTradesScanFrequency)
   329  
   330  	for _, data := range tradesWsResponse.Trades {
   331  
   332  		if pair == (dia.ExchangePair{}) {
   333  			val, ok := s.pairScrapers.Load(data.Symbol)
   334  			if !ok {
   335  				log.Error("Pair not found %s", data.Symbol)
   336  				continue
   337  			} else {
   338  				pair = val.(dia.ExchangePair)
   339  			}
   340  		}
   341  
   342  		volume := data.HomeNotional
   343  		if data.Side == "Sell" {
   344  			volume = -volume
   345  		}
   346  
   347  		exchangepair, err := s.db.GetExchangePairCache(s.exchangeName, pair.ForeignName)
   348  		if err != nil {
   349  			log.Error("get exchangepair from cache: ", err)
   350  		}
   351  		trade := &dia.Trade{
   352  			Symbol:         pair.Symbol,
   353  			Pair:           pair.ForeignName,
   354  			Price:          data.Price,
   355  			Time:           data.Timestamp,
   356  			Volume:         volume,
   357  			Source:         s.exchangeName,
   358  			ForeignTradeID: data.TrdMatchID,
   359  			VerifiedPair:   exchangepair.Verified,
   360  			BaseToken:      exchangepair.UnderlyingPair.BaseToken,
   361  			QuoteToken:     exchangepair.UnderlyingPair.QuoteToken,
   362  		}
   363  		if exchangepair.Verified {
   364  			log.Infoln("Got verified trade", trade)
   365  		}
   366  		// Handle duplicate trades.
   367  		discardTrade := trade.IdentifyDuplicateFull(tmFalseDuplicateTrades, duplicateTradesMemory)
   368  		if !discardTrade {
   369  			trade.IdentifyDuplicateTagset(tmDuplicateTrades, duplicateTradesMemory)
   370  			select {
   371  			case <-s.shutdown:
   372  			case s.chanTrades <- trade:
   373  			}
   374  		}
   375  
   376  	}
   377  }
   378  
   379  func (s *BitMexScraper) newConn() error {
   380  	conn, _, err := ws.DefaultDialer.Dial(bitMexWSEndpoint, nil)
   381  	if err != nil {
   382  		return err
   383  	}
   384  
   385  	defer s.connMutex.Unlock()
   386  	s.connMutex.Lock()
   387  	s.ws = conn
   388  
   389  	return nil
   390  }
   391  
   392  func (s *BitMexScraper) wsConn() *ws.Conn {
   393  	defer s.connMutex.RUnlock()
   394  	s.connMutex.RLock()
   395  
   396  	return s.ws
   397  }
   398  
   399  func (s *BitMexScraper) ping() error {
   400  	s.rl.Take()
   401  
   402  	return s.wsConn().WriteMessage(ws.TextMessage, []byte("ping"))
   403  }
   404  
   405  func (s *BitMexScraper) cleanup() {
   406  	s.pingTicker.Stop()
   407  	s.stopPingRoutine <- true
   408  	if err := s.wsConn().Close(); err != nil {
   409  		s.setError(err)
   410  	}
   411  
   412  	close(s.chanTrades)
   413  	s.close()
   414  	s.signalShutdownDone.Do(func() {
   415  		close(s.shutdownDone)
   416  	})
   417  }
   418  
   419  func (s *BitMexScraper) error() error {
   420  	s.errMutex.RLock()
   421  	defer s.errMutex.RUnlock()
   422  
   423  	return s.err
   424  }
   425  
   426  func (s *BitMexScraper) setError(err error) {
   427  	s.errMutex.Lock()
   428  	defer s.errMutex.Unlock()
   429  
   430  	s.err = err
   431  }
   432  
   433  func (s *BitMexScraper) isClosed() bool {
   434  	s.closedMutex.RLock()
   435  	defer s.closedMutex.RUnlock()
   436  
   437  	return s.closed
   438  }
   439  
   440  func (s *BitMexScraper) close() {
   441  	s.closedMutex.Lock()
   442  	defer s.closedMutex.Unlock()
   443  
   444  	s.closed = true
   445  }
   446  
   447  func (s *BitMexScraper) subscribe(pairs []dia.ExchangePair) error {
   448  	channels := make([]string, len(pairs))
   449  	for idx, pair := range pairs {
   450  		bitMexInstrumentSymbol := strings.Replace(pair.ForeignName, "_", "", 1)
   451  		channels[idx] = "trade:" + bitMexInstrumentSymbol
   452  		s.pairScrapers.Store(bitMexInstrumentSymbol, pair)
   453  	}
   454  
   455  	task := bitMexWSTask{
   456  		Op:   "subscribe",
   457  		Args: channels,
   458  
   459  		RetryCount: 0,
   460  	}
   461  	taskID := s.getTaskID(task)
   462  	s.tasks.Store(taskID, task)
   463  
   464  	return s.send(task)
   465  }
   466  
   467  func (s *BitMexScraper) getTaskID(task bitMexWSTask) string {
   468  	return fmt.Sprintf("%s-%s", task.Op, strings.Join(task.Args, ","))
   469  }
   470  
   471  func (s *BitMexScraper) unsubscribe(pairs []dia.ExchangePair) error {
   472  	channels := make([]string, len(pairs))
   473  	for idx, pair := range pairs {
   474  		channels[idx] = "trade." + pair.ForeignName
   475  		s.pairScrapers.Delete(pair.ForeignName)
   476  	}
   477  
   478  	task := bitMexWSTask{
   479  		Op:         "unsubscribe",
   480  		Args:       channels,
   481  		RetryCount: 0,
   482  	}
   483  	taskID := s.getTaskID(task)
   484  	s.tasks.Store(taskID, task)
   485  
   486  	return s.send(task)
   487  }
   488  
   489  func (s *BitMexScraper) retryConnection() error {
   490  	s.connRetryCount += 1
   491  	if s.connRetryCount > bitMexConnMaxRetry {
   492  		return errors.New("BitMexPairScraper: Reached max retry connection")
   493  	}
   494  	if err := s.wsConn().Close(); err != nil {
   495  		return err
   496  	}
   497  	if err := s.newConn(); err != nil {
   498  		return err
   499  	}
   500  
   501  	var pairs []dia.ExchangePair
   502  	s.pairScrapers.Range(func(key, value interface{}) bool {
   503  		pair := value.(dia.ExchangePair)
   504  		pairs = append(pairs, pair)
   505  		return true
   506  	})
   507  	if err := s.subscribe(pairs); err != nil {
   508  		return err
   509  	}
   510  
   511  	return nil
   512  }
   513  
   514  func (s *BitMexScraper) retryTask(taskID string) error {
   515  	val, ok := s.tasks.Load(taskID)
   516  	if !ok {
   517  		return fmt.Errorf("BitMexScraper: Facing unknown task id, taskId=%v", taskID)
   518  	}
   519  
   520  	task := val.(bitMexWSTask)
   521  	task.RetryCount += 1
   522  	if task.RetryCount > bitMexTaskMaxRetry {
   523  		return fmt.Errorf("BitMexScraper: Exeeding max retry, taskId=%v, %s", taskID, task.toString())
   524  	}
   525  
   526  	log.Warnf("BitMexScraper: Retrying a task, taskId=%v, %s", taskID, task.toString())
   527  	s.tasks.Store(taskID, task)
   528  
   529  	return s.send(task)
   530  }
   531  
   532  func (s *BitMexScraper) send(task bitMexWSTask) error {
   533  	s.rl.Take()
   534  
   535  	return s.wsConn().WriteJSON(&bitMexWSRequest{
   536  		Op:   task.Op,
   537  		Args: task.Args,
   538  	})
   539  }
   540  
   541  // BitMexPairScraper implements PairScraper for BitMex
   542  type BitMexPairScraper struct {
   543  	parent *BitMexScraper
   544  	pair   dia.ExchangePair
   545  	closed bool
   546  }
   547  
   548  // Error returns an error when the channel Channel() is closed
   549  // and nil otherwise
   550  func (p *BitMexPairScraper) Error() error {
   551  	return p.parent.error()
   552  }
   553  
   554  // Pair returns the pair this scraper is subscribed to
   555  func (p *BitMexPairScraper) Pair() dia.ExchangePair {
   556  	return p.pair
   557  }
   558  
   559  // Close stops listening for trades of the pair associated with the BitMex scraper
   560  func (p *BitMexPairScraper) Close() error {
   561  	if err := p.parent.error(); err != nil {
   562  		return err
   563  	}
   564  	if p.closed {
   565  		return errors.New("BitMexPairScraper: Already closed")
   566  	}
   567  	if err := p.parent.unsubscribe([]dia.ExchangePair{p.pair}); err != nil {
   568  		return err
   569  	}
   570  
   571  	p.closed = true
   572  
   573  	return nil
   574  }