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

     1  package scrapers
     2  
     3  import (
     4  	"errors"
     5  	"strconv"
     6  	"strings"
     7  	"sync"
     8  	"time"
     9  
    10  	"github.com/diadata-org/diadata/pkg/dia"
    11  	models "github.com/diadata-org/diadata/pkg/model"
    12  	ws "github.com/gorilla/websocket"
    13  	"github.com/zekroTJA/timedmap"
    14  )
    15  
    16  const (
    17  	bitgetWsAPI        = "wss://ws.bitget.com/v2/ws/public"
    18  	bitgetPingInterval = 30
    19  )
    20  
    21  type bitgetSubscribeMessage struct {
    22  	Operation string                     `json:"op"`
    23  	Arguments []bitgetSubscribeArguments `json:"args"`
    24  }
    25  
    26  type bitgetSubscribeArguments struct {
    27  	InstrumentType string `json:"instType"`
    28  	Channel        string `json:"channel"`
    29  	InstrumentID   string `json:"instId"`
    30  }
    31  
    32  type bitgetWsResponse struct {
    33  	Action    string         `json:"action"`
    34  	Argument  bitgetArgument `json:"arg"`
    35  	Data      []bitgetData   `json:"data"`
    36  	Timestamp int64          `json:"ts"`
    37  }
    38  
    39  type bitgetArgument struct {
    40  	InstrumentType string `json:"instType"`
    41  	Channel        string `json:"channel"`
    42  	InstrumentID   string `json:"instId"`
    43  }
    44  
    45  type bitgetData struct {
    46  	Timestamp      string `json:"ts"`
    47  	Price          string `json:"price"`
    48  	Volume         string `json:"size"`
    49  	Side           string `json:"side"`
    50  	ForeignTradeID string `json:"tradeId"`
    51  }
    52  
    53  type BitgetScraper struct {
    54  	// signaling channels
    55  	shutdown     chan nothing
    56  	shutdownDone chan nothing
    57  	// error handling; to read error or closed, first acquire read lock
    58  	// only cleanup method should hold write lock
    59  	errorLock    sync.RWMutex
    60  	error        error
    61  	closed       bool
    62  	pairScrapers map[string]*BitgetPairScraper // pc.ExchangePair -> pairScraperSet
    63  	wsConn       *ws.Conn
    64  	exchangeName string
    65  	chanTrades   chan *dia.Trade
    66  	db           *models.RelDB
    67  }
    68  
    69  // NewBitgetScraper returns a new BitgetScraper initialized with default values.
    70  // The instance is asynchronously scraping as soon as it is created.
    71  func NewBitgetScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *BitgetScraper {
    72  	s := &BitgetScraper{
    73  		shutdown:     make(chan nothing),
    74  		shutdownDone: make(chan nothing),
    75  		pairScrapers: make(map[string]*BitgetPairScraper),
    76  		exchangeName: exchange.Name,
    77  		error:        nil,
    78  		chanTrades:   make(chan *dia.Trade),
    79  		db:           relDB,
    80  	}
    81  	var wsDialer ws.Dialer
    82  	SwConn, _, err := wsDialer.Dial(bitgetWsAPI, nil)
    83  	if err != nil {
    84  		log.Errorf("Dial websocket api: %s", err.Error())
    85  	}
    86  
    87  	go s.pingRoutine(time.Duration(bitgetPingInterval * time.Second))
    88  
    89  	s.wsConn = SwConn
    90  	if scrape {
    91  		go s.mainLoop()
    92  	}
    93  	return s
    94  }
    95  
    96  // mainLoop runs in a goroutine until channel s is closed.
    97  func (s *BitgetScraper) mainLoop() {
    98  
    99  	tmFalseDuplicateTrades := timedmap.New(duplicateTradesScanFrequency)
   100  	tmDuplicateTrades := timedmap.New(duplicateTradesScanFrequency)
   101  	time.Sleep(5 * time.Second)
   102  
   103  	for {
   104  
   105  		// Check if we get a pong message back.
   106  		_, p, err := s.wsConn.ReadMessage()
   107  		if err != nil {
   108  			log.Error("ReadMessage: ", err)
   109  		} else {
   110  			if strings.Contains(string(p), "pong") || strings.Contains(string(p), "ng") {
   111  				log.Infof("got  %s", string(p))
   112  			}
   113  		}
   114  
   115  		var message bitgetWsResponse
   116  		if err = s.wsConn.ReadJSON(&message); err != nil {
   117  			log.Errorf("ReadJSON: %s", err.Error())
   118  			log.Info("instead of pong got ", string(p))
   119  			if strings.Contains(err.Error(), "invalid character") {
   120  				continue
   121  			}
   122  			return
   123  		}
   124  
   125  		ps, ok := s.pairScrapers[message.Argument.InstrumentID]
   126  		if ok && message.Action != "snapshot" {
   127  			for _, data := range message.Data {
   128  				var f64Price float64
   129  				var f64Volume float64
   130  				var exchangepair dia.ExchangePair
   131  				f64Price, err = strconv.ParseFloat(data.Price, 64)
   132  				if err != nil {
   133  					log.Error("error parsing price " + data.Price)
   134  				}
   135  				f64Volume, err = strconv.ParseFloat(data.Volume, 64)
   136  				if err != nil {
   137  					log.Error("error parsing volume " + data.Volume)
   138  				}
   139  
   140  				if data.Side != "buy" {
   141  					f64Volume = -f64Volume
   142  				}
   143  
   144  				timestamp, err := strconv.ParseInt(data.Timestamp, 10, 64)
   145  				if err != nil {
   146  					log.Error("Parse timestamp: ", err)
   147  				}
   148  
   149  				exchangepair, err = s.db.GetExchangePairCache(s.exchangeName, message.Argument.InstrumentID)
   150  				if err != nil {
   151  					// log.Error("get exchangepair from cache: ", err)
   152  				}
   153  				t := dia.Trade{
   154  					Symbol:         ps.pair.Symbol,
   155  					Pair:           message.Argument.InstrumentID,
   156  					Price:          f64Price,
   157  					Volume:         f64Volume,
   158  					Time:           time.Unix(0, timestamp*1e6),
   159  					ForeignTradeID: data.ForeignTradeID,
   160  					Source:         s.exchangeName,
   161  					VerifiedPair:   exchangepair.Verified,
   162  					BaseToken:      exchangepair.UnderlyingPair.BaseToken,
   163  					QuoteToken:     exchangepair.UnderlyingPair.QuoteToken,
   164  				}
   165  				if t.VerifiedPair {
   166  					log.Info("got verified trade: ", t)
   167  				} else {
   168  					log.Infof("got trade at %v : %s -- %v -- %v", t.Time, t.Pair, t.Price, t.Volume)
   169  				}
   170  				// Handle duplicate trades.
   171  				discardTrade := t.IdentifyDuplicateFull(tmFalseDuplicateTrades, duplicateTradesMemory)
   172  				if !discardTrade {
   173  					t.IdentifyDuplicateTagset(tmDuplicateTrades, duplicateTradesMemory)
   174  					ps.parent.chanTrades <- &t
   175  				}
   176  			}
   177  
   178  		}
   179  	}
   180  
   181  }
   182  
   183  func (s *BitgetScraper) cleanup(err error) {
   184  	s.errorLock.Lock()
   185  	defer s.errorLock.Unlock()
   186  	if err != nil {
   187  		s.error = err
   188  	}
   189  	s.closed = true
   190  	close(s.shutdownDone) // signal that shutdown is complete
   191  }
   192  
   193  // Close closes any existing API connections, as well as channels of
   194  // PairScrapers from calls to ScrapePair
   195  func (s *BitgetScraper) Close() error {
   196  	if s.closed {
   197  		return errors.New("BitgetScraper: Already closed")
   198  	}
   199  	err := s.wsConn.Close()
   200  	if err != nil {
   201  		log.Error(err)
   202  	}
   203  	close(s.shutdown)
   204  	<-s.shutdownDone
   205  	s.errorLock.RLock()
   206  	defer s.errorLock.RUnlock()
   207  	return s.error
   208  }
   209  
   210  func (s *BitgetScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) {
   211  	return pair, nil
   212  }
   213  
   214  func (s *BitgetScraper) pingRoutine(d time.Duration) {
   215  	ticker := time.NewTicker(d)
   216  	for range ticker.C {
   217  		if err := s.wsConn.WriteMessage(ws.TextMessage, []byte("ping")); err != nil {
   218  			log.Errorf("send ping: %s.", err.Error())
   219  		} else {
   220  			log.Info("sent ping.")
   221  		}
   222  	}
   223  }
   224  
   225  // FetchAvailablePairs returns a list with all available trade pairs
   226  func (s *BitgetScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) {
   227  
   228  	// data, _, err := utils.GetRequest("https://api.pro.coinbase.com/products")
   229  	// if err != nil {
   230  	// 	return
   231  	// }
   232  
   233  	// err = json.Unmarshal(data, &ar)
   234  	// if err == nil {
   235  	// 	for _, p := range ar {
   236  	// 		pairToNormalise := dia.ExchangePair{
   237  	// 			Symbol:      p.BaseCurrency,
   238  	// 			ForeignName: p.ID,
   239  	// 			Exchange:    s.exchangeName,
   240  	// 		}
   241  	// 		pair, serr := s.NormalizePair(pairToNormalise)
   242  	// 		if serr == nil {
   243  	// 			pairs = append(pairs, pair)
   244  	// 		} else {
   245  	// 			log.Error(serr)
   246  	// 		}
   247  	// 	}
   248  	// }
   249  	return
   250  }
   251  
   252  // FillSymbolData collects all available information on an asset traded on Bitget
   253  func (s *BitgetScraper) FillSymbolData(symbol string) (asset dia.Asset, err error) {
   254  	asset.Symbol = symbol
   255  	return asset, nil
   256  }
   257  
   258  // BitgetPairScraper implements PairScraper
   259  type BitgetPairScraper struct {
   260  	parent     *BitgetScraper
   261  	pair       dia.ExchangePair
   262  	closed     bool
   263  	lastRecord int64
   264  }
   265  
   266  // ScrapePair returns a PairScraper that can be used to get trades for a single pair from
   267  // this APIScraper
   268  func (s *BitgetScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) {
   269  
   270  	s.errorLock.RLock()
   271  	defer s.errorLock.RUnlock()
   272  	if s.error != nil {
   273  		return nil, s.error
   274  	}
   275  	if s.closed {
   276  		return nil, errors.New("BitgetScraper: Call ScrapePair on closed scraper")
   277  	}
   278  	ps := &BitgetPairScraper{
   279  		parent:     s,
   280  		pair:       pair,
   281  		lastRecord: 0,
   282  	}
   283  
   284  	s.pairScrapers[pair.ForeignName] = ps
   285  
   286  	subscribeMessage := bitgetSubscribeMessage{
   287  		Operation: "subscribe",
   288  		Arguments: []bitgetSubscribeArguments{
   289  			{
   290  				InstrumentType: "SPOT",
   291  				Channel:        "trade",
   292  				InstrumentID:   pair.ForeignName,
   293  			},
   294  		},
   295  	}
   296  	if err := s.wsConn.WriteJSON(subscribeMessage); err != nil {
   297  		println(err.Error())
   298  	}
   299  	log.Info("subscribed to: ", pair.ForeignName)
   300  
   301  	return ps, nil
   302  }
   303  
   304  // Channel returns a channel that can be used to receive trades/pricing information
   305  func (ps *BitgetScraper) Channel() chan *dia.Trade {
   306  	return ps.chanTrades
   307  }
   308  
   309  func (ps *BitgetPairScraper) Close() error {
   310  	ps.closed = true
   311  	return nil
   312  }
   313  
   314  // Error returns an error when the channel Channel() is closed
   315  // and nil otherwise
   316  func (ps *BitgetPairScraper) Error() error {
   317  	s := ps.parent
   318  	s.errorLock.RLock()
   319  	defer s.errorLock.RUnlock()
   320  	return s.error
   321  }
   322  
   323  // Pair returns the pair this scraper is subscribed to
   324  func (ps *BitgetPairScraper) Pair() dia.ExchangePair {
   325  	return ps.pair
   326  }