diff --git a/apps/api/src/services/configuration.service.ts b/apps/api/src/services/configuration.service.ts index 60e36623e..7441429dd 100644 --- a/apps/api/src/services/configuration.service.ts +++ b/apps/api/src/services/configuration.service.ts @@ -19,7 +19,7 @@ export class ConfigurationService { CACHE_TTL: num({ default: 1 }), DATA_SOURCE_PRIMARY: str({ default: DataSource.YAHOO }), DATA_SOURCES: json({ - default: [DataSource.MANUAL, DataSource.YAHOO] + default: [DataSource.MANUAL, DataSource.YAHOO, DataSource.COINGECKO] }), ENABLE_FEATURE_BLOG: bool({ default: false }), ENABLE_FEATURE_CUSTOM_SYMBOLS: bool({ default: false }), diff --git a/apps/api/src/services/data-provider/coingecko/coingecko-data.service.ts b/apps/api/src/services/data-provider/coingecko/coingecko-data.service.ts new file mode 100644 index 000000000..ee8b00a7f --- /dev/null +++ b/apps/api/src/services/data-provider/coingecko/coingecko-data.service.ts @@ -0,0 +1,198 @@ +import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; +import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; +import { + IDataProviderHistoricalResponse, + IDataProviderResponse +} from '@ghostfolio/api/services/interfaces/interfaces'; +import { DATE_FORMAT } from '@ghostfolio/common/helper'; +import { Granularity } from '@ghostfolio/common/types'; +import { Injectable, Logger } from '@nestjs/common'; +import { + AssetClass, + AssetSubClass, + DataSource, + SymbolProfile +} from '@prisma/client'; +import bent from 'bent'; +import { format, differenceInDays, addDays, subDays } from 'date-fns'; + +@Injectable() +export class CoinGeckoDataService implements DataProviderInterface { + private readonly URL = 'https://api.coingecko.com/api/v3'; + private COINLIST = []; + private DEFAULT_CURRENCY: string; + private DB = {}; + + public constructor( + private readonly configurationService: ConfigurationService + ) { + this.DEFAULT_CURRENCY = this.configurationService + .get('BASE_CURRENCY') + .toUpperCase(); + this.DB = {}; + } + + public canHandle(symbol: string) { + return true; + } + + public async getDividends({ + from, + granularity = 'day', + symbol, + to + }: { + from: Date; + granularity: Granularity; + symbol: string; + to: Date; + }) { + return {}; + } + + public async getCoinList() { + // TODO: Some caching refresh after X? + if (this.COINLIST.length == 0) { + const req = bent(`${this.URL}/coins/list`, 'GET', 'json', 200); + const response = await req(); + this.COINLIST = response; + } + } + + public async getAssetProfile( + aSymbol: string + ): Promise> { + return { + assetClass: AssetClass.CASH, + assetSubClass: AssetSubClass.CRYPTOCURRENCY, + currency: this.DEFAULT_CURRENCY.toUpperCase(), + dataSource: this.getName(), + name: aSymbol, + symbol: aSymbol + }; + } + + public async popolateDb(datefrom: Date, symbol: string) { + let start_day; + let end_day; + datefrom.setHours(0, 0, 1); + start_day = Math.round(datefrom.getTime() / 1000); + end_day = Math.round(new Date().getTime() / 1000); + const targeturl = `${ + this.URL + }/coins/${symbol.toLowerCase()}/market_chart/range?vs_currency=${this.DEFAULT_CURRENCY.toLowerCase()}&from=${start_day}&to=${end_day}`; + const req = bent(targeturl, 'GET', 'json', 200); + const response = await req(); + if (response.prices.length) { + for (const iter of response.prices) { + let day = new Date(iter[0]); + day.setHours(0, 0, 1, 1); + let dayepoch = Math.round(day.getTime() / 1000); + this.DB[dayepoch] = iter[1]; + } + } + } + + public async getDayStat(datein: Date, symbol: string) { + let out = { marketPrice: 0 }; + let prevday = subDays(datein, 1); + datein.setHours(0, 0, 1, 1); + let start_day = Math.round(datein.getTime() / 1000); + let prev_day = Math.round(prevday.getTime() / 1000); + out['marketPrice'] = this.DB[start_day]; + if (prev_day in this.DB) { + out['performance'] = this.DB[start_day] / this.DB[prev_day]; + } else { + out['performance'] = 0; + } + return out; + } + + public async getHistorical( + aSymbol: string, + aGranularity: Granularity = 'day', + from: Date, + to: Date + ): Promise<{ + [symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; + }> { + let out = {}; + out[aSymbol] = {}; + const total_days = Math.abs(differenceInDays(from, to)) + 1; + await this.popolateDb(from, aSymbol); + for (const iter of Array(total_days).keys()) { + let day = addDays(from, iter); + let datestr = format(day, DATE_FORMAT); + out[aSymbol][datestr] = await this.getDayStat(day, aSymbol); + } + + return out; + } + + public getMaxNumberOfSymbolsPerRequest() { + // Safe Rate Limit: https://www.coingecko.com/en/api/pricing#general + return 20; + } + + public getName(): DataSource { + return DataSource.COINGECKO; + } + + public async getQuotes( + aSymbols: string[] + ): Promise<{ [symbol: string]: IDataProviderResponse }> { + var results = {}; + + if (aSymbols.length <= 0) { + return {}; + } + try { + for (const coin of aSymbols) { + const coinlower = coin.toLowerCase(); + const req = bent( + `${ + this.URL + }/simple/price?ids=${coinlower}&vs_currencies=${this.DEFAULT_CURRENCY.toLowerCase()}`, + 'GET', + 'json', + 200 + ); + const response = await req(); + const price = response[coinlower][this.DEFAULT_CURRENCY.toLowerCase()]; + + results[coin] = { + currency: this.DEFAULT_CURRENCY, + dataSource: DataSource.COINGECKO, + marketPrice: price, + marketState: 'closed' + }; + } + + return results; + } catch (error) { + Logger.error(error, 'CoinGecko'); + return {}; + } + } + + public async search(aQuery: string): Promise<{ items: LookupItem[] }> { + await this.getCoinList(); + if (aQuery.length <= 2) { + return { items: [] }; + } + var coins = []; + + for (const coiniter of this.COINLIST) { + if (coiniter.id.toLowerCase().includes(aQuery)) { + coins.push({ + symbol: coiniter.id.toUpperCase(), + currency: this.DEFAULT_CURRENCY, + dataSource: this.getName(), + name: `${coiniter.name} (From CoinGecko)` + }); + } + } + return { items: coins }; + } +} \ No newline at end of file diff --git a/apps/api/src/services/data-provider/data-provider.module.ts b/apps/api/src/services/data-provider/data-provider.module.ts index abafe6189..eb596bb3c 100644 --- a/apps/api/src/services/data-provider/data-provider.module.ts +++ b/apps/api/src/services/data-provider/data-provider.module.ts @@ -1,6 +1,7 @@ import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { CryptocurrencyModule } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.module'; import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service'; +import { CoinGeckoDataService } from '@ghostfolio/api/services/data-provider/coingecko/coingecko-data.service'; import { EodHistoricalDataService } from '@ghostfolio/api/services/data-provider/eod-historical-data/eod-historical-data.service'; import { GoogleSheetsService } from '@ghostfolio/api/services/data-provider/google-sheets/google-sheets.service'; import { ManualService } from '@ghostfolio/api/services/data-provider/manual/manual.service'; @@ -21,6 +22,7 @@ import { DataProviderService } from './data-provider.service'; ], providers: [ AlphaVantageService, + CoinGeckoDataService, DataProviderService, EodHistoricalDataService, GoogleSheetsService, @@ -30,6 +32,7 @@ import { DataProviderService } from './data-provider.service'; { inject: [ AlphaVantageService, + CoinGeckoDataService, EodHistoricalDataService, GoogleSheetsService, ManualService, @@ -39,6 +42,7 @@ import { DataProviderService } from './data-provider.service'; provide: 'DataProviderInterfaces', useFactory: ( alphaVantageService, + CoinGeckoDataService, eodHistoricalDataService, googleSheetsService, manualService, @@ -46,6 +50,7 @@ import { DataProviderService } from './data-provider.service'; yahooFinanceService ) => [ alphaVantageService, + CoinGeckoDataService, eodHistoricalDataService, googleSheetsService, manualService, diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 937897a1d..dc4c1302d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -205,6 +205,7 @@ enum AssetSubClass { enum DataSource { ALPHA_VANTAGE + COINGECKO EOD_HISTORICAL_DATA GOOGLE_SHEETS MANUAL