github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/historical-scrapers/CoingeckoScraper.go (about)

     1  package historicalscrapers
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"time"
     8  
     9  	log "github.com/sirupsen/logrus"
    10  
    11  	"net/http"
    12  
    13  	"github.com/diadata-org/diadata/pkg/dia"
    14  	models "github.com/diadata-org/diadata/pkg/model"
    15  	"github.com/diadata-org/diadata/pkg/utils"
    16  )
    17  
    18  type cgPriceUSD struct {
    19  	PriceUSD float64 `json:"usd"`
    20  }
    21  
    22  type cgAPIResponse struct {
    23  	Quote struct {
    24  		CurrentPrice cgPriceUSD `json:"current_price"`
    25  	} `json:"market_data"`
    26  }
    27  
    28  type CoingeckoScraper struct {
    29  	datastore        *models.DB
    30  	rdb              *models.RelDB
    31  	quotationChannel chan models.AssetQuotation
    32  	doneChannel      chan bool
    33  	firstDate        string
    34  	apiKey           string
    35  }
    36  
    37  const (
    38  	firstDate       = "01-01-2018"
    39  	cgDateLayout    = "02-01-2006"
    40  	cgErrCountLimit = 5
    41  	cgWaitSeconds   = 1
    42  )
    43  
    44  func NewCoingeckoScraper(rdb *models.RelDB, datastore *models.DB, apiKey string) (cgScraper CoingeckoScraper) {
    45  	cgScraper.doneChannel = make(chan bool)
    46  	cgScraper.quotationChannel = make(chan models.AssetQuotation)
    47  	cgScraper.firstDate = firstDate
    48  	cgScraper.datastore = datastore
    49  	cgScraper.rdb = rdb
    50  	cgScraper.apiKey = apiKey
    51  
    52  	go func() {
    53  		cgScraper.FetchQuotations()
    54  	}()
    55  	return cgScraper
    56  }
    57  
    58  func (s CoingeckoScraper) FetchQuotations() {
    59  
    60  	log.Info("Starting historical quotes scraper for Coingecko.")
    61  
    62  	// Outlook: Coingecko id for asset and blockchain,address as input so we can run
    63  	// the scraper for various assets.
    64  	ethAsset := dia.Asset{
    65  		Symbol:     "ETH",
    66  		Name:       "Ethereum",
    67  		Address:    "0x0000000000000000000000000000000000000000",
    68  		Decimals:   18,
    69  		Blockchain: "Ethereum",
    70  	}
    71  
    72  	// cgAPIKey := utils.Getenv("CG_API_KEY", "")
    73  	endDate, err := time.Parse(cgDateLayout, time.Now().Format(cgDateLayout))
    74  	if err != nil {
    75  		log.Error("Normalize date: ", err)
    76  	}
    77  	// Fetch CG data up to @endDateCG. Afterwards, use our DB.
    78  	var endDateCG time.Time
    79  	var currentDate time.Time
    80  
    81  	oldestQuotation, err := s.datastore.GetOldestQuotation(ethAsset)
    82  	if err != nil {
    83  		log.Error("Fetch oldest quotation: ", err)
    84  		endDateCG = endDate
    85  	} else {
    86  		// We only need CG data up to the oldest quotation in our DB. Normalize date to cg format.
    87  		endDateCG, err = time.Parse(cgDateLayout, oldestQuotation.Time.AddDate(0, 0, 1).Format(cgDateLayout))
    88  		if err != nil {
    89  			log.Error("Normalize date: ", err)
    90  		}
    91  	}
    92  
    93  	// Get latest recorded timestamp in historicalquotation table.
    94  	latestTimestamp, err := s.rdb.GetLastHistoricalQuotationTimestamp(ethAsset)
    95  	if err != nil {
    96  		log.Warn("fetch latestTimestamp: ", err)
    97  		var errDate error
    98  		currentDate, errDate = time.Parse(cgDateLayout, s.firstDate)
    99  		if errDate != nil {
   100  			log.Fatal("parse cg first date: ", errDate)
   101  		}
   102  		s.fetchCGPrices(endDateCG, currentDate, ethAsset)
   103  		currentDate = endDateCG
   104  	} else {
   105  		currentDate = latestTimestamp.AddDate(0, 0, 1)
   106  	}
   107  
   108  	// In case the oldest quotation from DIA DB is not old enough, fetch remaining gap
   109  	// from Coingecko...
   110  	if currentDate.Before(endDateCG) {
   111  		s.fetchCGPrices(endDateCG, currentDate, ethAsset)
   112  		currentDate = endDateCG
   113  	}
   114  	// ... otherwise fetch quotations from DIA DB.
   115  	for currentDate.Before(endDate) {
   116  		quotation, err := s.datastore.GetAssetQuotation(ethAsset, dia.CRYPTO_ZERO_UNIX_TIME, currentDate)
   117  		if err != nil {
   118  			log.Fatal("Get asset quotation: ", err)
   119  		}
   120  		currentDate = currentDate.AddDate(0, 0, 1)
   121  		s.quotationChannel <- *quotation
   122  		time.Sleep(1 * time.Second)
   123  	}
   124  	s.doneChannel <- true
   125  
   126  }
   127  
   128  func (s *CoingeckoScraper) fetchCGPrices(endDate time.Time, currentDate time.Time, asset dia.Asset) {
   129  	var errCount int
   130  	for endDate.After(currentDate) {
   131  		price, errQuot := fetchCGQuotation("ethereum", currentDate.Format(cgDateLayout), s.apiKey)
   132  		if errQuot != nil {
   133  			log.Error("fetch CG quotation: ", errQuot)
   134  			if errCount < cgErrCountLimit {
   135  				time.Sleep(3 * time.Second)
   136  				errCount++
   137  				continue
   138  			} else {
   139  				log.Fatal("Repeated fail on CG rest API.")
   140  			}
   141  		} else {
   142  			quotation := models.AssetQuotation{
   143  				Asset:  asset,
   144  				Price:  price,
   145  				Time:   currentDate,
   146  				Source: "Coingecko",
   147  			}
   148  			currentDate = currentDate.AddDate(0, 0, 1)
   149  			s.quotationChannel <- quotation
   150  			time.Sleep(cgWaitSeconds * time.Second)
   151  		}
   152  	}
   153  }
   154  
   155  func fetchCGQuotation(assetSymbol string, date string, apiKey string) (price float64, err error) {
   156  
   157  	var url string
   158  	if apiKey != "" {
   159  		log.Info("Found API key.")
   160  		url = fmt.Sprintf("https://pro-api.coingecko.com/api/v3/coins/%s/history?date=%s&x_cg_pro_api_key=%s", assetSymbol, date, apiKey)
   161  	} else {
   162  		log.Warn("No API key found. Proceed with standard API call.")
   163  		url = fmt.Sprintf("https://api.coingecko.com/api/v3/coins/%s/history?date=%s", assetSymbol, date)
   164  	}
   165  	response, statusCode, err := utils.GetRequest(url)
   166  	if err != nil {
   167  		return
   168  	}
   169  	if statusCode != http.StatusOK {
   170  		err = errors.New("non-200 status code from Coinmarketcap API")
   171  		return
   172  	}
   173  
   174  	var quote cgAPIResponse
   175  	if err := json.Unmarshal(response, &quote); err != nil {
   176  		log.Error("Unmarshal response: ", err)
   177  	}
   178  
   179  	price = quote.Quote.CurrentPrice.PriceUSD
   180  	return
   181  }
   182  
   183  func (s CoingeckoScraper) QuoteChannel() chan models.AssetQuotation {
   184  	return s.quotationChannel
   185  }
   186  
   187  func (s CoingeckoScraper) Done() chan bool {
   188  	return s.doneChannel
   189  }