github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/exchange-scrapers/BKEXScraper.go (about) 1 package scrapers 2 3 import ( 4 "encoding/json" 5 "errors" 6 "io" 7 "io/ioutil" 8 "net/http" 9 "strconv" 10 "strings" 11 "sync" 12 "time" 13 14 "github.com/diadata-org/diadata/pkg/dia" 15 models "github.com/diadata-org/diadata/pkg/model" 16 ws "github.com/gorilla/websocket" 17 "github.com/zekroTJA/timedmap" 18 ) 19 20 type BKEXScraper struct { 21 wsClient map[int]*ws.Conn 22 // signaling channels for session initialization and finishing 23 shutdown chan nothing 24 shutdownDone chan nothing 25 // error handling; to read error or closed, first acquire read lock 26 // only cleanup method should hold write lock 27 errorLock sync.RWMutex 28 error error 29 closed bool 30 // used to keep track of trading pairs that we subscribed to 31 pairScrapers map[string]*BKEXPairScraper 32 exchangeName string 33 scraperName string 34 chanTrades chan *dia.Trade 35 db *models.RelDB 36 } 37 38 func NewBKEXScraper(exchange dia.Exchange, scraperName string, scrape bool, relDB *models.RelDB) *BKEXScraper { 39 s := &BKEXScraper{ 40 wsClient: make(map[int]*ws.Conn), 41 shutdown: make(chan nothing), 42 shutdownDone: make(chan nothing), 43 pairScrapers: make(map[string]*BKEXPairScraper), 44 exchangeName: exchange.Name, 45 scraperName: scraperName, 46 error: nil, 47 chanTrades: make(chan *dia.Trade), 48 db: relDB, 49 } 50 51 if scrape { 52 go s.mainLoop() 53 } 54 55 return s 56 } 57 58 type BKEXTradeRecord struct { 59 Symbol string `json:"symbol"` 60 Price string `json:"price"` 61 Volume float64 `json:"volume"` 62 Direction string `json:"direction"` 63 Ts int64 `json:"ts"` 64 } 65 66 type BKEXTradeResponse struct { 67 quotationAllDeal string 68 records []BKEXTradeRecord 69 } 70 71 func chunkSlice(slice []string, chunkSize int) [][]string { 72 var chunks [][]string 73 for { 74 if len(slice) == 0 { 75 break 76 } 77 78 // necessary check to avoid slicing beyond 79 // slice capacity 80 if len(slice) < chunkSize { 81 chunkSize = len(slice) 82 } 83 84 chunks = append(chunks, slice[0:chunkSize]) 85 slice = slice[chunkSize:] 86 } 87 88 return chunks 89 } 90 91 func (s *BKEXScraper) connect(i int) *ws.Conn { 92 var wsDialer ws.Dialer 93 SwConn, _, err := wsDialer.Dial("wss://api.bkex.com/socket.io/?EIO=3&transport=websocket", nil) 94 95 if err != nil { 96 println(err.Error()) 97 return nil 98 } 99 s.wsClient[i] = SwConn 100 101 // Two time read message 102 messageType, p, err := SwConn.ReadMessage() 103 104 if err != nil { 105 println(err.Error()) 106 } 107 108 log.Info("Connected ", messageType, "-", string(p)) 109 110 messageType, p, err = SwConn.ReadMessage() 111 112 if err != nil { 113 println(err.Error()) 114 } 115 116 log.Info("Connected ", messageType, "-", string(p)) 117 // Connect Finished 118 119 // Send 40/quotation and receive it 120 writeErr := SwConn.WriteMessage(ws.TextMessage, []byte("40/quotation")) 121 if writeErr != nil { 122 log.Error("Error writing message ", writeErr) 123 } 124 messageType, p, err = SwConn.ReadMessage() 125 if err != nil { 126 log.Error("Error writing message ", err) 127 } 128 log.Info("Connected ", messageType, "-", string(p)) 129 130 return SwConn 131 } 132 133 func (s *BKEXScraper) subLoop(wsClient *ws.Conn, pairs string) { 134 pingTimer := time.NewTicker(25 * time.Second) 135 go func() { 136 for range pingTimer.C { 137 go s.ping(wsClient) 138 } 139 }() 140 141 tmFalseDuplicateTrades := timedmap.New(duplicateTradesScanFrequency) 142 tmDuplicateTrades := timedmap.New(duplicateTradesScanFrequency) 143 144 message := `42/quotation,["quotationDealConnect",{"symbol": "` + pairs + `","number": 50}]` 145 146 if err := wsClient.WriteMessage(ws.TextMessage, []byte(message)); err != nil { 147 log.Error("write pair sub: ", err.Error()) 148 } 149 log.Info("Subscribed to get trades for ", pairs) 150 151 for { 152 messageType, p, err := wsClient.ReadMessage() 153 if err != nil { 154 log.Fatal("read message: ", err.Error()) 155 } 156 if messageType != ws.TextMessage { 157 log.Fatal("unknow type ", messageType) 158 } 159 160 c := string(p) 161 162 if c == "3" { 163 continue 164 } 165 166 if len(strings.Split(c, "42/quotation,")) < 2 { 167 continue 168 } 169 d := strings.Split(c, "42/quotation,")[1] 170 171 var r BKEXTradeResponse 172 tmp := []interface{}{&r.quotationAllDeal, &r.records} 173 174 jsonErr := json.Unmarshal([]byte(d), &tmp) 175 if jsonErr != nil { 176 log.Error("can't unmarshal json ", jsonErr) 177 } 178 179 if e := len(tmp); e != 2 { 180 log.Fatal("unknow length ", e) 181 } 182 183 records := tmp[1].(*[]BKEXTradeRecord) 184 185 for _, trade := range *records { 186 var exchangePair dia.ExchangePair 187 priceFloat, _ := strconv.ParseFloat(trade.Price, 64) 188 189 exchangePair, err = s.db.GetExchangePairCache(s.scraperName, trade.Symbol) 190 if err != nil { 191 log.Error("Get Exchange Pair ", trade.Symbol) 192 } 193 volume := trade.Volume 194 if trade.Direction == "S" { 195 volume *= -1 196 } 197 198 t := &dia.Trade{ 199 Symbol: strings.Split(trade.Symbol, "_")[0], 200 Pair: trade.Symbol, 201 Price: priceFloat, 202 Volume: volume, 203 Time: time.Unix(0, trade.Ts*int64(time.Millisecond)), 204 Source: s.exchangeName, 205 VerifiedPair: exchangePair.Verified, 206 BaseToken: exchangePair.UnderlyingPair.BaseToken, 207 QuoteToken: exchangePair.UnderlyingPair.QuoteToken, 208 } 209 210 if exchangePair.Verified { 211 log.Infoln("Got verified trade", t) 212 } 213 // Handle duplicate trades. 214 discardTrade := t.IdentifyDuplicateFull(tmFalseDuplicateTrades, duplicateTradesMemory) 215 if !discardTrade { 216 t.IdentifyDuplicateTagset(tmDuplicateTrades, duplicateTradesMemory) 217 s.chanTrades <- t 218 } 219 220 } 221 } 222 } 223 224 func (s *BKEXScraper) mainLoop() { 225 226 log.Info("Wait 5s untill subscribe all Pairs") 227 time.Sleep(5 * time.Second) 228 229 keys := make([]string, 0, len(s.pairScrapers)) 230 for k := range s.pairScrapers { 231 keys = append(keys, k) 232 } 233 234 miniPairs := chunkSlice(keys, 10) 235 236 for i, v := range miniPairs { 237 log.Info("Connect Websocket ...", i, v) 238 time.Sleep(5 * time.Second) 239 conn := s.connect(i) 240 if conn != nil { 241 log.Info("Connect Done Websocket", i, v) 242 go s.subLoop(conn, strings.Join(v, ",")) 243 } else { 244 log.Error("Connection Failed !!!", i) 245 return 246 } 247 } 248 } 249 250 func (s *BKEXScraper) ping(conn *ws.Conn) { 251 writeErr := conn.WriteMessage(ws.TextMessage, []byte("2")) 252 if writeErr != nil { 253 log.Error("Error writing message ", writeErr) 254 } 255 } 256 257 // FillSymbolData from MEXCScraper 258 // @todo more update 259 func (s *BKEXScraper) FillSymbolData(symbol string) (asset dia.Asset, err error) { 260 asset.Symbol = symbol 261 return 262 } 263 264 func (s *BKEXScraper) Close() error { 265 if s.closed { 266 return errors.New("BKEXScraper: Already closed") 267 } 268 close(s.shutdown) 269 for _, c := range s.wsClient { 270 err := c.Close() 271 if err != nil { 272 return err 273 } 274 } 275 276 <-s.shutdownDone 277 s.errorLock.RLock() 278 defer s.errorLock.RUnlock() 279 return s.error 280 } 281 282 func (s *BKEXScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) { 283 return dia.ExchangePair{}, nil 284 } 285 286 func (s *BKEXScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) { 287 s.errorLock.RLock() 288 defer s.errorLock.RUnlock() 289 290 if s.error != nil { 291 return nil, s.error 292 } 293 294 if s.closed { 295 return nil, errors.New("BKEXScraper: Call ScrapePair on closed scraper") 296 } 297 298 ps := &BKEXPairScraper{ 299 parent: s, 300 pair: pair, 301 } 302 303 // message := `42/quotation,["quotationDealConnect",{"symbol": "` + pair.ForeignName + `","number": 50}]` 304 // // message := `42/quotation,["quotationDealConnect",{"symbol": "BTC_USDT","number": 50}]` 305 // log.Info(message) 306 307 // if err := s.wsClient.WriteMessage(ws.TextMessage, []byte(message)); err != nil { 308 // log.Error("write pair sub: ", err.Error()) 309 // } 310 log.Info("Add to get trades for ", pair.ForeignName) 311 s.pairScrapers[pair.ForeignName] = ps 312 return ps, nil 313 } 314 315 type BKEXExchangeSymbol struct { 316 MinimumOrderSize float64 `json:"minimumOrderSize"` 317 MinimumTradeVolume float64 `json:"minimumTradeVolume"` 318 PricePrecision int `json:"pricePrecision"` 319 SupportTrade bool `json:"supportTrade"` 320 Symbol string `json:"symbol"` 321 VolumePrecision int `json:"volumePrecision"` 322 } 323 324 type BKEXExchangeInfo struct { 325 Code string `json:"code"` 326 Data []BKEXExchangeSymbol `json:"data"` 327 } 328 329 func (s *BKEXScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) { 330 var bkexExchangeInfo BKEXExchangeInfo 331 response, err := http.Get("https://api.bkex.com/v2/common/symbols") 332 if err != nil { 333 log.Error("get symbols: ", err) 334 } 335 336 defer func(Body io.ReadCloser) { 337 errClose := Body.Close() 338 if errClose != nil { 339 log.Error("body close got error ", errClose) 340 } 341 }(response.Body) 342 343 body, err := ioutil.ReadAll(response.Body) 344 345 if err != nil { 346 log.Error("read symbols: ", err) 347 } 348 349 err = json.Unmarshal(body, &bkexExchangeInfo) 350 351 if err != nil { 352 log.Error("unmarshal symbols: ", err) 353 } 354 355 for _, p := range bkexExchangeInfo.Data { 356 pairToNormalized := dia.ExchangePair{ 357 Symbol: strings.Split(p.Symbol, "_")[0], 358 ForeignName: p.Symbol, 359 Exchange: s.exchangeName, 360 } 361 pairs = append(pairs, pairToNormalized) 362 } 363 return 364 } 365 366 // Channel returns a channel that can be used to receive trades 367 func (s *BKEXScraper) Channel() chan *dia.Trade { 368 return s.chanTrades 369 } 370 371 // BKEXPairScraper implements PairScraper for BKEX 372 type BKEXPairScraper struct { 373 parent *BKEXScraper 374 pair dia.ExchangePair 375 closed bool 376 } 377 378 // Close stops listening for trades of the pair associated with s 379 func (ps *BKEXPairScraper) Close() error { 380 ps.closed = true 381 return nil 382 } 383 384 // Error returns an error when the channel Channel() is closed 385 // and nil otherwise 386 func (ps *BKEXPairScraper) Error() error { 387 s := ps.parent 388 s.errorLock.RLock() 389 defer s.errorLock.RUnlock() 390 return s.error 391 } 392 393 // Pair returns the pair this scraper is subscribed to 394 func (ps *BKEXPairScraper) Pair() dia.ExchangePair { 395 return ps.pair 396 }