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

     1  package scrapers
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"strconv"
     7  	"strings"
     8  	"sync"
     9  	"time"
    10  
    11  	ws "github.com/gorilla/websocket"
    12  	"github.com/zekroTJA/timedmap"
    13  
    14  	"github.com/diadata-org/diadata/pkg/dia"
    15  	"github.com/diadata-org/diadata/pkg/dia/helpers"
    16  	models "github.com/diadata-org/diadata/pkg/model"
    17  	"github.com/diadata-org/diadata/pkg/utils"
    18  )
    19  
    20  var ByBitSocketURL string = utils.Getenv("BYBIT_WS_URL", "wss://stream.bybit.com/v5/public/spot")
    21  
    22  type ByBitMarket struct {
    23  	Name           string `json:"name"`
    24  	Alias          string `json:"alias"`
    25  	Status         string `json:"status"`
    26  	BaseCurrency   string `json:"base_currency"`
    27  	QuoteCurrency  string `json:"quote_currency"`
    28  	PriceScale     int    `json:"price_scale"`
    29  	TakerFee       string `json:"taker_fee"`
    30  	MakerFee       string `json:"maker_fee"`
    31  	LeverageFilter struct {
    32  		MinLeverage  int    `json:"min_leverage"`
    33  		MaxLeverage  int    `json:"max_leverage"`
    34  		LeverageStep string `json:"leverage_step"`
    35  	} `json:"leverage_filter"`
    36  	PriceFilter struct {
    37  		MinPrice string `json:"min_price"`
    38  		MaxPrice string `json:"max_price"`
    39  		TickSize string `json:"tick_size"`
    40  	} `json:"price_filter"`
    41  	LotSizeFilter struct {
    42  		MaxTradingQty float64 `json:"max_trading_qty"`
    43  		MinTradingQty float64 `json:"min_trading_qty"`
    44  		QtyStep       float64 `json:"qty_step"`
    45  	} `json:"lot_size_filter"`
    46  }
    47  
    48  type ByBitMarketsResponse struct {
    49  	RetCode int           `json:"ret_code"`
    50  	RetMsg  string        `json:"ret_msg"`
    51  	ExtCode string        `json:"ext_code"`
    52  	ExtInfo string        `json:"ext_info"`
    53  	Result  []ByBitMarket `json:"result"`
    54  	TimeNow string        `json:"time_now"`
    55  }
    56  
    57  type ByBitTradeResponse struct {
    58  	Data      []ByBitTradeResponseData `json:"data"`
    59  	Type      string                   `json:"type"`
    60  	Topic     string                   `json:"topic"`
    61  	Timestamp int64                    `json:"ts"`
    62  }
    63  
    64  type ByBitTradeResponseData struct {
    65  	TradeID   string `json:"i"`
    66  	Timestamp int64  `json:"T"`
    67  	Price     string `json:"p"`
    68  	Size      string `json:"v"`
    69  	Side      string `json:"S"`
    70  	Symbol    string `json:"s"`
    71  }
    72  
    73  type ByBitSubscribe struct {
    74  	OP   string   `json:"op"`
    75  	Args []string `json:"args"`
    76  }
    77  
    78  // ByBitScraper provides  methods needed to get Trade information from ByBit
    79  type ByBitScraper struct {
    80  	// control flag for main loop
    81  	run      bool
    82  	wsClient *ws.Conn
    83  
    84  	// signaling channels for session initialization and finishing
    85  	shutdown     chan nothing
    86  	shutdownDone chan nothing
    87  	// error handling; to read error or closed, first acquire read lock
    88  	// only cleanup method should hold write lock
    89  	errorLock sync.RWMutex
    90  	error     error
    91  	closed    bool
    92  	// used to keep track of trading pairs that we subscribed to
    93  	pairScrapers map[string]*ByBitPairScraper
    94  	// exchange name
    95  	exchangeName string
    96  	// channel to send trades
    97  	chanTrades chan *dia.Trade
    98  	db         *models.RelDB
    99  }
   100  
   101  // NewByBitScraper get a scrapper for ByBit exchange
   102  func NewByBitScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *ByBitScraper {
   103  	s := &ByBitScraper{
   104  		shutdown:     make(chan nothing),
   105  		shutdownDone: make(chan nothing),
   106  		pairScrapers: make(map[string]*ByBitPairScraper),
   107  
   108  		exchangeName: exchange.Name,
   109  		error:        nil,
   110  		chanTrades:   make(chan *dia.Trade),
   111  		closed:       false,
   112  		db:           relDB,
   113  	}
   114  
   115  	/*
   116  	    In the case of needing access to private urls.
   117  	    // Create HMAC instance from the secret key
   118  	   	h := hmac.New(sha256.New, []byte(secret))
   119  
   120  	   	// Write Data to it
   121  	   	apiSecretBytes := []byte(secret)
   122  	   	// Generate expires.
   123  	   	expires := int((time.Now().UnixNano() + 1) * 1000)
   124  	   	expiresBytes := []byte(fmt.Sprintf("GET/realtime%d", expires))
   125  	   	data := append(apiSecretBytes, expiresBytes...)
   126  	   	h.Write([]byte(data))
   127  
   128  	   	// Get the signature
   129  	   	signature := hex.EncodeToString(h.Sum(nil))
   130  
   131  	   	// Generate the ws url.
   132  	   	params := fmt.Sprintf("api_key=%s&expires=%d&signature=%s", secret, expires, signature)
   133  	*/
   134  
   135  	// Create the ws connection
   136  	var wsDialer ws.Dialer
   137  
   138  	SwConn, _, err := wsDialer.Dial(ByBitSocketURL, nil)
   139  	if err != nil {
   140  		log.Errorf("Connect to websocket server: %s.", err.Error())
   141  	}
   142  
   143  	s.wsClient = SwConn
   144  
   145  	if scrape {
   146  		go s.mainLoop()
   147  	}
   148  
   149  	return s
   150  }
   151  
   152  func (s *ByBitScraper) ping() {
   153  	a := &ByBitSubscribe{
   154  		OP: "ping",
   155  	}
   156  	log.Infoln("Ping: ", a.OP)
   157  	if err := s.wsClient.WriteJSON(a); err != nil {
   158  		log.Errorf("write ping message: %s.", err.Error())
   159  	}
   160  }
   161  
   162  func (s *ByBitScraper) subscribe(foreignName string) {
   163  	// Subscribing to the all markets at once.
   164  	a := &ByBitSubscribe{
   165  		OP:   "subscribe",
   166  		Args: []string{"publicTrade." + foreignName},
   167  	}
   168  	log.Println("subscribing", a)
   169  	if err := s.wsClient.WriteJSON(a); err != nil {
   170  		log.Errorf("subscribe %v: %v.", a, err.Error())
   171  	}
   172  }
   173  
   174  // runs in a goroutine until s is closed
   175  func (s *ByBitScraper) mainLoop() {
   176  	var err error
   177  
   178  	pingTimer := time.NewTicker(10 * time.Second)
   179  	go func() {
   180  		for range pingTimer.C {
   181  			go s.ping()
   182  		}
   183  	}()
   184  
   185  	tmFalseDuplicateTrades := timedmap.New(duplicateTradesScanFrequency)
   186  	tmDuplicateTrades := timedmap.New(duplicateTradesScanFrequency)
   187  
   188  	for {
   189  
   190  		message := &ByBitTradeResponse{}
   191  		if err = s.wsClient.ReadJSON(&message); err != nil {
   192  			log.Errorf("read ws response %s.", err.Error())
   193  		}
   194  
   195  		// the topic format is something like publicTrade.BTCUSD
   196  		topic := strings.Split(message.Topic, ".")
   197  
   198  		if len(topic) == 2 && topic[0] == "publicTrade" {
   199  			ps, ok := s.pairScrapers[topic[1]]
   200  			if ok {
   201  				var (
   202  					f64Price     float64
   203  					f64Volume    float64
   204  					exchangepair dia.ExchangePair
   205  				)
   206  				for _, mdData := range message.Data {
   207  
   208  					f64Price, err = strconv.ParseFloat(mdData.Price, 64)
   209  					if err != nil {
   210  						log.Error("parse price: ", err)
   211  					}
   212  
   213  					f64Volume, err = strconv.ParseFloat(mdData.Size, 64)
   214  					if err != nil {
   215  						log.Error("parse volume: ", err)
   216  					}
   217  
   218  					timeStamp := time.Unix(0, mdData.Timestamp*1e6)
   219  					if mdData.TradeID != "" {
   220  						if mdData.Side == "Sell" {
   221  							f64Volume = -f64Volume
   222  						}
   223  
   224  						exchangepair, err = s.db.GetExchangePairCache(s.exchangeName, topic[1])
   225  						if err != nil {
   226  							log.Errorf("GetExchangePairCache for %s: %v", topic[1], err)
   227  						}
   228  						t := &dia.Trade{
   229  							Symbol:         ps.pair.Symbol,
   230  							Pair:           topic[1],
   231  							Price:          f64Price,
   232  							Volume:         f64Volume,
   233  							Time:           timeStamp,
   234  							ForeignTradeID: mdData.TradeID,
   235  							Source:         s.exchangeName,
   236  							VerifiedPair:   exchangepair.Verified,
   237  							BaseToken:      exchangepair.UnderlyingPair.BaseToken,
   238  							QuoteToken:     exchangepair.UnderlyingPair.QuoteToken,
   239  						}
   240  						if exchangepair.Verified {
   241  							log.Infoln("Got verified trade: ", t)
   242  						}
   243  						// Handle duplicate trades.
   244  						discardTrade := t.IdentifyDuplicateFull(tmFalseDuplicateTrades, duplicateTradesMemory)
   245  						if !discardTrade {
   246  							t.IdentifyDuplicateTagset(tmDuplicateTrades, duplicateTradesMemory)
   247  							ps.parent.chanTrades <- t
   248  						}
   249  					}
   250  				}
   251  
   252  			} else {
   253  				log.Error("Unknown Pair " + topic[1])
   254  			}
   255  		}
   256  	}
   257  
   258  }
   259  
   260  // Close any existing API connections, as well as channels, and terminates main loop
   261  func (s *ByBitScraper) Close() error {
   262  	if s.closed {
   263  		return errors.New(s.exchangeName + "Scraper: Already closed")
   264  	}
   265  	s.run = false
   266  	<-s.shutdownDone
   267  	s.errorLock.RLock()
   268  	defer s.errorLock.RUnlock()
   269  	return s.error
   270  }
   271  
   272  // ScrapePair returns a PairScraper that can be used to get trades for a single pair from
   273  // this APIScraper
   274  func (s *ByBitScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) {
   275  	if s.closed {
   276  		return nil, errors.New("s.exchangeName+Scraper: Call ScrapePair on closed scraper")
   277  	}
   278  	ps := &ByBitPairScraper{
   279  		parent:      s,
   280  		pair:        pair,
   281  		apiEndPoint: pair.ForeignName,
   282  		latestTrade: 0,
   283  	}
   284  	s.pairScrapers[pair.ForeignName] = ps
   285  	s.subscribe(pair.ForeignName)
   286  	return ps, nil
   287  }
   288  
   289  func (s *ByBitScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) {
   290  	symbol := strings.ToUpper(pair.Symbol)
   291  	pair.Symbol = symbol
   292  
   293  	if helpers.NameForSymbol(symbol) == symbol {
   294  		if !helpers.SymbolIsName(symbol) {
   295  			return pair, errors.New("Foreign name can not be normalized:" + pair.ForeignName + " symbol:" + symbol)
   296  		}
   297  	}
   298  	if helpers.SymbolIsBlackListed(symbol) {
   299  		return pair, errors.New("Symbol is black listed:" + symbol)
   300  	}
   301  	return pair, nil
   302  }
   303  
   304  // Channel returns the channel to get trades
   305  func (s *ByBitScraper) Channel() chan *dia.Trade {
   306  	return s.chanTrades
   307  }
   308  
   309  func (s *ByBitScraper) FillSymbolData(symbol string) (dia.Asset, error) {
   310  	// TO DO
   311  	return dia.Asset{Symbol: symbol}, nil
   312  }
   313  
   314  // FetchAvailablePairs returns a list with all available trade pairs
   315  func (s *ByBitScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) {
   316  
   317  	data, _, err := utils.GetRequest("https://api.bybit.com/v2/public/symbols")
   318  	if err != nil {
   319  		return
   320  	}
   321  	var ar ByBitMarketsResponse
   322  	err = json.Unmarshal(data, &ar)
   323  	if err == nil {
   324  		for _, p := range ar.Result {
   325  			if p.Status != "Trading" {
   326  				continue
   327  			}
   328  			pairToNormalize := dia.ExchangePair{
   329  				Symbol:      p.BaseCurrency,
   330  				ForeignName: p.Name,
   331  				Exchange:    s.exchangeName,
   332  			}
   333  			pair, serr := s.NormalizePair(pairToNormalize)
   334  			if serr == nil {
   335  				pairs = append(pairs, pair)
   336  			} else {
   337  				log.Error(serr)
   338  			}
   339  		}
   340  	}
   341  	return
   342  }
   343  
   344  // Error returns an error when the channel Channel() is closed
   345  // and nil otherwise
   346  func (s *ByBitScraper) Error() error {
   347  	s.errorLock.RLock()
   348  	defer s.errorLock.RUnlock()
   349  	return s.error
   350  }
   351  
   352  // ByBitPairScraper implements PairScraper for ByBit
   353  type ByBitPairScraper struct {
   354  	apiEndPoint string
   355  	parent      *ByBitScraper
   356  	pair        dia.ExchangePair
   357  	closed      bool
   358  	latestTrade int
   359  }
   360  
   361  // Close stops listening for trades of the pair associated
   362  func (ps *ByBitPairScraper) Close() error {
   363  	ps.closed = true
   364  	return ps.Error()
   365  }
   366  
   367  // Error returns an error when the channel Channel() is closed
   368  // and nil otherwise
   369  func (ps *ByBitPairScraper) Error() error {
   370  	ps.parent.errorLock.RLock()
   371  	defer ps.parent.errorLock.RUnlock()
   372  	return ps.parent.error
   373  }
   374  
   375  // Pair returns the pair this scraper is subscribed to
   376  func (ps *ByBitPairScraper) Pair() dia.ExchangePair {
   377  	return ps.pair
   378  }