github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/exchange-scrapers/ByBitScraper.go (about) 1 package scrapers 2 3 import ( 4 "encoding/json" 5 "errors" 6 "strconv" 7 "strings" 8 "sync" 9 "time" 10 11 ws "github.com/gorilla/websocket" 12 "github.com/zekroTJA/timedmap" 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 "github.com/diadata-org/diadata/pkg/utils" 18 ) 19 20 var ByBitSocketURL string = utils.Getenv("BYBIT_WS_URL", "wss://stream.bybit.com/v5/public/spot") 21 22 type ByBitMarket struct { 23 Name string `json:"name"` 24 Alias string `json:"alias"` 25 Status string `json:"status"` 26 BaseCurrency string `json:"base_currency"` 27 QuoteCurrency string `json:"quote_currency"` 28 PriceScale int `json:"price_scale"` 29 TakerFee string `json:"taker_fee"` 30 MakerFee string `json:"maker_fee"` 31 LeverageFilter struct { 32 MinLeverage int `json:"min_leverage"` 33 MaxLeverage int `json:"max_leverage"` 34 LeverageStep string `json:"leverage_step"` 35 } `json:"leverage_filter"` 36 PriceFilter struct { 37 MinPrice string `json:"min_price"` 38 MaxPrice string `json:"max_price"` 39 TickSize string `json:"tick_size"` 40 } `json:"price_filter"` 41 LotSizeFilter struct { 42 MaxTradingQty float64 `json:"max_trading_qty"` 43 MinTradingQty float64 `json:"min_trading_qty"` 44 QtyStep float64 `json:"qty_step"` 45 } `json:"lot_size_filter"` 46 } 47 48 type ByBitMarketsResponse struct { 49 RetCode int `json:"ret_code"` 50 RetMsg string `json:"ret_msg"` 51 ExtCode string `json:"ext_code"` 52 ExtInfo string `json:"ext_info"` 53 Result []ByBitMarket `json:"result"` 54 TimeNow string `json:"time_now"` 55 } 56 57 type ByBitTradeResponse struct { 58 Data []ByBitTradeResponseData `json:"data"` 59 Type string `json:"type"` 60 Topic string `json:"topic"` 61 Timestamp int64 `json:"ts"` 62 } 63 64 type ByBitTradeResponseData struct { 65 TradeID string `json:"i"` 66 Timestamp int64 `json:"T"` 67 Price string `json:"p"` 68 Size string `json:"v"` 69 Side string `json:"S"` 70 Symbol string `json:"s"` 71 } 72 73 type ByBitSubscribe struct { 74 OP string `json:"op"` 75 Args []string `json:"args"` 76 } 77 78 // ByBitScraper provides methods needed to get Trade information from ByBit 79 type ByBitScraper struct { 80 // control flag for main loop 81 run bool 82 wsClient *ws.Conn 83 84 // signaling channels for session initialization and finishing 85 shutdown chan nothing 86 shutdownDone chan nothing 87 // error handling; to read error or closed, first acquire read lock 88 // only cleanup method should hold write lock 89 errorLock sync.RWMutex 90 error error 91 closed bool 92 // used to keep track of trading pairs that we subscribed to 93 pairScrapers map[string]*ByBitPairScraper 94 // exchange name 95 exchangeName string 96 // channel to send trades 97 chanTrades chan *dia.Trade 98 db *models.RelDB 99 } 100 101 // NewByBitScraper get a scrapper for ByBit exchange 102 func NewByBitScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *ByBitScraper { 103 s := &ByBitScraper{ 104 shutdown: make(chan nothing), 105 shutdownDone: make(chan nothing), 106 pairScrapers: make(map[string]*ByBitPairScraper), 107 108 exchangeName: exchange.Name, 109 error: nil, 110 chanTrades: make(chan *dia.Trade), 111 closed: false, 112 db: relDB, 113 } 114 115 /* 116 In the case of needing access to private urls. 117 // Create HMAC instance from the secret key 118 h := hmac.New(sha256.New, []byte(secret)) 119 120 // Write Data to it 121 apiSecretBytes := []byte(secret) 122 // Generate expires. 123 expires := int((time.Now().UnixNano() + 1) * 1000) 124 expiresBytes := []byte(fmt.Sprintf("GET/realtime%d", expires)) 125 data := append(apiSecretBytes, expiresBytes...) 126 h.Write([]byte(data)) 127 128 // Get the signature 129 signature := hex.EncodeToString(h.Sum(nil)) 130 131 // Generate the ws url. 132 params := fmt.Sprintf("api_key=%s&expires=%d&signature=%s", secret, expires, signature) 133 */ 134 135 // Create the ws connection 136 var wsDialer ws.Dialer 137 138 SwConn, _, err := wsDialer.Dial(ByBitSocketURL, nil) 139 if err != nil { 140 log.Errorf("Connect to websocket server: %s.", err.Error()) 141 } 142 143 s.wsClient = SwConn 144 145 if scrape { 146 go s.mainLoop() 147 } 148 149 return s 150 } 151 152 func (s *ByBitScraper) ping() { 153 a := &ByBitSubscribe{ 154 OP: "ping", 155 } 156 log.Infoln("Ping: ", a.OP) 157 if err := s.wsClient.WriteJSON(a); err != nil { 158 log.Errorf("write ping message: %s.", err.Error()) 159 } 160 } 161 162 func (s *ByBitScraper) subscribe(foreignName string) { 163 // Subscribing to the all markets at once. 164 a := &ByBitSubscribe{ 165 OP: "subscribe", 166 Args: []string{"publicTrade." + foreignName}, 167 } 168 log.Println("subscribing", a) 169 if err := s.wsClient.WriteJSON(a); err != nil { 170 log.Errorf("subscribe %v: %v.", a, err.Error()) 171 } 172 } 173 174 // runs in a goroutine until s is closed 175 func (s *ByBitScraper) mainLoop() { 176 var err error 177 178 pingTimer := time.NewTicker(10 * time.Second) 179 go func() { 180 for range pingTimer.C { 181 go s.ping() 182 } 183 }() 184 185 tmFalseDuplicateTrades := timedmap.New(duplicateTradesScanFrequency) 186 tmDuplicateTrades := timedmap.New(duplicateTradesScanFrequency) 187 188 for { 189 190 message := &ByBitTradeResponse{} 191 if err = s.wsClient.ReadJSON(&message); err != nil { 192 log.Errorf("read ws response %s.", err.Error()) 193 } 194 195 // the topic format is something like publicTrade.BTCUSD 196 topic := strings.Split(message.Topic, ".") 197 198 if len(topic) == 2 && topic[0] == "publicTrade" { 199 ps, ok := s.pairScrapers[topic[1]] 200 if ok { 201 var ( 202 f64Price float64 203 f64Volume float64 204 exchangepair dia.ExchangePair 205 ) 206 for _, mdData := range message.Data { 207 208 f64Price, err = strconv.ParseFloat(mdData.Price, 64) 209 if err != nil { 210 log.Error("parse price: ", err) 211 } 212 213 f64Volume, err = strconv.ParseFloat(mdData.Size, 64) 214 if err != nil { 215 log.Error("parse volume: ", err) 216 } 217 218 timeStamp := time.Unix(0, mdData.Timestamp*1e6) 219 if mdData.TradeID != "" { 220 if mdData.Side == "Sell" { 221 f64Volume = -f64Volume 222 } 223 224 exchangepair, err = s.db.GetExchangePairCache(s.exchangeName, topic[1]) 225 if err != nil { 226 log.Errorf("GetExchangePairCache for %s: %v", topic[1], err) 227 } 228 t := &dia.Trade{ 229 Symbol: ps.pair.Symbol, 230 Pair: topic[1], 231 Price: f64Price, 232 Volume: f64Volume, 233 Time: timeStamp, 234 ForeignTradeID: mdData.TradeID, 235 Source: s.exchangeName, 236 VerifiedPair: exchangepair.Verified, 237 BaseToken: exchangepair.UnderlyingPair.BaseToken, 238 QuoteToken: exchangepair.UnderlyingPair.QuoteToken, 239 } 240 if exchangepair.Verified { 241 log.Infoln("Got verified trade: ", t) 242 } 243 // Handle duplicate trades. 244 discardTrade := t.IdentifyDuplicateFull(tmFalseDuplicateTrades, duplicateTradesMemory) 245 if !discardTrade { 246 t.IdentifyDuplicateTagset(tmDuplicateTrades, duplicateTradesMemory) 247 ps.parent.chanTrades <- t 248 } 249 } 250 } 251 252 } else { 253 log.Error("Unknown Pair " + topic[1]) 254 } 255 } 256 } 257 258 } 259 260 // Close any existing API connections, as well as channels, and terminates main loop 261 func (s *ByBitScraper) Close() error { 262 if s.closed { 263 return errors.New(s.exchangeName + "Scraper: Already closed") 264 } 265 s.run = false 266 <-s.shutdownDone 267 s.errorLock.RLock() 268 defer s.errorLock.RUnlock() 269 return s.error 270 } 271 272 // ScrapePair returns a PairScraper that can be used to get trades for a single pair from 273 // this APIScraper 274 func (s *ByBitScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) { 275 if s.closed { 276 return nil, errors.New("s.exchangeName+Scraper: Call ScrapePair on closed scraper") 277 } 278 ps := &ByBitPairScraper{ 279 parent: s, 280 pair: pair, 281 apiEndPoint: pair.ForeignName, 282 latestTrade: 0, 283 } 284 s.pairScrapers[pair.ForeignName] = ps 285 s.subscribe(pair.ForeignName) 286 return ps, nil 287 } 288 289 func (s *ByBitScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) { 290 symbol := strings.ToUpper(pair.Symbol) 291 pair.Symbol = symbol 292 293 if helpers.NameForSymbol(symbol) == symbol { 294 if !helpers.SymbolIsName(symbol) { 295 return pair, errors.New("Foreign name can not be normalized:" + pair.ForeignName + " symbol:" + symbol) 296 } 297 } 298 if helpers.SymbolIsBlackListed(symbol) { 299 return pair, errors.New("Symbol is black listed:" + symbol) 300 } 301 return pair, nil 302 } 303 304 // Channel returns the channel to get trades 305 func (s *ByBitScraper) Channel() chan *dia.Trade { 306 return s.chanTrades 307 } 308 309 func (s *ByBitScraper) FillSymbolData(symbol string) (dia.Asset, error) { 310 // TO DO 311 return dia.Asset{Symbol: symbol}, nil 312 } 313 314 // FetchAvailablePairs returns a list with all available trade pairs 315 func (s *ByBitScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) { 316 317 data, _, err := utils.GetRequest("https://api.bybit.com/v2/public/symbols") 318 if err != nil { 319 return 320 } 321 var ar ByBitMarketsResponse 322 err = json.Unmarshal(data, &ar) 323 if err == nil { 324 for _, p := range ar.Result { 325 if p.Status != "Trading" { 326 continue 327 } 328 pairToNormalize := dia.ExchangePair{ 329 Symbol: p.BaseCurrency, 330 ForeignName: p.Name, 331 Exchange: s.exchangeName, 332 } 333 pair, serr := s.NormalizePair(pairToNormalize) 334 if serr == nil { 335 pairs = append(pairs, pair) 336 } else { 337 log.Error(serr) 338 } 339 } 340 } 341 return 342 } 343 344 // Error returns an error when the channel Channel() is closed 345 // and nil otherwise 346 func (s *ByBitScraper) Error() error { 347 s.errorLock.RLock() 348 defer s.errorLock.RUnlock() 349 return s.error 350 } 351 352 // ByBitPairScraper implements PairScraper for ByBit 353 type ByBitPairScraper struct { 354 apiEndPoint string 355 parent *ByBitScraper 356 pair dia.ExchangePair 357 closed bool 358 latestTrade int 359 } 360 361 // Close stops listening for trades of the pair associated 362 func (ps *ByBitPairScraper) Close() error { 363 ps.closed = true 364 return ps.Error() 365 } 366 367 // Error returns an error when the channel Channel() is closed 368 // and nil otherwise 369 func (ps *ByBitPairScraper) Error() error { 370 ps.parent.errorLock.RLock() 371 defer ps.parent.errorLock.RUnlock() 372 return ps.parent.error 373 } 374 375 // Pair returns the pair this scraper is subscribed to 376 func (ps *ByBitPairScraper) Pair() dia.ExchangePair { 377 return ps.pair 378 }