mirror of https://github.com/ghostfolio/ghostfolio
4 changed files with 205 additions and 1 deletions
@ -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<Partial<SymbolProfile>> { |
||||
|
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 }; |
||||
|
} |
||||
|
} |
Loading…
Reference in new issue