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

     1  package scrapers
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"io/ioutil"
     7  	"net/http"
     8  	"strconv"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/diadata-org/diadata/pkg/dia"
    13  	models "github.com/diadata-org/diadata/pkg/model"
    14  	ws "github.com/gorilla/websocket"
    15  	"github.com/zekroTJA/timedmap"
    16  )
    17  
    18  const (
    19  	mexc_socketurl    = "wss://wbs.mexc.com/ws"
    20  	api_url           = "https://api.mexc.com"
    21  	mexcMaxSubPerConn = 20
    22  )
    23  
    24  type MEXCExchangeSymbol struct {
    25  	Symbol                     string   `json:"symbol"`
    26  	Status                     string   `json:"status"`
    27  	BaseAsset                  string   `json:"baseAsset"`
    28  	BaseAssetPrecision         int      `json:"baseAssetPrecision"`
    29  	QuoteAsset                 string   `json:"quoteAsset"`
    30  	QuotePrecision             int      `json:"quotePrecision"`
    31  	QuoteAssetPrecision        int      `json:"quoteAssetPrecision"`
    32  	BaseCommissionPrecision    int      `json:"baseCommissionPrecision"`
    33  	QuoteCommissionPrecision   int      `json:"quoteCommissionPrecision"`
    34  	OrderTypes                 []string `json:"orderTypes"` // [LIMIT, LIMIT_MAKER]
    35  	QuoteOrderQtyMarketAllowed bool     `json:"quoteOrderQtyMarketAllowed"`
    36  	IsSpotTradingAllowed       bool     `json:"isSpotTradingAllowed"`
    37  	IsMarginTradingAllowed     bool     `json:"isMarginTradingAllowed"`
    38  	QuoteAmountPrecision       string   `json:"quoteAmountPrecision"`
    39  	BaseSizePrecision          string   `json:"baseSizePrecision"`
    40  	Permissions                []string `json:"permissions"`
    41  	Filters                    []string `json:"filters"`
    42  	MaxQuoteAmount             string   `json:"maxQuoteAmount"`
    43  	MakerCommission            string   `json:"makerCommission"`
    44  	TakerCommission            string   `json:"takerCommission"`
    45  }
    46  
    47  type MEXCExchangeInfo struct {
    48  	Timezone        string               `json:"timezone"`
    49  	ServerTime      int                  `json:"serverTime"`
    50  	RateLimits      string               `json:"rateLimits"`
    51  	ExchangeFilters string               `json:"exchangeFilters"`
    52  	Symbols         []MEXCExchangeSymbol `json:"symbols"`
    53  }
    54  
    55  type MEXCRequest struct {
    56  	Method string   `json:"method"`
    57  	Params []string `json:"params"`
    58  	ID     int64    `json:"id"`
    59  }
    60  
    61  type MEXCTradeResponse struct {
    62  	C string `json:"c"`
    63  	D struct {
    64  		Deals []struct {
    65  			Side   int    `json:"S"`
    66  			Price  string `json:"p"`
    67  			Volume string `json:"v"`
    68  			TS     int64  `json:"t"`
    69  		} `json:"deals"`
    70  	} `json:"d"`
    71  	Symbol string `json:"s"`
    72  }
    73  
    74  type MEXCWSConnection struct {
    75  	wsConn           *ws.Conn
    76  	numSubscriptions int
    77  }
    78  
    79  // MEXCScraper is a scraper for MEXC
    80  type MEXCScraper struct {
    81  	connections map[int]MEXCWSConnection
    82  	// signaling channels for session initialization and finishing
    83  	shutdown     chan nothing
    84  	shutdownDone chan nothing
    85  	// error handling; to read error or closed, first acquire read lock
    86  	// only cleanup method should hold write lock
    87  	errorLock sync.RWMutex
    88  	error     error
    89  	closed    bool
    90  	// used to keep track of trading pairs that we subscribed to
    91  	pairScrapers map[string]*MEXCPairScraper
    92  	exchangeName string
    93  	chanTrades   chan *dia.Trade
    94  	db           *models.RelDB
    95  }
    96  
    97  func NewMEXCScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *MEXCScraper {
    98  	s := &MEXCScraper{
    99  		shutdown:     make(chan nothing),
   100  		shutdownDone: make(chan nothing),
   101  		connections:  make(map[int]MEXCWSConnection),
   102  		pairScrapers: make(map[string]*MEXCPairScraper),
   103  		exchangeName: exchange.Name,
   104  		error:        nil,
   105  		chanTrades:   make(chan *dia.Trade),
   106  		db:           relDB,
   107  	}
   108  
   109  	err := s.newConn()
   110  	if err != nil {
   111  		log.Fatal("new connection: ", err)
   112  	}
   113  
   114  	if scrape {
   115  		go s.mainLoop()
   116  	}
   117  
   118  	return s
   119  }
   120  
   121  func (s *MEXCScraper) mainLoop() {
   122  
   123  	// Wait for subscription to all pairs.
   124  	time.Sleep(5 * time.Second)
   125  	for _, c := range s.connections {
   126  		go s.subLoop(c.wsConn)
   127  	}
   128  
   129  }
   130  
   131  func (s *MEXCScraper) subLoop(client *ws.Conn) {
   132  	var err error
   133  	tmFalseDuplicateTrades := timedmap.New(duplicateTradesScanFrequency)
   134  	tmDuplicateTrades := timedmap.New(duplicateTradesScanFrequency)
   135  	for {
   136  		message := &MEXCTradeResponse{}
   137  		if err = client.ReadJSON(&message); err != nil {
   138  			log.Error("read message: ", err.Error())
   139  			continue
   140  			// deal it
   141  		}
   142  		for _, trade := range message.D.Deals {
   143  			var exchangePair dia.ExchangePair
   144  			priceFloat, _ := strconv.ParseFloat(trade.Price, 64)
   145  			volumeFloat, _ := strconv.ParseFloat(trade.Volume, 64)
   146  			if trade.Side == 2 {
   147  				volumeFloat *= -1
   148  			}
   149  			exchangePair, err = s.db.GetExchangePairCache(s.exchangeName, message.Symbol)
   150  			if err != nil {
   151  				log.Error("get exchange pair from cache: ", err)
   152  			}
   153  			t := &dia.Trade{
   154  				Symbol:       exchangePair.Symbol,
   155  				Pair:         message.Symbol,
   156  				Price:        priceFloat,
   157  				Volume:       volumeFloat,
   158  				Time:         time.Unix(0, trade.TS*int64(time.Millisecond)),
   159  				Source:       s.exchangeName,
   160  				VerifiedPair: exchangePair.Verified,
   161  				BaseToken:    exchangePair.UnderlyingPair.BaseToken,
   162  				QuoteToken:   exchangePair.UnderlyingPair.QuoteToken,
   163  			}
   164  			if exchangePair.Verified {
   165  				log.Infof("Got verified trade: %v", t)
   166  			}
   167  
   168  			// Handle duplicate trades.
   169  			discardTrade := t.IdentifyDuplicateFull(tmFalseDuplicateTrades, duplicateTradesMemory)
   170  			if !discardTrade {
   171  				t.IdentifyDuplicateTagset(tmDuplicateTrades, duplicateTradesMemory)
   172  				s.chanTrades <- t
   173  			}
   174  		}
   175  	}
   176  }
   177  
   178  func (s *MEXCScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) {
   179  	s.errorLock.RLock()
   180  	defer s.errorLock.RUnlock()
   181  
   182  	if s.error != nil {
   183  		return nil, s.error
   184  	}
   185  
   186  	if s.closed {
   187  		return nil, errors.New("MEXCScraper: Call ScrapePair on closed scraper")
   188  	}
   189  
   190  	ps := &MEXCPairScraper{
   191  		parent: s,
   192  		pair:   pair,
   193  	}
   194  
   195  	err := s.subscribe(pair)
   196  	if err != nil {
   197  		log.Error("subscribe pair: ", err)
   198  		return nil, err
   199  	}
   200  
   201  	s.pairScrapers[pair.ForeignName] = ps
   202  	return ps, nil
   203  }
   204  
   205  // Subscribe to @pair, taking into account the max subscription number.
   206  func (s *MEXCScraper) subscribe(pair dia.ExchangePair) error {
   207  	id := len(s.connections)
   208  
   209  	a := &MEXCRequest{
   210  		Method: "SUBSCRIPTION",
   211  		Params: []string{"spot@public.deals.v3.api@" + pair.ForeignName},
   212  	}
   213  
   214  	if s.connections[id-1].numSubscriptions < mexcMaxSubPerConn {
   215  		a.ID = int64(id)
   216  		if err := s.connections[id-1].wsConn.WriteJSON(a); err != nil {
   217  			return err
   218  		}
   219  		conn := s.connections[id-1]
   220  		conn.numSubscriptions++
   221  		s.connections[id-1] = conn
   222  
   223  	} else {
   224  		err := s.newConn()
   225  		if err != nil {
   226  			return err
   227  		}
   228  		id++
   229  		a.ID = int64(id)
   230  		if err := s.connections[id-1].wsConn.WriteJSON(a); err != nil {
   231  			return err
   232  		}
   233  		conn := s.connections[id-1]
   234  		conn.numSubscriptions++
   235  		s.connections[id-1] = conn
   236  
   237  	}
   238  	return nil
   239  }
   240  
   241  // Add a connection to the connection pool.
   242  func (s *MEXCScraper) newConn() error {
   243  	var wsDialer ws.Dialer
   244  	wsConn, _, err := wsDialer.Dial(mexc_socketurl, nil)
   245  	if err != nil {
   246  		return err
   247  	}
   248  	s.connections[len(s.connections)] = MEXCWSConnection{wsConn: wsConn, numSubscriptions: 0}
   249  	return nil
   250  }
   251  
   252  // FillSymbolData from MEXCScraper
   253  // @todo more update
   254  func (s *MEXCScraper) FillSymbolData(symbol string) (asset dia.Asset, err error) {
   255  	asset.Symbol = symbol
   256  	return
   257  }
   258  
   259  func (s *MEXCScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) {
   260  	return dia.ExchangePair{}, nil
   261  }
   262  
   263  func (s *MEXCScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) {
   264  	var mexcExchangeInfo MEXCExchangeInfo
   265  	response, err := http.Get(api_url + "/api/v3/exchangeInfo")
   266  	if err != nil {
   267  		log.Error("get symbols: ", err)
   268  	}
   269  
   270  	defer response.Body.Close()
   271  
   272  	body, err := ioutil.ReadAll(response.Body)
   273  
   274  	if err != nil {
   275  		log.Error("read symbols: ", err)
   276  	}
   277  
   278  	err = json.Unmarshal(body, &mexcExchangeInfo)
   279  
   280  	if err != nil {
   281  		log.Error("unmarshal symbols: ", err)
   282  	}
   283  
   284  	for _, p := range mexcExchangeInfo.Symbols {
   285  		pairToNormalized := dia.ExchangePair{
   286  			Symbol:      p.BaseAsset,
   287  			ForeignName: p.BaseAsset + p.QuoteAsset,
   288  			Exchange:    s.exchangeName,
   289  		}
   290  		pairs = append(pairs, pairToNormalized)
   291  	}
   292  	return
   293  }
   294  
   295  func (s *MEXCScraper) Close() error {
   296  	if s.closed {
   297  		return errors.New("MEXCScraper: Already closed")
   298  	}
   299  	close(s.shutdown)
   300  	for i := range s.connections {
   301  		err := s.connections[i].wsConn.Close()
   302  		if err != nil {
   303  			return err
   304  		}
   305  	}
   306  
   307  	<-s.shutdownDone
   308  	s.errorLock.RLock()
   309  	defer s.errorLock.RUnlock()
   310  	return s.error
   311  }
   312  
   313  // Channel returns a channel that can be used to receive trades
   314  func (s *MEXCScraper) Channel() chan *dia.Trade {
   315  	return s.chanTrades
   316  }
   317  
   318  // MEXCPairScraper implements PairScraper for MEXC
   319  type MEXCPairScraper struct {
   320  	parent *MEXCScraper
   321  	pair   dia.ExchangePair
   322  	closed bool
   323  }
   324  
   325  // Close stops listening for trades of the pair associated with s
   326  func (ps *MEXCPairScraper) Close() error {
   327  	ps.closed = true
   328  	return nil
   329  }
   330  
   331  // Error returns an error when the channel Channel() is closed
   332  // and nil otherwise
   333  func (ps *MEXCPairScraper) Error() error {
   334  	s := ps.parent
   335  	s.errorLock.RLock()
   336  	defer s.errorLock.RUnlock()
   337  	return s.error
   338  }
   339  
   340  // Pair returns the pair this scraper is subscribed to
   341  func (ps *MEXCPairScraper) Pair() dia.ExchangePair {
   342  	return ps.pair
   343  }