github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/exchange-scrapers/BitMexScraper.go (about) 1 package scrapers 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "strings" 9 "sync" 10 "time" 11 12 ws "github.com/gorilla/websocket" 13 "github.com/zekroTJA/timedmap" 14 "go.uber.org/ratelimit" 15 16 "github.com/diadata-org/diadata/pkg/dia" 17 models "github.com/diadata-org/diadata/pkg/model" 18 "github.com/diadata-org/diadata/pkg/utils" 19 ) 20 21 const ( 22 bitMexAPIEndpoint = "https://www.bitMex.com/api/v1" 23 bitMexWSEndpoint = "wss://ws.bitMex.com/realtime" 24 25 // bitMexWSRateLimitPerSec is a max request per second for sending websocket requests 26 bitMexWSRateLimitPerSec = 10 27 28 // bitMexTaskMaxRetry is a max retry value used when retrying subscribe/unsubscribe trades 29 bitMexTaskMaxRetry = 20 30 31 // bitMexConnMaxRetry is a max retry value used when retrying to create a new connection 32 bitMexConnMaxRetry = 50 33 34 // bitMexRateLimitError is a rate limit error code 35 bitMexRateLimitError = 429 36 37 // bitMexPingInterval is the number of seconds between ping messages 38 bitMexPingInterval = 25 39 ) 40 41 // bitMexWSTask is a websocket task tracking subscription/unsubscription 42 type bitMexWSTask struct { 43 Op string 44 Args []string 45 RetryCount int 46 } 47 48 func (c *bitMexWSTask) toString() string { 49 return fmt.Sprintf("op=%s, param=%s, retry=%d", c.Op, c.Args, c.RetryCount) 50 } 51 52 // bitMexWSRequest is a websocket request 53 type bitMexWSRequest struct { 54 Op string `json:"op"` 55 Args []string `json:"args,omitempty"` 56 } 57 58 // bitMexSubscriptionResult is a subscription result coming from websocket 59 type bitMexSubscriptionResult struct { 60 Success bool `json:"success"` 61 Subscribe string `json:"subscribe"` 62 Error string `json:"error"` 63 Status int `json:"status"` 64 Request json.RawMessage `json:"request"` 65 Table string `json:"table"` 66 Trades []bitMexWSTrade `json:"data"` 67 } 68 69 // bitMexWSTrade is a trade result coming from websocket 70 type bitMexWSTrade struct { 71 Timestamp time.Time `json:"timestamp"` 72 Symbol string `json:"symbol"` 73 Side string `json:"side"` 74 Size float64 `json:"size"` 75 Price float64 `json:"price"` 76 TickDirection string `json:"tickDirection"` 77 TrdMatchID string `json:"trdMatchID"` 78 GrossValue float64 `json:"grossValue"` 79 HomeNotional float64 `json:"homeNotional"` 80 ForeignNotional float64 `json:"foreignNotional"` 81 } 82 83 // bitMexInstrument represents a trading pair 84 type bitMexInstrument struct { 85 Symbol string `json:"symbol"` 86 RootSymbol string `json:"rootSymbol"` 87 Expiry time.Time `json:"expiry"` 88 } 89 90 // BitMexScraper is a scraper for bitmex.com 91 type BitMexScraper struct { 92 ws *ws.Conn 93 rl ratelimit.Limiter 94 95 // signaling channels for session initialization and finishing 96 shutdown chan nothing 97 shutdownDone chan nothing 98 signalShutdown sync.Once 99 signalShutdownDone sync.Once 100 101 // error handling; err should be read from error(), closed should be read from isClosed() 102 // those two methods implement RW lock 103 errMutex sync.RWMutex 104 err error 105 closedMutex sync.RWMutex 106 closed bool 107 //consecutiveErrCount int 108 109 // used to keep track of trading pairs that we subscribed to 110 pairScrapers sync.Map 111 exchangeName string 112 chanTrades chan *dia.Trade 113 db *models.RelDB 114 tasks sync.Map 115 pingTicker *time.Ticker 116 stopPingRoutine chan bool 117 118 // used to handle connection retry 119 connMutex sync.RWMutex 120 connRetryCount int 121 } 122 123 // NewBitMexScraper returns a new BitMex scraper 124 func NewBitMexScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *BitMexScraper { 125 s := &BitMexScraper{ 126 shutdown: make(chan nothing), 127 shutdownDone: make(chan nothing), 128 exchangeName: exchange.Name, 129 err: nil, 130 chanTrades: make(chan *dia.Trade), 131 db: relDB, 132 pingTicker: time.NewTicker(bitMexPingInterval * time.Second), 133 stopPingRoutine: make(chan bool), 134 } 135 136 if err := s.newConn(); err != nil { 137 log.Error(err) 138 139 return nil 140 } 141 142 s.rl = ratelimit.New(bitMexWSRateLimitPerSec) 143 144 if scrape { 145 go s.mainLoop() 146 } 147 148 return s 149 } 150 151 // Close unsubscribes data and closes any existing WebSocket connections, as well as channels of BitMexScraper 152 func (s *BitMexScraper) Close() error { 153 if s.isClosed() { 154 return errors.New("BitMexScraper: Already closed") 155 } 156 157 s.signalShutdown.Do(func() { 158 close(s.shutdown) 159 }) 160 161 <-s.shutdownDone 162 163 return s.error() 164 } 165 166 // Channel returns a channel that can be used to receive trades 167 func (s *BitMexScraper) Channel() chan *dia.Trade { 168 return s.chanTrades 169 } 170 171 // FetchAvailablePairs returns all traded pairs on BitMex 172 func (s *BitMexScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) { 173 174 data, _, err := utils.GetRequest(bitMexAPIEndpoint + "/instrument") 175 if err != nil { 176 return nil, err 177 } 178 179 var res []bitMexInstrument 180 if err := json.Unmarshal(data, &res); err != nil { 181 return nil, err 182 } 183 184 for _, i := range res { 185 186 // fmt.Printf(` 187 // { 188 // "Symbol": "%s", 189 // "ForeignName": "%s", 190 // "Exchange": "BitMex", 191 // "Ignore": false 192 // }, 193 // `, i.RootSymbol, i.RootSymbol+"_"+strings.TrimPrefix(i.Symbol, i.RootSymbol)) 194 195 pairs = append(pairs, dia.ExchangePair{ 196 Symbol: i.RootSymbol, 197 ForeignName: i.RootSymbol + "_" + strings.TrimPrefix(i.Symbol, i.RootSymbol), 198 Exchange: s.exchangeName, 199 }) 200 } 201 202 return pairs, nil 203 } 204 205 // FillSymbolData adds the name to the asset underlying @symbol on BitMex 206 func (s *BitMexScraper) FillSymbolData(symbol string) (dia.Asset, error) { 207 return dia.Asset{Symbol: symbol}, nil 208 } 209 210 func (s *BitMexScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) { 211 return pair, nil 212 } 213 214 // ScrapePair returns a PairScraper that can be used to get trades for a single pair from the BitMex scraper 215 func (s *BitMexScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) { 216 217 if err := s.error(); err != nil { 218 return nil, err 219 } 220 221 if s.isClosed() { 222 return nil, errors.New("BitMexScraper: Call ScrapePair on closed scraper") 223 } 224 225 ps := &BitMexPairScraper{ 226 parent: s, 227 pair: pair, 228 } 229 230 if err := s.subscribe([]dia.ExchangePair{pair}); err != nil { 231 return nil, err 232 } 233 234 return ps, nil 235 } 236 237 func (s *BitMexScraper) startPing() { 238 for { 239 select { 240 case <-s.stopPingRoutine: 241 return 242 case <-s.pingTicker.C: 243 err := s.ping() 244 if err != nil { 245 log.Error("ping brought error ", err) 246 } 247 } 248 } 249 } 250 251 func (s *BitMexScraper) mainLoop() { 252 defer s.cleanup() 253 254 go s.startPing() 255 256 for { 257 select { 258 case <-s.shutdown: 259 log.Warn("BitMexScraper: Shutting down main loop") 260 default: 261 } 262 263 _, msg, err := s.wsConn().ReadMessage() 264 if err != nil { 265 266 log.Warnf("BitMexScraper: Creating a new connection caused by err=%s", err.Error()) 267 268 if retryErr := s.retryConnection(); retryErr != nil { 269 s.setError(retryErr) 270 log.Errorf("BitMexScraper: Shutting down main loop after retrying to create a new connection, err=%s", retryErr.Error()) 271 } 272 273 log.Info("BitMexScraper: Successfully created a new connection") 274 continue 275 276 } 277 278 if string(msg) == "pong" { 279 continue 280 } 281 282 var subResult bitMexSubscriptionResult 283 if err := json.Unmarshal(msg, &subResult); err == nil { 284 285 if subResult.Status == 400 { 286 log.Warning(bytes.NewBuffer(msg)) 287 } 288 if subResult.Table == "trade" { 289 s.handleTrades(subResult) 290 continue 291 } 292 293 if subResult.Success { 294 // subscription Success 295 continue 296 } 297 if subResult.Status == bitMexRateLimitError { 298 299 var failedRequest bitMexWSRequest 300 if errUnmarshal := json.Unmarshal(subResult.Request, &failedRequest); errUnmarshal == nil { 301 302 task := bitMexWSTask{ 303 Op: failedRequest.Op, 304 Args: failedRequest.Args, 305 } 306 if errRetryTask := s.retryTask(s.getTaskID(task)); err != nil { 307 s.setError(errRetryTask) 308 log.Errorf("BitMexScraper: Shutting down main loop due to failing to retry a task, err=%s", errRetryTask.Error()) 309 310 } 311 312 } 313 314 continue 315 } 316 317 } else { 318 log.Println(err) 319 } 320 321 } 322 323 } 324 325 func (s *BitMexScraper) handleTrades(tradesWsResponse bitMexSubscriptionResult) { 326 var pair dia.ExchangePair 327 tmFalseDuplicateTrades := timedmap.New(duplicateTradesScanFrequency) 328 tmDuplicateTrades := timedmap.New(duplicateTradesScanFrequency) 329 330 for _, data := range tradesWsResponse.Trades { 331 332 if pair == (dia.ExchangePair{}) { 333 val, ok := s.pairScrapers.Load(data.Symbol) 334 if !ok { 335 log.Error("Pair not found %s", data.Symbol) 336 continue 337 } else { 338 pair = val.(dia.ExchangePair) 339 } 340 } 341 342 volume := data.HomeNotional 343 if data.Side == "Sell" { 344 volume = -volume 345 } 346 347 exchangepair, err := s.db.GetExchangePairCache(s.exchangeName, pair.ForeignName) 348 if err != nil { 349 log.Error("get exchangepair from cache: ", err) 350 } 351 trade := &dia.Trade{ 352 Symbol: pair.Symbol, 353 Pair: pair.ForeignName, 354 Price: data.Price, 355 Time: data.Timestamp, 356 Volume: volume, 357 Source: s.exchangeName, 358 ForeignTradeID: data.TrdMatchID, 359 VerifiedPair: exchangepair.Verified, 360 BaseToken: exchangepair.UnderlyingPair.BaseToken, 361 QuoteToken: exchangepair.UnderlyingPair.QuoteToken, 362 } 363 if exchangepair.Verified { 364 log.Infoln("Got verified trade", trade) 365 } 366 // Handle duplicate trades. 367 discardTrade := trade.IdentifyDuplicateFull(tmFalseDuplicateTrades, duplicateTradesMemory) 368 if !discardTrade { 369 trade.IdentifyDuplicateTagset(tmDuplicateTrades, duplicateTradesMemory) 370 select { 371 case <-s.shutdown: 372 case s.chanTrades <- trade: 373 } 374 } 375 376 } 377 } 378 379 func (s *BitMexScraper) newConn() error { 380 conn, _, err := ws.DefaultDialer.Dial(bitMexWSEndpoint, nil) 381 if err != nil { 382 return err 383 } 384 385 defer s.connMutex.Unlock() 386 s.connMutex.Lock() 387 s.ws = conn 388 389 return nil 390 } 391 392 func (s *BitMexScraper) wsConn() *ws.Conn { 393 defer s.connMutex.RUnlock() 394 s.connMutex.RLock() 395 396 return s.ws 397 } 398 399 func (s *BitMexScraper) ping() error { 400 s.rl.Take() 401 402 return s.wsConn().WriteMessage(ws.TextMessage, []byte("ping")) 403 } 404 405 func (s *BitMexScraper) cleanup() { 406 s.pingTicker.Stop() 407 s.stopPingRoutine <- true 408 if err := s.wsConn().Close(); err != nil { 409 s.setError(err) 410 } 411 412 close(s.chanTrades) 413 s.close() 414 s.signalShutdownDone.Do(func() { 415 close(s.shutdownDone) 416 }) 417 } 418 419 func (s *BitMexScraper) error() error { 420 s.errMutex.RLock() 421 defer s.errMutex.RUnlock() 422 423 return s.err 424 } 425 426 func (s *BitMexScraper) setError(err error) { 427 s.errMutex.Lock() 428 defer s.errMutex.Unlock() 429 430 s.err = err 431 } 432 433 func (s *BitMexScraper) isClosed() bool { 434 s.closedMutex.RLock() 435 defer s.closedMutex.RUnlock() 436 437 return s.closed 438 } 439 440 func (s *BitMexScraper) close() { 441 s.closedMutex.Lock() 442 defer s.closedMutex.Unlock() 443 444 s.closed = true 445 } 446 447 func (s *BitMexScraper) subscribe(pairs []dia.ExchangePair) error { 448 channels := make([]string, len(pairs)) 449 for idx, pair := range pairs { 450 bitMexInstrumentSymbol := strings.Replace(pair.ForeignName, "_", "", 1) 451 channels[idx] = "trade:" + bitMexInstrumentSymbol 452 s.pairScrapers.Store(bitMexInstrumentSymbol, pair) 453 } 454 455 task := bitMexWSTask{ 456 Op: "subscribe", 457 Args: channels, 458 459 RetryCount: 0, 460 } 461 taskID := s.getTaskID(task) 462 s.tasks.Store(taskID, task) 463 464 return s.send(task) 465 } 466 467 func (s *BitMexScraper) getTaskID(task bitMexWSTask) string { 468 return fmt.Sprintf("%s-%s", task.Op, strings.Join(task.Args, ",")) 469 } 470 471 func (s *BitMexScraper) unsubscribe(pairs []dia.ExchangePair) error { 472 channels := make([]string, len(pairs)) 473 for idx, pair := range pairs { 474 channels[idx] = "trade." + pair.ForeignName 475 s.pairScrapers.Delete(pair.ForeignName) 476 } 477 478 task := bitMexWSTask{ 479 Op: "unsubscribe", 480 Args: channels, 481 RetryCount: 0, 482 } 483 taskID := s.getTaskID(task) 484 s.tasks.Store(taskID, task) 485 486 return s.send(task) 487 } 488 489 func (s *BitMexScraper) retryConnection() error { 490 s.connRetryCount += 1 491 if s.connRetryCount > bitMexConnMaxRetry { 492 return errors.New("BitMexPairScraper: Reached max retry connection") 493 } 494 if err := s.wsConn().Close(); err != nil { 495 return err 496 } 497 if err := s.newConn(); err != nil { 498 return err 499 } 500 501 var pairs []dia.ExchangePair 502 s.pairScrapers.Range(func(key, value interface{}) bool { 503 pair := value.(dia.ExchangePair) 504 pairs = append(pairs, pair) 505 return true 506 }) 507 if err := s.subscribe(pairs); err != nil { 508 return err 509 } 510 511 return nil 512 } 513 514 func (s *BitMexScraper) retryTask(taskID string) error { 515 val, ok := s.tasks.Load(taskID) 516 if !ok { 517 return fmt.Errorf("BitMexScraper: Facing unknown task id, taskId=%v", taskID) 518 } 519 520 task := val.(bitMexWSTask) 521 task.RetryCount += 1 522 if task.RetryCount > bitMexTaskMaxRetry { 523 return fmt.Errorf("BitMexScraper: Exeeding max retry, taskId=%v, %s", taskID, task.toString()) 524 } 525 526 log.Warnf("BitMexScraper: Retrying a task, taskId=%v, %s", taskID, task.toString()) 527 s.tasks.Store(taskID, task) 528 529 return s.send(task) 530 } 531 532 func (s *BitMexScraper) send(task bitMexWSTask) error { 533 s.rl.Take() 534 535 return s.wsConn().WriteJSON(&bitMexWSRequest{ 536 Op: task.Op, 537 Args: task.Args, 538 }) 539 } 540 541 // BitMexPairScraper implements PairScraper for BitMex 542 type BitMexPairScraper struct { 543 parent *BitMexScraper 544 pair dia.ExchangePair 545 closed bool 546 } 547 548 // Error returns an error when the channel Channel() is closed 549 // and nil otherwise 550 func (p *BitMexPairScraper) Error() error { 551 return p.parent.error() 552 } 553 554 // Pair returns the pair this scraper is subscribed to 555 func (p *BitMexPairScraper) Pair() dia.ExchangePair { 556 return p.pair 557 } 558 559 // Close stops listening for trades of the pair associated with the BitMex scraper 560 func (p *BitMexPairScraper) Close() error { 561 if err := p.parent.error(); err != nil { 562 return err 563 } 564 if p.closed { 565 return errors.New("BitMexPairScraper: Already closed") 566 } 567 if err := p.parent.unsubscribe([]dia.ExchangePair{p.pair}); err != nil { 568 return err 569 } 570 571 p.closed = true 572 573 return nil 574 }