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 }