github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/exchange-scrapers/CoinBaseScraper.go (about) 1 package scrapers 2 3 import ( 4 "encoding/json" 5 "errors" 6 "strconv" 7 "sync" 8 9 "github.com/diadata-org/diadata/pkg/dia" 10 models "github.com/diadata-org/diadata/pkg/model" 11 "github.com/diadata-org/diadata/pkg/utils" 12 ws "github.com/gorilla/websocket" 13 gdax "github.com/preichenberger/go-coinbasepro/v2" 14 "github.com/zekroTJA/timedmap" 15 ) 16 17 type CoinBaseScraper struct { 18 // signaling channels 19 shutdown chan nothing 20 shutdownDone chan nothing 21 // error handling; to read error or closed, first acquire read lock 22 // only cleanup method should hold write lock 23 errorLock sync.RWMutex 24 error error 25 closed bool 26 pairScrapers map[string]*CoinBasePairScraper // pc.ExchangePair -> pairScraperSet 27 wsConn *ws.Conn 28 exchangeName string 29 chanTrades chan *dia.Trade 30 db *models.RelDB 31 } 32 33 const ( 34 ChannelHeartbeat = "heartbeat" 35 ChannelTicker = "ticker" 36 ChannelLevel2 = "level2" 37 ChannelUser = "user" 38 ChannelMatches = "matches" 39 ChannelFull = "full" 40 ) 41 42 // NewCoinBaseScraper returns a new CoinBaseScraper initialized with default values. 43 // The instance is asynchronously scraping as soon as it is created. 44 func NewCoinBaseScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *CoinBaseScraper { 45 s := &CoinBaseScraper{ 46 shutdown: make(chan nothing), 47 shutdownDone: make(chan nothing), 48 pairScrapers: make(map[string]*CoinBasePairScraper), 49 exchangeName: exchange.Name, 50 error: nil, 51 chanTrades: make(chan *dia.Trade), 52 db: relDB, 53 } 54 var wsDialer ws.Dialer 55 SwConn, _, err := wsDialer.Dial(utils.Getenv("WEBSOCKET_API_URL", "wss://ws-feed.exchange.coinbase.com"), nil) 56 if err != nil { 57 println(err.Error()) 58 } 59 s.wsConn = SwConn 60 if scrape { 61 go s.mainLoop() 62 } 63 return s 64 } 65 66 // mainLoop runs in a goroutine until channel s is closed. 67 func (s *CoinBaseScraper) mainLoop() { 68 var err error 69 tmFalseDuplicateTrades := timedmap.New(duplicateTradesScanFrequency) 70 tmDuplicateTrades := timedmap.New(duplicateTradesScanFrequency) 71 72 for { 73 message := gdax.Message{} 74 if err = s.wsConn.ReadJSON(&message); err != nil { 75 println(err.Error()) 76 break 77 } 78 if message.Type == ChannelTicker { 79 ps, ok := s.pairScrapers[message.ProductID] 80 if ok { 81 var f64Price float64 82 var f64Volume float64 83 var exchangepair dia.ExchangePair 84 f64Price, err = strconv.ParseFloat(message.Price, 64) 85 if err == nil { 86 f64Volume, err = strconv.ParseFloat(message.LastSize, 64) 87 if err == nil { 88 if message.TradeID != 0 { 89 if message.Side == "sell" { 90 f64Volume = -f64Volume 91 } 92 93 exchangepair, err = s.db.GetExchangePairCache(s.exchangeName, message.ProductID) 94 if err != nil { 95 log.Error("get exchangepair from cache: ", err) 96 } 97 t := &dia.Trade{ 98 Symbol: ps.pair.Symbol, 99 Pair: message.ProductID, 100 Price: f64Price, 101 Volume: f64Volume, 102 Time: message.Time.Time(), 103 ForeignTradeID: strconv.FormatInt(int64(message.TradeID), 16), 104 Source: s.exchangeName, 105 VerifiedPair: exchangepair.Verified, 106 BaseToken: exchangepair.UnderlyingPair.BaseToken, 107 QuoteToken: exchangepair.UnderlyingPair.QuoteToken, 108 } 109 if t.VerifiedPair { 110 log.Info("got verified trade: ", t) 111 } 112 // Handle duplicate trades. 113 discardTrade := t.IdentifyDuplicateFull(tmFalseDuplicateTrades, duplicateTradesMemory) 114 if !discardTrade { 115 t.IdentifyDuplicateTagset(tmDuplicateTrades, duplicateTradesMemory) 116 ps.parent.chanTrades <- t 117 } 118 119 } 120 } else { 121 log.Error("error parsing LastSize " + message.LastSize) 122 } 123 } else { 124 log.Error("error parsing price " + message.Price) 125 } 126 } else { 127 log.Error("unknown productError" + message.ProductID) 128 } 129 } 130 } 131 s.cleanup(err) 132 } 133 134 // closes all connected PairScrapers 135 // must only be called from mainLoop 136 func (s *CoinBaseScraper) cleanup(err error) { 137 s.errorLock.Lock() 138 defer s.errorLock.Unlock() 139 if err != nil { 140 s.error = err 141 } 142 s.closed = true 143 close(s.shutdownDone) // signal that shutdown is complete 144 } 145 146 // Close closes any existing API connections, as well as channels of 147 // PairScrapers from calls to ScrapePair 148 func (s *CoinBaseScraper) Close() error { 149 if s.closed { 150 return errors.New("CoinBaseScraper: Already closed") 151 } 152 err := s.wsConn.Close() 153 if err != nil { 154 log.Error(err) 155 } 156 close(s.shutdown) 157 <-s.shutdownDone 158 s.errorLock.RLock() 159 defer s.errorLock.RUnlock() 160 return s.error 161 } 162 163 func (s *CoinBaseScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) { 164 // str := strings.Split(pair.ForeignName, "-") 165 // symbol := str[0] 166 // pair.Symbol = symbol 167 // if helpers.NameForSymbol(symbol) == symbol { 168 // return pair, errors.New("Foreign name can not be normalized:" + pair.ForeignName + " symbol:" + symbol) 169 // } 170 // if helpers.SymbolIsBlackListed(symbol) { 171 // return pair, errors.New("Symbol is black listed:" + symbol) 172 // } 173 return pair, nil 174 175 } 176 177 // FetchAvailablePairs returns a list with all available trade pairs 178 func (s *CoinBaseScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) { 179 180 data, _, err := utils.GetRequest("https://api.pro.coinbase.com/products") 181 if err != nil { 182 return 183 } 184 var ar []gdax.Product 185 err = json.Unmarshal(data, &ar) 186 if err == nil { 187 for _, p := range ar { 188 pairToNormalise := dia.ExchangePair{ 189 Symbol: p.BaseCurrency, 190 ForeignName: p.ID, 191 Exchange: s.exchangeName, 192 } 193 pair, serr := s.NormalizePair(pairToNormalise) 194 if serr == nil { 195 pairs = append(pairs, pair) 196 } else { 197 log.Error(serr) 198 } 199 } 200 } 201 return 202 } 203 204 // FillSymbolData collects all available information on an asset traded on CoinBase 205 func (s *CoinBaseScraper) FillSymbolData(symbol string) (asset dia.Asset, err error) { 206 var response gdax.Currency 207 data, _, err := utils.GetRequest("https://api.pro.coinbase.com/currencies/" + symbol) 208 if err != nil { 209 return 210 } 211 err = json.Unmarshal(data, &response) 212 if err != nil { 213 return 214 } 215 asset.Symbol = response.ID 216 asset.Name = response.Name 217 return asset, nil 218 } 219 220 // CoinBasePairScraper implements PairScraper for GDax 221 type CoinBasePairScraper struct { 222 parent *CoinBaseScraper 223 pair dia.ExchangePair 224 closed bool 225 lastRecord int64 226 } 227 228 // ScrapePair returns a PairScraper that can be used to get trades for a single pair from 229 // this APIScraper 230 func (s *CoinBaseScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) { 231 232 s.errorLock.RLock() 233 defer s.errorLock.RUnlock() 234 if s.error != nil { 235 return nil, s.error 236 } 237 if s.closed { 238 return nil, errors.New("CoinBaseScraper: Call ScrapePair on closed scraper") 239 } 240 ps := &CoinBasePairScraper{ 241 parent: s, 242 pair: pair, 243 lastRecord: 0, //TODO FIX to figure out the last we got... 244 } 245 246 s.pairScrapers[pair.ForeignName] = ps 247 248 subscribe := gdax.Message{ 249 Type: "subscribe", 250 Channels: []gdax.MessageChannel{ 251 { 252 Name: ChannelHeartbeat, 253 ProductIds: []string{ 254 pair.ForeignName, 255 }, 256 }, 257 { 258 Name: ChannelTicker, 259 ProductIds: []string{ 260 pair.ForeignName, 261 }, 262 }, 263 }, 264 } 265 if err := s.wsConn.WriteJSON(subscribe); err != nil { 266 println(err.Error()) 267 } 268 269 return ps, nil 270 } 271 272 // Channel returns a channel that can be used to receive trades/pricing information 273 func (ps *CoinBaseScraper) Channel() chan *dia.Trade { 274 return ps.chanTrades 275 } 276 277 func (ps *CoinBasePairScraper) Close() error { 278 ps.closed = true 279 return nil 280 } 281 282 // Error returns an error when the channel Channel() is closed 283 // and nil otherwise 284 func (ps *CoinBasePairScraper) Error() error { 285 s := ps.parent 286 s.errorLock.RLock() 287 defer s.errorLock.RUnlock() 288 return s.error 289 } 290 291 // Pair returns the pair this scraper is subscribed to 292 func (ps *CoinBasePairScraper) Pair() dia.ExchangePair { 293 return ps.pair 294 }