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

     1  package scrapers
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"strconv"
     7  	"strings"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/diadata-org/diadata/pkg/dia"
    12  	"github.com/diadata-org/diadata/pkg/dia/helpers"
    13  	models "github.com/diadata-org/diadata/pkg/model"
    14  	utils "github.com/diadata-org/diadata/pkg/utils"
    15  	ws "github.com/gorilla/websocket"
    16  	"github.com/zekroTJA/timedmap"
    17  )
    18  
    19  var _GateIOsocketurl string = "wss://api.gateio.ws/ws/v4/"
    20  
    21  type GateIOTickerData struct {
    22  	Result string           `json:"result"`
    23  	Data   []GateIOCurrency `json:"data"`
    24  }
    25  
    26  type GateIOCurrency struct {
    27  	No          int    `json:"no"`
    28  	Symbol      string `json:"symbol"`
    29  	Name        string `json:"name"`
    30  	NameEn      string `json:"name_en"`
    31  	NameCn      string `json:"name_cn"`
    32  	Pair        string `json:"pair"`
    33  	Rate        string `json:"rate"`
    34  	VolA        string `json:"vol_a"`
    35  	VolB        string `json:"vol_b"`
    36  	CurrA       string `json:"curr_a"`
    37  	CurrB       string `json:"curr_b"`
    38  	CurrSuffix  string `json:"curr_suffix"`
    39  	RatePercent string `json:"rate_percent"`
    40  	Trend       string `json:"trend"`
    41  	Lq          string `json:"lq"`
    42  	PRate       int    `json:"p_rate"`
    43  }
    44  
    45  type ResponseGate struct {
    46  	Method string        `json:"method,omitempty"`
    47  	Params []interface{} `json:"params,omitempty"`
    48  	Id     interface{}   `json:"id,omitempty"`
    49  }
    50  
    51  type SubscribeGate struct {
    52  	Time    int64    `json:"time"`
    53  	Channel string   `json:"channel"`
    54  	Event   string   `json:"event"`
    55  	Payload []string `json:"payload"`
    56  }
    57  
    58  type GateIOScraper struct {
    59  	wsClient *ws.Conn
    60  	// signaling channels for session initialization and finishing
    61  	//initDone     chan nothing
    62  	shutdown     chan nothing
    63  	shutdownDone chan nothing
    64  	// error handling; to read error or closed, first acquire read lock
    65  	// only cleanup method should hold write lock
    66  	errorLock sync.RWMutex
    67  	error     error
    68  	closed    bool
    69  	// used to keep track of trading pairs that we subscribed to
    70  	pairScrapers           map[string]*GateIOPairScraper
    71  	exchangeName           string
    72  	chanTrades             chan *dia.Trade
    73  	currencySymbolName     map[string]string
    74  	isTickerMapInitialised bool
    75  	db                     *models.RelDB
    76  }
    77  
    78  // NewGateIOScraper returns a new GateIOScraper for the given pair
    79  func NewGateIOScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *GateIOScraper {
    80  
    81  	s := &GateIOScraper{
    82  		shutdown:               make(chan nothing),
    83  		shutdownDone:           make(chan nothing),
    84  		pairScrapers:           make(map[string]*GateIOPairScraper),
    85  		exchangeName:           exchange.Name,
    86  		error:                  nil,
    87  		chanTrades:             make(chan *dia.Trade),
    88  		currencySymbolName:     make(map[string]string),
    89  		isTickerMapInitialised: false,
    90  		db:                     relDB,
    91  	}
    92  	var wsDialer ws.Dialer
    93  	SwConn, _, err := wsDialer.Dial(_GateIOsocketurl, nil)
    94  	if err != nil {
    95  		println(err.Error())
    96  	}
    97  	s.wsClient = SwConn
    98  
    99  	if scrape {
   100  		go s.mainLoop()
   101  	}
   102  	return s
   103  }
   104  
   105  type GateIPPairResponse []GateIOPair
   106  
   107  type GateIOPair struct {
   108  	ID              string `json:"id"`
   109  	Base            string `json:"base"`
   110  	Quote           string `json:"quote"`
   111  	Fee             string `json:"fee"`
   112  	MinQuoteAmount  string `json:"min_quote_amount,omitempty"`
   113  	AmountPrecision int    `json:"amount_precision"`
   114  	Precision       int    `json:"precision"`
   115  	TradeStatus     string `json:"trade_status"`
   116  	SellStart       int    `json:"sell_start"`
   117  	BuyStart        int    `json:"buy_start"`
   118  	MinBaseAmount   string `json:"min_base_amount,omitempty"`
   119  }
   120  
   121  type GateIOResponseTrade struct {
   122  	Time    int    `json:"time"`
   123  	Channel string `json:"channel"`
   124  	Event   string `json:"event"`
   125  	Result  struct {
   126  		ID           int    `json:"id"`
   127  		CreateTime   int    `json:"create_time"`
   128  		CreateTimeMs string `json:"create_time_ms"`
   129  		Side         string `json:"side"`
   130  		CurrencyPair string `json:"currency_pair"`
   131  		Amount       string `json:"amount"`
   132  		Price        string `json:"price"`
   133  	} `json:"result"`
   134  }
   135  
   136  // runs in a goroutine until s is closed
   137  func (s *GateIOScraper) mainLoop() {
   138  	var gresponse GateIPPairResponse
   139  	tmFalseDuplicateTrades := timedmap.New(duplicateTradesScanFrequency)
   140  	tmDuplicateTrades := timedmap.New(duplicateTradesScanFrequency)
   141  
   142  	b, _, err := utils.GetRequest("https://api.gateio.ws/api/v4/spot/currency_pairs")
   143  	if err != nil {
   144  		log.Error(err)
   145  	}
   146  	err = json.Unmarshal(b, &gresponse)
   147  	if err != nil {
   148  		log.Error(err)
   149  	}
   150  
   151  	for _, v := range gresponse {
   152  
   153  		a := &SubscribeGate{
   154  			Event:   "subscribe",
   155  			Time:    time.Now().Unix(),
   156  			Channel: "spot.trades",
   157  			Payload: []string{v.ID},
   158  		}
   159  		log.Infof("Subscribed for Pair %v", v.ID)
   160  		if err = s.wsClient.WriteJSON(a); err != nil {
   161  			log.Error(err.Error())
   162  		}
   163  	}
   164  
   165  	for {
   166  
   167  		var message GateIOResponseTrade
   168  		if err = s.wsClient.ReadJSON(&message); err != nil {
   169  			log.Error(err.Error())
   170  			break
   171  		}
   172  
   173  		ps, ok := s.pairScrapers[message.Result.CurrencyPair]
   174  		if ok {
   175  			var f64Price float64
   176  			var f64Volume float64
   177  			var exchangepair dia.ExchangePair
   178  			f64Price, err = strconv.ParseFloat(message.Result.Price, 64)
   179  			if err != nil {
   180  				log.Errorln("error parsing float Price", err)
   181  				continue
   182  			}
   183  
   184  			f64Volume, err = strconv.ParseFloat(message.Result.Amount, 64)
   185  			if err != nil {
   186  				log.Errorln("error parsing float Price", err)
   187  				continue
   188  			}
   189  
   190  			if message.Result.Side == "sell" {
   191  				f64Volume = -f64Volume
   192  			}
   193  
   194  			exchangepair, err = s.db.GetExchangePairCache(s.exchangeName, message.Result.CurrencyPair)
   195  			if err != nil {
   196  				log.Error(err)
   197  			}
   198  
   199  			t := &dia.Trade{
   200  				Symbol:         ps.pair.Symbol,
   201  				Pair:           message.Result.CurrencyPair,
   202  				Price:          f64Price,
   203  				Volume:         f64Volume,
   204  				Time:           time.Unix(int64(message.Result.CreateTime), 0),
   205  				ForeignTradeID: strconv.FormatInt(int64(message.Result.ID), 16),
   206  				Source:         s.exchangeName,
   207  				VerifiedPair:   exchangepair.Verified,
   208  				BaseToken:      exchangepair.UnderlyingPair.BaseToken,
   209  				QuoteToken:     exchangepair.UnderlyingPair.QuoteToken,
   210  			}
   211  			if exchangepair.Verified {
   212  				log.Infoln("Got verified trade", t)
   213  			}
   214  			// Handle duplicate trades.
   215  			discardTrade := t.IdentifyDuplicateFull(tmFalseDuplicateTrades, duplicateTradesMemory)
   216  			if !discardTrade {
   217  				t.IdentifyDuplicateTagset(tmDuplicateTrades, duplicateTradesMemory)
   218  				ps.parent.chanTrades <- t
   219  			}
   220  
   221  		}
   222  
   223  	}
   224  	s.cleanup(err)
   225  }
   226  
   227  func (s *GateIOScraper) cleanup(err error) {
   228  	s.errorLock.Lock()
   229  	defer s.errorLock.Unlock()
   230  
   231  	if err != nil {
   232  		s.error = err
   233  	}
   234  	s.closed = true
   235  
   236  	close(s.shutdownDone)
   237  }
   238  
   239  // Close closes any existing API connections, as well as channels of
   240  // PairScrapers from calls to ScrapePair
   241  func (s *GateIOScraper) Close() error {
   242  
   243  	if s.closed {
   244  		return errors.New("GateIOScraper: Already closed")
   245  	}
   246  	err := s.wsClient.Close()
   247  	if err != nil {
   248  		log.Error(err)
   249  	}
   250  	close(s.shutdown)
   251  
   252  	<-s.shutdownDone
   253  	s.errorLock.RLock()
   254  	defer s.errorLock.RUnlock()
   255  	return s.error
   256  }
   257  
   258  // ScrapePair returns a PairScraper that can be used to get trades for a single pair from
   259  // this APIScraper
   260  func (s *GateIOScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) {
   261  
   262  	s.errorLock.RLock()
   263  	defer s.errorLock.RUnlock()
   264  
   265  	if s.error != nil {
   266  		return nil, s.error
   267  	}
   268  
   269  	if s.closed {
   270  		return nil, errors.New("GateIOScraper: Call ScrapePair on closed scraper")
   271  	}
   272  
   273  	ps := &GateIOPairScraper{
   274  		parent: s,
   275  		pair:   pair,
   276  	}
   277  
   278  	s.pairScrapers[pair.ForeignName] = ps
   279  
   280  	return ps, nil
   281  }
   282  
   283  // func (s *GateIOScraper) normalizeSymbol(foreignName string, params ...interface{}) (symbol string, err error) {
   284  // 	str := strings.Split(foreignName, "_")
   285  // 	symbol = strings.ToUpper(str[0])
   286  // 	if helpers.NameForSymbol(symbol) == symbol {
   287  // 		if !helpers.SymbolIsName(symbol) {
   288  // 			if symbol == "IOTA" {
   289  // 				return "MIOTA", nil
   290  // 			}
   291  // 			return symbol, errors.New("Foreign name can not be normalized:" + foreignName + " symbol:" + symbol)
   292  // 		}
   293  // 	}
   294  // 	if helpers.SymbolIsBlackListed(symbol) {
   295  // 		return symbol, errors.New("Symbol is black listed:" + symbol)
   296  // 	}
   297  // 	return symbol, nil
   298  // }
   299  
   300  func (s *GateIOScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) {
   301  	str := strings.Split(pair.ForeignName, "_")
   302  	symbol := strings.ToUpper(str[0])
   303  	pair.Symbol = symbol
   304  	if helpers.NameForSymbol(symbol) == symbol {
   305  		if !helpers.SymbolIsName(symbol) {
   306  			if symbol == "IOTA" {
   307  				pair.Symbol = "MIOTA"
   308  			}
   309  			return pair, errors.New("Foreign name can not be normalized:" + pair.ForeignName + " symbol:" + symbol)
   310  		}
   311  	}
   312  	if helpers.SymbolIsBlackListed(symbol) {
   313  		return pair, errors.New("Symbol is black listed:" + symbol)
   314  	}
   315  	return pair, nil
   316  }
   317  
   318  // FetchTickerData collects all available information on an asset traded on GateIO
   319  func (s *GateIOScraper) FillSymbolData(symbol string) (asset dia.Asset, err error) {
   320  
   321  	// // Fetch Data
   322  	// if !s.isTickerMapInitialised {
   323  	// 	var (
   324  	// 		response GateIOTickerData
   325  	// 		data     []byte
   326  	// 	)
   327  	// 	data, _, err = utils.GetRequest("https://data.gateapi.io/api2/1/marketlist")
   328  	// 	if err != nil {
   329  	// 		return
   330  	// 	}
   331  	// 	err = json.Unmarshal(data, &response)
   332  	// 	if err != nil {
   333  	// 		return
   334  	// 	}
   335  
   336  	// 	for _, gateioasset := range response.Data {
   337  	// 		s.currencySymbolName[gateioasset.Symbol] = gateioasset.Name
   338  	// 	}
   339  	// 	s.isTickerMapInitialised = true
   340  
   341  	// }
   342  
   343  	asset.Symbol = symbol
   344  	// asset.Name = s.currencySymbolName[symbol]
   345  	return asset, nil
   346  }
   347  
   348  // FetchAvailablePairs returns a list with all available trade pairs
   349  func (s *GateIOScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) {
   350  	data, _, err := utils.GetRequest("https://data.gate.io/api2/1/pairs")
   351  	if err != nil {
   352  		return
   353  	}
   354  	ls := strings.Split(strings.Replace(string(data)[1:len(data)-1], "\"", "", -1), ",")
   355  	for _, p := range ls {
   356  		pairToNormalize := dia.ExchangePair{
   357  			Symbol:      "",
   358  			ForeignName: p,
   359  			Exchange:    s.exchangeName,
   360  		}
   361  		pair, serr := s.NormalizePair(pairToNormalize)
   362  		if serr == nil {
   363  			pairs = append(pairs, pair)
   364  		} else {
   365  			log.Error(serr)
   366  		}
   367  	}
   368  	return
   369  }
   370  
   371  // GateIOPairScraper implements PairScraper for GateIO
   372  type GateIOPairScraper struct {
   373  	parent *GateIOScraper
   374  	pair   dia.ExchangePair
   375  	closed bool
   376  }
   377  
   378  // Close stops listening for trades of the pair associated with s
   379  func (ps *GateIOPairScraper) Close() error {
   380  	ps.closed = true
   381  	return nil
   382  }
   383  
   384  // Channel returns a channel that can be used to receive trades
   385  func (ps *GateIOScraper) Channel() chan *dia.Trade {
   386  	return ps.chanTrades
   387  }
   388  
   389  // Error returns an error when the channel Channel() is closed
   390  // and nil otherwise
   391  func (ps *GateIOPairScraper) Error() error {
   392  	s := ps.parent
   393  	s.errorLock.RLock()
   394  	defer s.errorLock.RUnlock()
   395  	return s.error
   396  }
   397  
   398  // Pair returns the pair this scraper is subscribed to
   399  func (ps *GateIOPairScraper) Pair() dia.ExchangePair {
   400  	return ps.pair
   401  }