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 }