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, "e); 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 }