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  }