github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/exchange-scrapers/BitstampScraper.go (about) 1 package scrapers 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "io" 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 const () 21 22 type BitstampScraper struct { 23 wsClient *ws.Conn 24 shutdown chan nothing 25 shutdownDone chan nothing 26 // error handling; to read error or closed, first acquire read lock 27 // only cleanup method should hold write lock 28 errorLock sync.RWMutex 29 error error 30 closed bool 31 32 pairScrapers map[string]*BitstampPairScraper 33 exchangeName string 34 chanTrades chan *dia.Trade 35 db *models.RelDB 36 } 37 38 type BitstampPairScraper struct { 39 parent *BitstampScraper 40 pair dia.ExchangePair 41 closed bool 42 } 43 44 type BitstampPairsInfo []struct { 45 Name string `json:"name"` 46 UrlSymbol string `json:"url_symbol"` 47 BaseDecimal uint8 `json:"base_decimal"` 48 CounterDecimals uint8 `json:"counter_decimals"` 49 InstantOrderCounterDecimals uint8 `json:"instant_order_counter_decimals"` 50 MinimumOrder string `json:"minimum_order"` 51 Trading string `json:"trading"` 52 InstantAndMarketOrders string `json:"instant_and_market_orders"` 53 Description string `json:"description"` 54 } 55 56 type BitstampWsResponse struct { 57 Event string `json:"event"` 58 Channel string `json:"channel"` 59 Data interface{} `json:"data"` 60 } 61 62 type BitstampPingData struct { 63 Status string `json:"status"` 64 } 65 66 type BitstampTradeData struct { 67 Id string `json:"id"` 68 Amount float64 `json:"amount"` 69 AmountStr string `json:"amount_str"` 70 Price float64 `json:"price"` 71 PriceStr string `json:"price_str"` 72 Type uint8 `json:"type"` 73 Timestamp string `json:"timestamp"` 74 Microtimestamp string `json:"microtimestamp"` 75 BuyOrderId uint64 `json:"buy_order_id"` 76 SellOrderId uint64 `json:"sell_order_id"` 77 } 78 79 func NewBitstampScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *BitstampScraper { 80 s := &BitstampScraper{ 81 shutdown: make(chan nothing), 82 shutdownDone: make(chan nothing), 83 error: nil, 84 pairScrapers: make(map[string]*BitstampPairScraper), 85 exchangeName: exchange.Name, 86 chanTrades: make(chan *dia.Trade), 87 db: relDB, 88 } 89 var wsDialer ws.Dialer 90 wsConn, _, err := wsDialer.Dial("wss://ws.bitstamp.net", nil) 91 if err != nil { 92 log.Error("Websocket connect error", err) 93 } 94 s.wsClient = wsConn 95 if scrape { 96 go s.mainLoop() 97 } 98 return s 99 } 100 101 func extractUrlSymbolFromChannel(channel string) (after string) { 102 // Channel is live_trades_{pair} 103 // Remove the prefix live_trades_ 104 if strings.HasPrefix(channel, "live_trades_") { 105 after = strings.Split(channel, "live_trades_")[1] 106 } 107 return after 108 } 109 110 func (s *BitstampScraper) foreignNameToUrlSymbol(foreignName string) string { 111 return strings.ToLower(strings.ReplaceAll(foreignName, "-", "")) 112 } 113 114 func (s *BitstampScraper) receive() { 115 tmFalseDuplicateTrades := timedmap.New(duplicateTradesScanFrequency) 116 tmDuplicateTrades := timedmap.New(duplicateTradesScanFrequency) 117 118 var resp BitstampWsResponse 119 if err := s.wsClient.ReadJSON(&resp); err != nil { 120 log.Error("Receive message error:", err) 121 return 122 } 123 foreignName := extractUrlSymbolFromChannel(resp.Channel) 124 125 switch event := resp.Event; event { 126 case "bts:subscription_succeeded": 127 log.Info("Subsription succeeded:", foreignName) 128 // TODO: add subscription failed... 129 // Probably "bts:subscription_failed" 130 // Just a guess 131 case "bts:request_reconnect": 132 // TODO: handle reconnect 133 log.Warn("Server request for a reconnect") 134 case "bts:heartbeat": 135 var data BitstampPingData 136 if err := json.Unmarshal([]byte(resp.Data.(string)), &data); err != nil { 137 log.Warn("Unmarshal ping error:", err, resp.Data) 138 } else if data.Status == "success" { 139 log.Info("Heart is beating") 140 } else { 141 log.Warning("Check Heart", data) 142 } 143 default: 144 145 if strings.HasPrefix(event, "trade") { 146 147 data := resp.Data.(map[string]interface{}) 148 ps, ok := s.pairScrapers[foreignName] 149 150 if ok { 151 timestamp, _ := strconv.ParseInt(data["microtimestamp"].(string), 10, 64) 152 volume := data["amount"].(float64) 153 side := data["type"].(float64) 154 if side == 1 { 155 volume *= -1 156 } 157 158 pair, err := s.db.GetExchangePairCache(s.exchangeName, foreignName) 159 if err != nil { 160 log.Error("get exchange pair from cache: ", err) 161 } 162 163 t := &dia.Trade{ 164 Symbol: ps.pair.Symbol, 165 Pair: foreignName, 166 Price: data["price"].(float64), 167 Volume: volume, 168 Time: time.Unix(0, 1000*timestamp), 169 Source: s.exchangeName, 170 ForeignTradeID: fmt.Sprintf("%d", int(data["id"].(float64))), 171 VerifiedPair: pair.Verified, 172 QuoteToken: pair.UnderlyingPair.QuoteToken, 173 BaseToken: pair.UnderlyingPair.BaseToken, 174 } 175 176 // Handle duplicate trades. 177 discardTrade := t.IdentifyDuplicateFull(tmFalseDuplicateTrades, duplicateTradesMemory) 178 if !discardTrade { 179 t.IdentifyDuplicateTagset(tmDuplicateTrades, duplicateTradesMemory) 180 s.chanTrades <- t 181 } 182 log.Info("Found trade:", t) 183 } 184 } else { 185 log.Warnf("Unidentified response event %s: -- %v", event, resp) 186 } 187 } 188 } 189 190 func (s *BitstampScraper) mainLoop() { 191 for { 192 select { 193 case <-s.shutdown: 194 log.Warn("Shutting down BitstampScraper") 195 s.cleanup(nil) 196 return 197 default: 198 } 199 s.receive() 200 } 201 } 202 203 func (s *BitstampScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) { 204 s.errorLock.RLock() 205 defer s.errorLock.RUnlock() 206 if s.error != nil { 207 return nil, s.error 208 } 209 if s.closed { 210 return nil, errors.New("BitstampScraper: Call ScrapePair on closed scraper") 211 } 212 ps := &BitstampPairScraper{ 213 parent: s, 214 pair: pair, 215 } 216 s.pairScrapers[pair.ForeignName] = ps 217 218 // Subscribe to pair 219 urlSymbol := s.foreignNameToUrlSymbol(pair.ForeignName) 220 message := map[string]interface{}{ 221 "event": "bts:subscribe", 222 "data": map[string]interface{}{ 223 "channel": "live_trades_" + urlSymbol, 224 }, 225 } 226 err := s.wsClient.WriteJSON(message) 227 if err != nil { 228 log.Error("Error sending subscription for", ps.pair.ForeignName, err) 229 } 230 log.Info("Sent subscription for:", ps.pair.ForeignName) 231 232 return ps, nil 233 } 234 235 func (s *BitstampScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) { 236 var bitstampPairsInfo BitstampPairsInfo 237 resp, err := http.Get("https://www.bitstamp.net/api/v2/trading-pairs-info/") 238 if err != nil { 239 log.Error("Get Pairs:", err) 240 } 241 242 defer resp.Body.Close() 243 body, err := io.ReadAll(resp.Body) 244 if err != nil { 245 log.Error("Read pair body:", err) 246 } 247 248 err = json.Unmarshal(body, &bitstampPairsInfo) 249 if err != nil { 250 log.Error("Unmarshal pairs:", err) 251 } 252 253 for _, p := range bitstampPairsInfo { 254 pairToNormalized := dia.ExchangePair{ 255 Symbol: strings.Split(p.Name, "/")[0], 256 ForeignName: p.UrlSymbol, 257 Exchange: s.exchangeName, 258 UnderlyingPair: dia.Pair{ 259 QuoteToken: dia.Asset{ 260 Symbol: strings.Split(p.Name, "/")[0], 261 }, 262 BaseToken: dia.Asset{ 263 Symbol: strings.Split(p.Name, "/")[1], 264 }, 265 }, 266 } 267 pairs = append(pairs, pairToNormalized) 268 } 269 return 270 } 271 272 func (s *BitstampScraper) FillSymbolData(symbol string) (dia.Asset, error) { 273 return dia.Asset{Symbol: symbol}, nil 274 } 275 276 func (s *BitstampScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) { 277 return dia.ExchangePair{}, nil 278 } 279 280 func (s *BitstampScraper) Channel() chan *dia.Trade { 281 return s.chanTrades 282 } 283 284 func (s *BitstampScraper) cleanup(err error) { 285 s.errorLock.Lock() 286 defer s.errorLock.Unlock() 287 if err != nil { 288 s.error = err 289 } 290 s.closed = true 291 close(s.shutdownDone) 292 } 293 294 func (s *BitstampScraper) Close() error { 295 if s.closed { 296 return errors.New("BitstampScraper: Already closed") 297 } 298 if err := s.wsClient.Close(); err != nil { 299 log.Error("Error closing Bitstamp.wsClient", err) 300 } 301 close(s.shutdown) 302 <-s.shutdownDone 303 defer s.errorLock.RUnlock() 304 return s.error 305 } 306 307 func (ps *BitstampPairScraper) Close() error { 308 ps.closed = true 309 return nil 310 } 311 312 func (ps *BitstampPairScraper) Error() error { 313 s := ps.parent 314 s.errorLock.RLock() 315 defer s.errorLock.RUnlock() 316 return s.error 317 } 318 319 func (ps *BitstampPairScraper) Pair() dia.ExchangePair { 320 return ps.pair 321 }