github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/exchange-scrapers/BitgetScraper.go (about) 1 package scrapers 2 3 import ( 4 "errors" 5 "strconv" 6 "strings" 7 "sync" 8 "time" 9 10 "github.com/diadata-org/diadata/pkg/dia" 11 models "github.com/diadata-org/diadata/pkg/model" 12 ws "github.com/gorilla/websocket" 13 "github.com/zekroTJA/timedmap" 14 ) 15 16 const ( 17 bitgetWsAPI = "wss://ws.bitget.com/v2/ws/public" 18 bitgetPingInterval = 30 19 ) 20 21 type bitgetSubscribeMessage struct { 22 Operation string `json:"op"` 23 Arguments []bitgetSubscribeArguments `json:"args"` 24 } 25 26 type bitgetSubscribeArguments struct { 27 InstrumentType string `json:"instType"` 28 Channel string `json:"channel"` 29 InstrumentID string `json:"instId"` 30 } 31 32 type bitgetWsResponse struct { 33 Action string `json:"action"` 34 Argument bitgetArgument `json:"arg"` 35 Data []bitgetData `json:"data"` 36 Timestamp int64 `json:"ts"` 37 } 38 39 type bitgetArgument struct { 40 InstrumentType string `json:"instType"` 41 Channel string `json:"channel"` 42 InstrumentID string `json:"instId"` 43 } 44 45 type bitgetData struct { 46 Timestamp string `json:"ts"` 47 Price string `json:"price"` 48 Volume string `json:"size"` 49 Side string `json:"side"` 50 ForeignTradeID string `json:"tradeId"` 51 } 52 53 type BitgetScraper struct { 54 // signaling channels 55 shutdown chan nothing 56 shutdownDone chan nothing 57 // error handling; to read error or closed, first acquire read lock 58 // only cleanup method should hold write lock 59 errorLock sync.RWMutex 60 error error 61 closed bool 62 pairScrapers map[string]*BitgetPairScraper // pc.ExchangePair -> pairScraperSet 63 wsConn *ws.Conn 64 exchangeName string 65 chanTrades chan *dia.Trade 66 db *models.RelDB 67 } 68 69 // NewBitgetScraper returns a new BitgetScraper initialized with default values. 70 // The instance is asynchronously scraping as soon as it is created. 71 func NewBitgetScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *BitgetScraper { 72 s := &BitgetScraper{ 73 shutdown: make(chan nothing), 74 shutdownDone: make(chan nothing), 75 pairScrapers: make(map[string]*BitgetPairScraper), 76 exchangeName: exchange.Name, 77 error: nil, 78 chanTrades: make(chan *dia.Trade), 79 db: relDB, 80 } 81 var wsDialer ws.Dialer 82 SwConn, _, err := wsDialer.Dial(bitgetWsAPI, nil) 83 if err != nil { 84 log.Errorf("Dial websocket api: %s", err.Error()) 85 } 86 87 go s.pingRoutine(time.Duration(bitgetPingInterval * time.Second)) 88 89 s.wsConn = SwConn 90 if scrape { 91 go s.mainLoop() 92 } 93 return s 94 } 95 96 // mainLoop runs in a goroutine until channel s is closed. 97 func (s *BitgetScraper) mainLoop() { 98 99 tmFalseDuplicateTrades := timedmap.New(duplicateTradesScanFrequency) 100 tmDuplicateTrades := timedmap.New(duplicateTradesScanFrequency) 101 time.Sleep(5 * time.Second) 102 103 for { 104 105 // Check if we get a pong message back. 106 _, p, err := s.wsConn.ReadMessage() 107 if err != nil { 108 log.Error("ReadMessage: ", err) 109 } else { 110 if strings.Contains(string(p), "pong") || strings.Contains(string(p), "ng") { 111 log.Infof("got %s", string(p)) 112 } 113 } 114 115 var message bitgetWsResponse 116 if err = s.wsConn.ReadJSON(&message); err != nil { 117 log.Errorf("ReadJSON: %s", err.Error()) 118 log.Info("instead of pong got ", string(p)) 119 if strings.Contains(err.Error(), "invalid character") { 120 continue 121 } 122 return 123 } 124 125 ps, ok := s.pairScrapers[message.Argument.InstrumentID] 126 if ok && message.Action != "snapshot" { 127 for _, data := range message.Data { 128 var f64Price float64 129 var f64Volume float64 130 var exchangepair dia.ExchangePair 131 f64Price, err = strconv.ParseFloat(data.Price, 64) 132 if err != nil { 133 log.Error("error parsing price " + data.Price) 134 } 135 f64Volume, err = strconv.ParseFloat(data.Volume, 64) 136 if err != nil { 137 log.Error("error parsing volume " + data.Volume) 138 } 139 140 if data.Side != "buy" { 141 f64Volume = -f64Volume 142 } 143 144 timestamp, err := strconv.ParseInt(data.Timestamp, 10, 64) 145 if err != nil { 146 log.Error("Parse timestamp: ", err) 147 } 148 149 exchangepair, err = s.db.GetExchangePairCache(s.exchangeName, message.Argument.InstrumentID) 150 if err != nil { 151 // log.Error("get exchangepair from cache: ", err) 152 } 153 t := dia.Trade{ 154 Symbol: ps.pair.Symbol, 155 Pair: message.Argument.InstrumentID, 156 Price: f64Price, 157 Volume: f64Volume, 158 Time: time.Unix(0, timestamp*1e6), 159 ForeignTradeID: data.ForeignTradeID, 160 Source: s.exchangeName, 161 VerifiedPair: exchangepair.Verified, 162 BaseToken: exchangepair.UnderlyingPair.BaseToken, 163 QuoteToken: exchangepair.UnderlyingPair.QuoteToken, 164 } 165 if t.VerifiedPair { 166 log.Info("got verified trade: ", t) 167 } else { 168 log.Infof("got trade at %v : %s -- %v -- %v", t.Time, t.Pair, t.Price, t.Volume) 169 } 170 // Handle duplicate trades. 171 discardTrade := t.IdentifyDuplicateFull(tmFalseDuplicateTrades, duplicateTradesMemory) 172 if !discardTrade { 173 t.IdentifyDuplicateTagset(tmDuplicateTrades, duplicateTradesMemory) 174 ps.parent.chanTrades <- &t 175 } 176 } 177 178 } 179 } 180 181 } 182 183 func (s *BitgetScraper) cleanup(err error) { 184 s.errorLock.Lock() 185 defer s.errorLock.Unlock() 186 if err != nil { 187 s.error = err 188 } 189 s.closed = true 190 close(s.shutdownDone) // signal that shutdown is complete 191 } 192 193 // Close closes any existing API connections, as well as channels of 194 // PairScrapers from calls to ScrapePair 195 func (s *BitgetScraper) Close() error { 196 if s.closed { 197 return errors.New("BitgetScraper: Already closed") 198 } 199 err := s.wsConn.Close() 200 if err != nil { 201 log.Error(err) 202 } 203 close(s.shutdown) 204 <-s.shutdownDone 205 s.errorLock.RLock() 206 defer s.errorLock.RUnlock() 207 return s.error 208 } 209 210 func (s *BitgetScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) { 211 return pair, nil 212 } 213 214 func (s *BitgetScraper) pingRoutine(d time.Duration) { 215 ticker := time.NewTicker(d) 216 for range ticker.C { 217 if err := s.wsConn.WriteMessage(ws.TextMessage, []byte("ping")); err != nil { 218 log.Errorf("send ping: %s.", err.Error()) 219 } else { 220 log.Info("sent ping.") 221 } 222 } 223 } 224 225 // FetchAvailablePairs returns a list with all available trade pairs 226 func (s *BitgetScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) { 227 228 // data, _, err := utils.GetRequest("https://api.pro.coinbase.com/products") 229 // if err != nil { 230 // return 231 // } 232 233 // err = json.Unmarshal(data, &ar) 234 // if err == nil { 235 // for _, p := range ar { 236 // pairToNormalise := dia.ExchangePair{ 237 // Symbol: p.BaseCurrency, 238 // ForeignName: p.ID, 239 // Exchange: s.exchangeName, 240 // } 241 // pair, serr := s.NormalizePair(pairToNormalise) 242 // if serr == nil { 243 // pairs = append(pairs, pair) 244 // } else { 245 // log.Error(serr) 246 // } 247 // } 248 // } 249 return 250 } 251 252 // FillSymbolData collects all available information on an asset traded on Bitget 253 func (s *BitgetScraper) FillSymbolData(symbol string) (asset dia.Asset, err error) { 254 asset.Symbol = symbol 255 return asset, nil 256 } 257 258 // BitgetPairScraper implements PairScraper 259 type BitgetPairScraper struct { 260 parent *BitgetScraper 261 pair dia.ExchangePair 262 closed bool 263 lastRecord int64 264 } 265 266 // ScrapePair returns a PairScraper that can be used to get trades for a single pair from 267 // this APIScraper 268 func (s *BitgetScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) { 269 270 s.errorLock.RLock() 271 defer s.errorLock.RUnlock() 272 if s.error != nil { 273 return nil, s.error 274 } 275 if s.closed { 276 return nil, errors.New("BitgetScraper: Call ScrapePair on closed scraper") 277 } 278 ps := &BitgetPairScraper{ 279 parent: s, 280 pair: pair, 281 lastRecord: 0, 282 } 283 284 s.pairScrapers[pair.ForeignName] = ps 285 286 subscribeMessage := bitgetSubscribeMessage{ 287 Operation: "subscribe", 288 Arguments: []bitgetSubscribeArguments{ 289 { 290 InstrumentType: "SPOT", 291 Channel: "trade", 292 InstrumentID: pair.ForeignName, 293 }, 294 }, 295 } 296 if err := s.wsConn.WriteJSON(subscribeMessage); err != nil { 297 println(err.Error()) 298 } 299 log.Info("subscribed to: ", pair.ForeignName) 300 301 return ps, nil 302 } 303 304 // Channel returns a channel that can be used to receive trades/pricing information 305 func (ps *BitgetScraper) Channel() chan *dia.Trade { 306 return ps.chanTrades 307 } 308 309 func (ps *BitgetPairScraper) Close() error { 310 ps.closed = true 311 return nil 312 } 313 314 // Error returns an error when the channel Channel() is closed 315 // and nil otherwise 316 func (ps *BitgetPairScraper) Error() error { 317 s := ps.parent 318 s.errorLock.RLock() 319 defer s.errorLock.RUnlock() 320 return s.error 321 } 322 323 // Pair returns the pair this scraper is subscribed to 324 func (ps *BitgetPairScraper) Pair() dia.ExchangePair { 325 return ps.pair 326 }