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

     1  package scrapers
     2  
     3  import (
     4  	"errors"
     5  	"strconv"
     6  	"strings"
     7  	"sync"
     8  	"time"
     9  
    10  	"github.com/Kucoin/kucoin-go-sdk"
    11  	"github.com/diadata-org/diadata/pkg/dia"
    12  	models "github.com/diadata-org/diadata/pkg/model"
    13  	"github.com/zekroTJA/timedmap"
    14  )
    15  
    16  type KuExchangePairs []KuExchangePair
    17  
    18  type KucoinMarketMatch struct {
    19  	Symbol       string `json:"symbol"`
    20  	Sequence     string `json:"sequence"`
    21  	Side         string `json:"side"`
    22  	Size         string `json:"size"`
    23  	Price        string `json:"price"`
    24  	TakerOrderID string `json:"takerOrderId"`
    25  	Time         string `json:"time"`
    26  	Type         string `json:"type"`
    27  	MakerOrderID string `json:"makerOrderId"`
    28  	TradeID      string `json:"tradeId"`
    29  }
    30  
    31  type KucoinCurrency struct {
    32  	Symbol  string `json:"currency"`
    33  	Name    string `json:"fullName"`
    34  	Address string `json:"contractAddress"`
    35  }
    36  
    37  type KuExchangePair struct {
    38  	Symbol          string `json:"symbol"`
    39  	Name            string `json:"name"`
    40  	BaseCurrency    string `json:"baseCurrency"`
    41  	QuoteCurrency   string `json:"quoteCurrency"`
    42  	FeeCurrency     string `json:"feeCurrency"`
    43  	Market          string `json:"market"`
    44  	BaseMinSize     string `json:"baseMinSize"`
    45  	QuoteMinSize    string `json:"quoteMinSize"`
    46  	BaseMaxSize     string `json:"baseMaxSize"`
    47  	QuoteMaxSize    string `json:"quoteMaxSize"`
    48  	BaseIncrement   string `json:"baseIncrement"`
    49  	QuoteIncrement  string `json:"quoteIncrement"`
    50  	PriceIncrement  string `json:"priceIncrement"`
    51  	PriceLimitRate  string `json:"priceLimitRate"`
    52  	IsMarginEnabled bool   `json:"isMarginEnabled"`
    53  	EnableTrading   bool   `json:"enableTrading"`
    54  }
    55  
    56  type KuCoinScraper struct {
    57  	// signaling channels for session initialization and finishing
    58  	initDone     chan nothing
    59  	shutdown     chan nothing
    60  	shutdownDone chan nothing
    61  	// error handling; to read error or closed, first acquire read lock
    62  	// only cleanup method should hold write lock
    63  	errorLock sync.RWMutex
    64  	error     error
    65  	closed    bool
    66  	// used to keep track of trading pairs that we subscribed to
    67  	// use sync.Maps to concurrently handle multiple pairs
    68  	pairScrapers map[string]*KuCoinPairScraper // dia.ExchangePair -> KuCoinPairScraper
    69  	// pairSubscriptions sync.Map                      // dia.ExchangePair -> string (subscription ID)
    70  	// pairLocks         sync.Map                      // dia.ExchangePair -> sync.Mutex
    71  	exchangeName string
    72  	chanTrades   chan *dia.Trade
    73  	apiService   *kucoin.ApiService
    74  	db           *models.RelDB
    75  }
    76  
    77  func NewKuCoinScraper(apiKey string, secretKey string, exchange dia.Exchange, scrape bool, relDB *models.RelDB) *KuCoinScraper {
    78  	apiService := kucoin.NewApiService()
    79  
    80  	s := &KuCoinScraper{
    81  		initDone:     make(chan nothing),
    82  		shutdown:     make(chan nothing),
    83  		shutdownDone: make(chan nothing),
    84  		exchangeName: exchange.Name,
    85  		pairScrapers: make(map[string]*KuCoinPairScraper),
    86  		error:        nil,
    87  		chanTrades:   make(chan *dia.Trade),
    88  		apiService:   apiService,
    89  		db:           relDB,
    90  	}
    91  
    92  	// establish connection in the background
    93  	if scrape {
    94  		go s.mainLoop()
    95  	}
    96  	return s
    97  }
    98  
    99  // runs in a goroutine until s is closed
   100  func (s *KuCoinScraper) mainLoop() {
   101  	var channelsForClient1, channelsForClient2, channelsForClient3 []*kucoin.WebSocketSubscribeMessage
   102  
   103  	close(s.initDone)
   104  
   105  	lastTradeMap := make(map[dia.Pair]time.Time)
   106  	countMap := make(map[dia.Pair]int)
   107  
   108  	rsp, err := s.apiService.WebSocketPublicToken()
   109  	if err != nil {
   110  		// Handle error
   111  		log.Error("Error WebSocketPublicToken", err)
   112  	}
   113  
   114  	tk := &kucoin.WebSocketTokenModel{}
   115  	if err = rsp.ReadData(tk); err != nil {
   116  		log.Error("Error Reading data", err)
   117  	}
   118  
   119  	client1 := s.apiService.NewWebSocketClient(tk)
   120  	client2 := s.apiService.NewWebSocketClient(tk)
   121  	client3 := s.apiService.NewWebSocketClient(tk)
   122  
   123  	client1DownStream, _, err := client1.Connect()
   124  	if err != nil {
   125  		log.Error("Error Reading data", err)
   126  	}
   127  	client2DownStream, _, err := client2.Connect()
   128  	if err != nil {
   129  		log.Error("Error Reading data", err)
   130  	}
   131  	client3DownStream, _, err := client3.Connect()
   132  	if err != nil {
   133  		log.Error("Error Reading data", err)
   134  	}
   135  
   136  	count := 0
   137  	for pair := range s.pairScrapers {
   138  		ch := kucoin.NewSubscribeMessage("/market/match:"+pair, false)
   139  		if count >= 598 {
   140  			channelsForClient3 = append(channelsForClient3, ch)
   141  			count++
   142  			continue
   143  		}
   144  		if count >= 299 {
   145  			channelsForClient2 = append(channelsForClient2, ch)
   146  			count++
   147  			continue
   148  		} else {
   149  			channelsForClient1 = append(channelsForClient1, ch)
   150  			count++
   151  		}
   152  	}
   153  
   154  	log.Info("number of pairs: ", count)
   155  
   156  	if err := client1.Subscribe(channelsForClient1...); err != nil {
   157  		log.Fatal("Error while subscribing client1 ", err)
   158  	}
   159  	if err := client2.Subscribe(channelsForClient2...); err != nil {
   160  		log.Fatal("Error while subscribing client2 ", err)
   161  	}
   162  	if err := client3.Subscribe(channelsForClient3...); err != nil {
   163  		log.Fatal("Error while subscribing client3 ", err)
   164  	}
   165  
   166  	tmFalseDuplicateTrades := timedmap.New(duplicateTradesScanFrequency)
   167  	tmDuplicateTrades := timedmap.New(duplicateTradesScanFrequency)
   168  	go func() {
   169  		var msg *kucoin.WebSocketDownstreamMessage
   170  		for {
   171  			select {
   172  			case msg = <-client1DownStream:
   173  				if msg == nil {
   174  					continue
   175  				}
   176  				t := &KucoinMarketMatch{}
   177  				if err := msg.ReadData(t); err != nil {
   178  					log.Printf("Failure to read: %s", err.Error())
   179  					return
   180  				}
   181  				asset := strings.Split(t.Symbol, "-")
   182  				f64Price, _ := strconv.ParseFloat(t.Price, 64)
   183  				f64Volume, _ := strconv.ParseFloat(t.Size, 64)
   184  				timeOrder, err := strconv.ParseInt(t.Time, 10, 64)
   185  				if err != nil {
   186  					log.Error("parse trade time: ", err)
   187  				}
   188  				// WS returns different lengths of Unix timestamps. Adjust to nanoseconds if returns milliseconds.
   189  				if len(t.Time) == 13 {
   190  					timeOrder *= 1e6
   191  				}
   192  
   193  				if t.Side == "sell" {
   194  					f64Volume = -f64Volume
   195  				}
   196  
   197  				exchangepair, err := s.db.GetExchangePairCache(s.exchangeName, t.Symbol)
   198  				if err != nil {
   199  					log.Error(err)
   200  				}
   201  
   202  				// Make trade times unique
   203  				tradeTime := time.Unix(0, timeOrder)
   204  				pair := dia.Pair{QuoteToken: exchangepair.UnderlyingPair.QuoteToken, BaseToken: exchangepair.UnderlyingPair.BaseToken}
   205  				if _, ok := lastTradeMap[pair]; ok {
   206  					if lastTradeMap[pair] != tradeTime {
   207  						lastTradeMap[pair] = tradeTime
   208  						countMap[pair] = 0
   209  					} else {
   210  						tradeTime = tradeTime.Add(time.Duration((countMap[pair] + 1)) * time.Nanosecond)
   211  						countMap[pair] += 1
   212  					}
   213  				} else {
   214  					lastTradeMap[pair] = tradeTime
   215  				}
   216  
   217  				trade := &dia.Trade{
   218  					Symbol:         asset[0],
   219  					Pair:           t.Symbol,
   220  					Price:          f64Price,
   221  					Time:           tradeTime,
   222  					Volume:         f64Volume,
   223  					Source:         s.exchangeName,
   224  					VerifiedPair:   exchangepair.Verified,
   225  					BaseToken:      exchangepair.UnderlyingPair.BaseToken,
   226  					QuoteToken:     exchangepair.UnderlyingPair.QuoteToken,
   227  					ForeignTradeID: t.TradeID,
   228  				}
   229  				if exchangepair.Verified {
   230  					log.Info("Got verified trade from stream 1: ", trade)
   231  				}
   232  
   233  				// Handle duplicate trades.
   234  				discardTrade := trade.IdentifyDuplicateFull(tmFalseDuplicateTrades, duplicateTradesMemory)
   235  				if !discardTrade {
   236  					trade.IdentifyDuplicateTagset(tmDuplicateTrades, duplicateTradesMemory)
   237  					s.chanTrades <- trade
   238  				}
   239  
   240  			case msg = <-client2DownStream:
   241  				if msg == nil {
   242  					continue
   243  				}
   244  				t := &KucoinMarketMatch{}
   245  				if err := msg.ReadData(t); err != nil {
   246  					log.Errorf("Failure to read: %v", err)
   247  					return
   248  				}
   249  				asset := strings.Split(t.Symbol, "-")
   250  				f64Price, _ := strconv.ParseFloat(t.Price, 64)
   251  				f64Volume, _ := strconv.ParseFloat(t.Size, 64)
   252  				timeOrder, err := strconv.ParseInt(t.Time, 10, 64)
   253  				if err != nil {
   254  					log.Error("parse trade time: ", err)
   255  				}
   256  				// WS returns different lengths of Unix timestamps. Adjust to nanoseconds if returns milliseconds.
   257  				if len(t.Time) == 13 {
   258  					timeOrder *= 1e6
   259  				}
   260  
   261  				if t.Side == "sell" {
   262  					f64Volume = -f64Volume
   263  				}
   264  
   265  				exchangepair, err := s.db.GetExchangePairCache(s.exchangeName, t.Symbol)
   266  				if err != nil {
   267  					log.Error(err)
   268  				}
   269  
   270  				// Make trade times unique
   271  				tradeTime := time.Unix(0, timeOrder)
   272  				pair := dia.Pair{QuoteToken: exchangepair.UnderlyingPair.QuoteToken, BaseToken: exchangepair.UnderlyingPair.BaseToken}
   273  				if _, ok := lastTradeMap[pair]; ok {
   274  					if lastTradeMap[pair] != tradeTime {
   275  						lastTradeMap[pair] = tradeTime
   276  						countMap[pair] = 0
   277  					} else {
   278  						//nolint
   279  						tradeTime.Add(time.Duration(countMap[pair]+1) * time.Nanosecond)
   280  
   281  						countMap[pair] += 1
   282  					}
   283  				} else {
   284  					lastTradeMap[pair] = tradeTime
   285  				}
   286  
   287  				trade := &dia.Trade{
   288  					Symbol:         asset[0],
   289  					Pair:           t.Symbol,
   290  					Price:          f64Price,
   291  					Time:           time.Unix(0, timeOrder),
   292  					Volume:         f64Volume,
   293  					Source:         s.exchangeName,
   294  					VerifiedPair:   exchangepair.Verified,
   295  					BaseToken:      exchangepair.UnderlyingPair.BaseToken,
   296  					QuoteToken:     exchangepair.UnderlyingPair.QuoteToken,
   297  					ForeignTradeID: t.TradeID,
   298  				}
   299  				if exchangepair.Verified {
   300  					log.Info("Got verified trade from stream 2: ", trade)
   301  				}
   302  				s.chanTrades <- trade
   303  
   304  			case msg = <-client3DownStream:
   305  				if msg == nil {
   306  					continue
   307  				}
   308  				t := &KucoinMarketMatch{}
   309  				if err := msg.ReadData(t); err != nil {
   310  					log.Errorf("Failure to read: %v", err)
   311  					return
   312  				}
   313  				asset := strings.Split(t.Symbol, "-")
   314  				f64Price, _ := strconv.ParseFloat(t.Price, 64)
   315  				f64Volume, _ := strconv.ParseFloat(t.Size, 64)
   316  				timeOrder, err := strconv.ParseInt(t.Time, 10, 64)
   317  				if err != nil {
   318  					log.Error("parse trade time: ", err)
   319  				}
   320  				// WS returns different lengths of Unix timestamps. Adjust to nanoseconds if returns milliseconds.
   321  				if len(t.Time) == 13 {
   322  					timeOrder *= 1e6
   323  				}
   324  
   325  				if t.Side == "sell" {
   326  					f64Volume = -f64Volume
   327  				}
   328  
   329  				exchangepair, err := s.db.GetExchangePairCache(s.exchangeName, t.Symbol)
   330  				if err != nil {
   331  					log.Error(err)
   332  				}
   333  
   334  				// Make trade times unique
   335  				tradeTime := time.Unix(0, timeOrder)
   336  				pair := dia.Pair{QuoteToken: exchangepair.UnderlyingPair.QuoteToken, BaseToken: exchangepair.UnderlyingPair.BaseToken}
   337  				if _, ok := lastTradeMap[pair]; ok {
   338  					if lastTradeMap[pair] != tradeTime {
   339  						lastTradeMap[pair] = tradeTime
   340  						countMap[pair] = 0
   341  					} else {
   342  						//nolint
   343  						tradeTime.Add(time.Duration(countMap[pair]+1) * time.Nanosecond)
   344  						countMap[pair] += 1
   345  					}
   346  				} else {
   347  					lastTradeMap[pair] = tradeTime
   348  				}
   349  
   350  				trade := &dia.Trade{
   351  					Symbol:         asset[0],
   352  					Pair:           t.Symbol,
   353  					Price:          f64Price,
   354  					Time:           time.Unix(0, timeOrder),
   355  					Volume:         f64Volume,
   356  					Source:         s.exchangeName,
   357  					VerifiedPair:   exchangepair.Verified,
   358  					BaseToken:      exchangepair.UnderlyingPair.BaseToken,
   359  					QuoteToken:     exchangepair.UnderlyingPair.QuoteToken,
   360  					ForeignTradeID: t.TradeID,
   361  				}
   362  				if exchangepair.Verified {
   363  					log.Info("Got verified trade from stream 3: ", trade)
   364  				}
   365  				s.chanTrades <- trade
   366  
   367  			case <-s.shutdown: // user requested shutdown
   368  				log.Println("KuCoin shutting down")
   369  				s.cleanup(nil)
   370  				return
   371  			}
   372  		}
   373  	}()
   374  }
   375  
   376  func (s *KuCoinScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) {
   377  	return dia.ExchangePair{}, nil
   378  }
   379  
   380  // closes all connected PairScrapers
   381  // must only be called from mainLoop
   382  func (s *KuCoinScraper) cleanup(err error) {
   383  	s.errorLock.Lock()
   384  	defer s.errorLock.Unlock()
   385  
   386  	if err != nil {
   387  		s.error = err
   388  	}
   389  	s.closed = true
   390  
   391  	close(s.shutdownDone)
   392  }
   393  
   394  // Close closes any existing API connections, as well as channels of
   395  // PairScrapers from calls to ScrapePair
   396  func (s *KuCoinScraper) Close() error {
   397  	if s.closed {
   398  		return errors.New("KuCoinScraper: Already closed")
   399  	}
   400  	close(s.shutdown)
   401  	<-s.shutdownDone
   402  	s.errorLock.RLock()
   403  	defer s.errorLock.RUnlock()
   404  	return s.error
   405  }
   406  
   407  // ScrapePair returns a PairScraper that can be used to get trades for a single pair from
   408  // this APIScraper
   409  func (s *KuCoinScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) {
   410  	s.errorLock.RLock()
   411  	defer s.errorLock.RUnlock()
   412  	if s.error != nil {
   413  		return nil, s.error
   414  	}
   415  	if s.closed {
   416  		return nil, errors.New("KucoinScraper: Call ScrapePair on closed scraper")
   417  	}
   418  
   419  	ps := &KuCoinPairScraper{
   420  		parent: s,
   421  		pair:   pair,
   422  	}
   423  	s.pairScrapers[pair.ForeignName] = ps
   424  	return ps, nil
   425  }
   426  
   427  // FetchAvailablePairs returns all traded pairs on kucoin.
   428  func (s *KuCoinScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) {
   429  	response, err := s.apiService.Symbols("")
   430  	if err != nil {
   431  		log.Println("Error Getting  Symbols for KuCoin Exchange", err)
   432  	}
   433  
   434  	var kep KuExchangePairs
   435  	err = response.ReadData(&kep)
   436  	if err != nil {
   437  		log.Println("Error Reading  Symbols for KuCoin Exchange", err)
   438  	}
   439  	for _, p := range kep {
   440  		pairs = append(pairs, dia.ExchangePair{
   441  			Symbol:      p.BaseCurrency,
   442  			ForeignName: p.Symbol,
   443  			Exchange:    s.exchangeName,
   444  		})
   445  	}
   446  	return
   447  }
   448  
   449  // FillSymbolData adds the name to the asset underlying @symbol on kucoin.
   450  func (s *KuCoinScraper) FillSymbolData(symbol string) (asset dia.Asset, err error) {
   451  	// Comment Philipp:
   452  	// Kucoin's notations for symbols differ too often from the ones used in the underlying contracts.
   453  
   454  	// resp, err := s.apiService.Currency(symbol, "")
   455  	// if err != nil {
   456  	// 	log.Errorf("error fetching %s from kucoin api: %v", symbol, err)
   457  	// }
   458  	// var kc KucoinCurrency
   459  	// err = resp.ReadData(&kc)
   460  	// if err != nil {
   461  	// 	log.Errorf("error reading data for %s: %v", symbol, err)
   462  	// }
   463  	asset.Symbol = symbol
   464  	// asset.Name = kc.Name
   465  	// asset.Address = kc.Address
   466  	return
   467  }
   468  
   469  // KuCoinPairScraper implements PairScraper for kuCoin
   470  type KuCoinPairScraper struct {
   471  	parent *KuCoinScraper
   472  	pair   dia.ExchangePair
   473  	closed bool
   474  }
   475  
   476  // Close stops listening for trades of the pair associated with s
   477  func (ps *KuCoinPairScraper) Close() error {
   478  	var err error
   479  	s := ps.parent
   480  	// if parent already errored, return early
   481  	s.errorLock.RLock()
   482  	defer s.errorLock.RUnlock()
   483  	if s.error != nil {
   484  		return s.error
   485  	}
   486  	if ps.closed {
   487  		return errors.New("KuCoinPairScraper: Already closed")
   488  	}
   489  
   490  	// TODO stop collection for the pair
   491  
   492  	ps.closed = true
   493  	return err
   494  }
   495  
   496  // Channel returns a channel that can be used to receive trades
   497  func (ps *KuCoinScraper) Channel() chan *dia.Trade {
   498  	return ps.chanTrades
   499  }
   500  
   501  // Error returns an error when the channel Channel() is closed
   502  // and nil otherwise
   503  func (ps *KuCoinPairScraper) Error() error {
   504  	s := ps.parent
   505  	s.errorLock.RLock()
   506  	defer s.errorLock.RUnlock()
   507  	return s.error
   508  }
   509  
   510  // Pair returns the pair this scraper is subscribed to
   511  func (ps *KuCoinPairScraper) Pair() dia.ExchangePair {
   512  	return ps.pair
   513  }