github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/exchange-scrapers/OKExScraper.go (about) 1 package scrapers 2 3 import ( 4 "bytes" 5 "compress/flate" 6 "encoding/json" 7 "errors" 8 "io/ioutil" 9 "strconv" 10 "strings" 11 "sync" 12 "time" 13 14 "github.com/diadata-org/diadata/pkg/dia" 15 "github.com/diadata-org/diadata/pkg/dia/helpers" 16 models "github.com/diadata-org/diadata/pkg/model" 17 utils "github.com/diadata-org/diadata/pkg/utils" 18 ws "github.com/gorilla/websocket" 19 "github.com/zekroTJA/timedmap" 20 ) 21 22 var _OKExSocketURL = utils.Getenv("OKEX_WS_URL", "wss://ws.okx.com:8443/ws/v5/public") 23 24 //var _OKExSocketURL = url.URL{Scheme: "wss", Host: "real.okex.com:10441", Path: "/ws/v1", RawQuery: "compress=true"} 25 26 type Response struct { 27 Channel string `json:"channel"` 28 Data [][]string `json:"data"` 29 Binary int `json:"binary"` 30 } 31 32 type Responses []Response 33 34 type Subscribe struct { 35 OP string `json:"op"` 36 Args []OKEXArgs `json:"args"` 37 } 38 39 type OKEXArgs struct { 40 Channel string `json:"channel"` 41 InstID string `json:"instId"` 42 } 43 44 type OKExScraper struct { 45 wsClient *ws.Conn 46 // signaling channels for session initialization and finishing 47 run bool 48 shutdown chan nothing 49 shutdownDone chan nothing 50 // error handling; to read error or closed, first acquire read lock 51 // only cleanup method should hold write lock 52 errorLock sync.RWMutex 53 error error 54 closed bool 55 // used to keep track of trading pairs that we subscribed to 56 pairScrapers map[string]*OKExPairScraper 57 exchangeName string 58 chanTrades chan *dia.Trade 59 db *models.RelDB 60 } 61 62 // NewOKExScraper returns a new OKExScraper for the given pair 63 func NewOKExScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *OKExScraper { 64 65 s := &OKExScraper{ 66 shutdown: make(chan nothing), 67 shutdownDone: make(chan nothing), 68 pairScrapers: make(map[string]*OKExPairScraper), 69 exchangeName: exchange.Name, 70 error: nil, 71 chanTrades: make(chan *dia.Trade), 72 db: relDB, 73 } 74 75 var wsDialer ws.Dialer 76 SwConn, _, err := wsDialer.Dial(_OKExSocketURL, nil) 77 if err != nil { 78 log.Error("dial:", err) 79 } 80 s.wsClient = SwConn 81 if scrape { 82 go s.mainLoop() 83 } 84 return s 85 } 86 87 // Useful to reconnect to ws when the connection is down 88 func (s *OKExScraper) reconnectToWS() { 89 90 var wsDialer ws.Dialer 91 SwConn, _, err := wsDialer.Dial(_OKExSocketURL, nil) 92 93 if err != nil { 94 log.Error("dial:", err) 95 } 96 97 s.wsClient = SwConn 98 } 99 100 type OKEXMarket struct { 101 Alias string `json:"alias"` 102 BaseCcy string `json:"baseCcy"` 103 Category string `json:"category"` 104 CtMult string `json:"ctMult"` 105 CtType string `json:"ctType"` 106 CtVal string `json:"ctVal"` 107 CtValCcy string `json:"ctValCcy"` 108 ExpTime string `json:"expTime"` 109 InstID string `json:"instId"` 110 InstType string `json:"instType"` 111 Lever string `json:"lever"` 112 ListTime string `json:"listTime"` 113 LotSz string `json:"lotSz"` 114 MinSz string `json:"minSz"` 115 OptType string `json:"optType"` 116 QuoteCcy string `json:"quoteCcy"` 117 SettleCcy string `json:"settleCcy"` 118 State string `json:"state"` 119 Stk string `json:"stk"` 120 TickSz string `json:"tickSz"` 121 Uly string `json:"uly"` 122 } 123 124 type AllOKEXMarketResponse struct { 125 Code string `json:"code"` 126 Data []OKEXMarket `json:"data"` 127 Msg string `json:"msg"` 128 } 129 130 // Subscribe again to all channels 131 func (s *OKExScraper) subscribeToALL() { 132 var ( 133 resp AllOKEXMarketResponse 134 allPairs []OKEXArgs 135 ) 136 137 b, _, err := utils.GetRequest("https://okx.com/api/v5/public/instruments?instType=SPOT") 138 if err != nil { 139 log.Errorln("Error getting OKex market", err) 140 } 141 142 err = json.Unmarshal(b, &resp) 143 if err != nil { 144 log.Errorln("Error Unmarshalling OKex market json", err) 145 } 146 147 for _, v := range resp.Data { 148 allPairs = append(allPairs, OKEXArgs{Channel: "trades", InstID: v.InstID}) 149 } 150 151 a := &Subscribe{ 152 OP: "subscribe", 153 Args: allPairs, 154 } 155 156 if err := s.wsClient.WriteJSON(a); err != nil { 157 log.Errorln(err.Error()) 158 } 159 160 } 161 162 type OKEXWSResponse struct { 163 Arg struct { 164 Channel string `json:"channel"` 165 InstID string `json:"instId"` 166 } `json:"arg"` 167 Data []struct { 168 InstID string `json:"instId"` 169 TradeID string `json:"tradeId"` 170 Px string `json:"px"` 171 Sz string `json:"sz"` 172 Side string `json:"side"` 173 Ts string `json:"ts"` 174 } `json:"data"` 175 } 176 177 // runs in a goroutine until s is closed 178 func (s *OKExScraper) mainLoop() { 179 180 tmFalseDuplicateTrades := timedmap.New(duplicateTradesScanFrequency) 181 tmDuplicateTrades := timedmap.New(duplicateTradesScanFrequency) 182 183 s.run = true 184 for s.run { 185 var message OKEXWSResponse 186 messageType, messageTemp, err := s.wsClient.ReadMessage() 187 if err != nil { 188 log.Warning("reconnect the scraping to ws, ", err, ":", message) 189 s.reconnectToWS() 190 s.subscribeToALL() 191 } else { 192 switch messageType { 193 case ws.TextMessage: 194 // no need uncompressed 195 err := json.Unmarshal(messageTemp, &message) 196 if err != nil { 197 log.Errorln("Error parsing response") 198 } 199 ps, ok := s.pairScrapers[message.Arg.InstID] 200 201 if ok && len(message.Data) > 0 { 202 203 f64PriceString := message.Data[0].Px 204 f64Price, err := strconv.ParseFloat(f64PriceString, 64) 205 206 if err == nil { 207 208 f64VolumeString := message.Data[0].Sz 209 f64Volume, err := strconv.ParseFloat(f64VolumeString, 64) 210 211 if err == nil { 212 213 ts, _ := strconv.ParseInt(message.Data[0].Ts, 10, 64) 214 timeStamp := time.Unix(int64(ts)/1e3, 0) 215 if message.Data[0].Side == "sell" { 216 f64Volume = -f64Volume 217 } 218 219 exchangepair, err := s.db.GetExchangePairCache(s.exchangeName, message.Arg.InstID) 220 if err != nil { 221 log.Error(err) 222 } 223 224 t := &dia.Trade{ 225 Symbol: ps.pair.Symbol, 226 Pair: message.Arg.InstID, 227 Price: f64Price, 228 Volume: f64Volume, 229 Time: timeStamp, 230 ForeignTradeID: message.Data[0].TradeID, 231 Source: s.exchangeName, 232 VerifiedPair: exchangepair.Verified, 233 BaseToken: exchangepair.UnderlyingPair.BaseToken, 234 QuoteToken: exchangepair.UnderlyingPair.QuoteToken, 235 } 236 if exchangepair.Verified { 237 log.Infoln("Got verified trade", t) 238 } 239 // Handle duplicate trades. 240 discardTrade := t.IdentifyDuplicateFull(tmFalseDuplicateTrades, duplicateTradesMemory) 241 if !discardTrade { 242 t.IdentifyDuplicateTagset(tmDuplicateTrades, duplicateTradesMemory) 243 244 ps.parent.chanTrades <- t 245 } 246 } else { 247 log.Errorf("parsing volume %v", f64VolumeString) 248 } 249 250 } else { 251 log.Errorf("parsing price %v", f64PriceString) 252 } 253 } 254 255 } 256 } 257 } 258 s.cleanup(errors.New("main loop terminated by Close()")) 259 } 260 261 func GzipDecode(in []byte) (content []byte, err error) { 262 reader := flate.NewReader(bytes.NewReader(in)) 263 defer func() { 264 cerr := reader.Close() 265 if err == nil { 266 err = cerr 267 } 268 }() 269 content, err = ioutil.ReadAll(reader) 270 271 return 272 } 273 274 func (s *OKExScraper) cleanup(err error) { 275 s.errorLock.Lock() 276 defer s.errorLock.Unlock() 277 278 if err != nil { 279 s.error = err 280 } 281 s.closed = true 282 283 close(s.shutdownDone) 284 } 285 286 // Close closes any existing API connections, as well as channels of 287 // PairScrapers from calls to ScrapePair 288 func (s *OKExScraper) Close() error { 289 290 if s.closed { 291 return errors.New("OKExScraper: Already closed") 292 } 293 294 close(s.shutdown) 295 // Set false first to prevent reconnect 296 s.run = false 297 err := s.wsClient.Close() 298 if err != nil { 299 return err 300 } 301 <-s.shutdownDone 302 s.errorLock.RLock() 303 defer s.errorLock.RUnlock() 304 return s.error 305 } 306 307 // ScrapePair returns a PairScraper that can be used to get trades for a single pair from 308 // this APIScraper 309 func (s *OKExScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) { 310 311 s.errorLock.RLock() 312 defer s.errorLock.RUnlock() 313 314 if s.error != nil { 315 return nil, s.error 316 } 317 318 if s.closed { 319 return nil, errors.New("OKExScraper: Call ScrapePair on closed scraper") 320 } 321 322 ps := &OKExPairScraper{ 323 parent: s, 324 pair: pair, 325 } 326 327 s.pairScrapers[pair.ForeignName] = ps 328 329 //a := &Subscribe{ 330 // OP: "addChannel", 331 // Args: "ok_sub_spot_" + strings.ToLower(pair.ForeignName) + "_deals", 332 //} 333 // 334 //subByteString := `{"channel":` + `"` + a.Channel + `"` + `,"event":` + `"` + a.Event + `"}` 335 //if err := s.wsClient.WriteMessage(ws.TextMessage, []byte(subByteString)); err != nil { 336 // fmt.Println(err.Error()) 337 //} 338 339 return ps, nil 340 } 341 342 /* 343 func (s *OKExScraper) normalizeSymbol(foreignName string, baseCurrency string) (symbol string, err error) { 344 symbol = strings.ToUpper(baseCurrency) 345 if helpers.NameForSymbol(symbol) == symbol { 346 if !helpers.SymbolIsName(symbol) { 347 if symbol == "IOTA" { 348 return "MIOTA", nil 349 } 350 if symbol == "YOYO" { 351 return "YOYOW", nil 352 } 353 return symbol, errors.New("Foreign name can not be normalized:" + foreignName + " symbol:" + symbol) 354 } 355 } 356 if helpers.SymbolIsBlackListed(symbol) { 357 return symbol, errors.New("Symbol is black listed:" + symbol) 358 } 359 return symbol, nil 360 } 361 */ 362 363 func (s *OKExScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) { 364 symbol := strings.ToUpper(pair.Symbol) 365 pair.Symbol = symbol 366 367 if helpers.NameForSymbol(symbol) == symbol { 368 if !helpers.SymbolIsName(symbol) { 369 if pair.Symbol == "IOTA" { 370 pair.Symbol = "MIOTA" 371 } 372 if pair.Symbol == "YOYO" { 373 pair.Symbol = "YOYOW" 374 } 375 return pair, errors.New("Foreign name can not be normalized:" + pair.ForeignName + " symbol:" + symbol) 376 } 377 } 378 if helpers.SymbolIsBlackListed(symbol) { 379 return pair, errors.New("Symbol is black listed:" + symbol) 380 } 381 return pair, nil 382 383 } 384 385 func (s *OKExScraper) FillSymbolData(symbol string) (asset dia.Asset, err error) { 386 // TO DO 387 return dia.Asset{Symbol: symbol}, nil 388 } 389 390 // FetchAvailablePairs returns a list with all available trade pairs 391 func (s *OKExScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) { 392 type APIResponse struct { 393 Id string `json:"instrument_id"` 394 BaseCurrency string `json:"base_currency"` 395 } 396 397 data, _, err := utils.GetRequest("https://www.okx.com/api/spot/v3/products") 398 399 if err != nil { 400 return 401 } 402 403 var ar []APIResponse 404 err = json.Unmarshal(data, &ar) 405 if err == nil { 406 for _, p := range ar { 407 pairToNormalize := dia.ExchangePair{ 408 Symbol: p.BaseCurrency, 409 ForeignName: p.Id, 410 Exchange: s.exchangeName, 411 } 412 pair, serr := s.NormalizePair(pairToNormalize) 413 if serr == nil { 414 pairs = append(pairs, pair) 415 } else { 416 log.Error(serr) 417 } 418 } 419 } 420 return 421 } 422 423 // OKExPairScraper implements PairScraper for OKEx exchange 424 type OKExPairScraper struct { 425 parent *OKExScraper 426 pair dia.ExchangePair 427 closed bool 428 } 429 430 // Close stops listening for trades of the pair associated with s 431 func (ps *OKExPairScraper) Close() error { 432 ps.closed = true 433 return nil 434 } 435 436 // Channel returns a channel that can be used to receive trades 437 func (s *OKExScraper) Channel() chan *dia.Trade { 438 return s.chanTrades 439 } 440 441 // Error returns an error when the channel Channel() is closed 442 // and nil otherwise 443 func (ps *OKExPairScraper) Error() error { 444 s := ps.parent 445 s.errorLock.RLock() 446 defer s.errorLock.RUnlock() 447 return s.error 448 } 449 450 // Pair returns the pair this scraper is subscribed to 451 func (ps *OKExPairScraper) Pair() dia.ExchangePair { 452 return ps.pair 453 }