|
@ -15,88 +15,58 @@ import { |
|
|
SymbolProfile |
|
|
SymbolProfile |
|
|
} from '@prisma/client'; |
|
|
} from '@prisma/client'; |
|
|
import bent from 'bent'; |
|
|
import bent from 'bent'; |
|
|
import { format, differenceInDays, addDays, subDays } from 'date-fns'; |
|
|
import { format, fromUnixTime, getUnixTime } from 'date-fns'; |
|
|
|
|
|
|
|
|
@Injectable() |
|
|
@Injectable() |
|
|
export class CoinGeckoService implements DataProviderInterface { |
|
|
export class CoinGeckoService implements DataProviderInterface { |
|
|
private readonly URL = 'https://api.coingecko.com/api/v3'; |
|
|
|
|
|
private baseCurrency: string; |
|
|
private baseCurrency: string; |
|
|
private DB = {}; |
|
|
private readonly URL = 'https://api.coingecko.com/api/v3'; |
|
|
|
|
|
|
|
|
public constructor( |
|
|
public constructor( |
|
|
private readonly configurationService: ConfigurationService |
|
|
private readonly configurationService: ConfigurationService |
|
|
) { |
|
|
) { |
|
|
this.baseCurrency = this.configurationService |
|
|
this.baseCurrency = this.configurationService.get('BASE_CURRENCY'); |
|
|
.get('BASE_CURRENCY') |
|
|
|
|
|
.toUpperCase(); |
|
|
|
|
|
this.DB = {}; |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
public canHandle(symbol: string) { |
|
|
public canHandle(symbol: string) { |
|
|
return true; |
|
|
return true; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
public async getDividends({ |
|
|
|
|
|
from, |
|
|
|
|
|
granularity = 'day', |
|
|
|
|
|
symbol, |
|
|
|
|
|
to |
|
|
|
|
|
}: { |
|
|
|
|
|
from: Date; |
|
|
|
|
|
granularity: Granularity; |
|
|
|
|
|
symbol: string; |
|
|
|
|
|
to: Date; |
|
|
|
|
|
}) { |
|
|
|
|
|
return {}; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public async getAssetProfile( |
|
|
public async getAssetProfile( |
|
|
aSymbol: string |
|
|
aSymbol: string |
|
|
): Promise<Partial<SymbolProfile>> { |
|
|
): Promise<Partial<SymbolProfile>> { |
|
|
return { |
|
|
const response: Partial<SymbolProfile> = { |
|
|
assetClass: AssetClass.CASH, |
|
|
assetClass: AssetClass.CASH, |
|
|
assetSubClass: AssetSubClass.CRYPTOCURRENCY, |
|
|
assetSubClass: AssetSubClass.CRYPTOCURRENCY, |
|
|
currency: this.baseCurrency.toUpperCase(), |
|
|
currency: this.baseCurrency, |
|
|
dataSource: this.getName(), |
|
|
dataSource: this.getName(), |
|
|
name: aSymbol, |
|
|
|
|
|
symbol: aSymbol |
|
|
symbol: aSymbol |
|
|
}; |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private async populateDatabase(datefrom: Date, symbol: string) { |
|
|
try { |
|
|
let start_day; |
|
|
const get = bent(`${this.URL}/coins/${aSymbol}`, 'GET', 'json', 200); |
|
|
let end_day; |
|
|
const { name } = await get(); |
|
|
datefrom.setHours(0, 0, 1); |
|
|
|
|
|
start_day = Math.round(datefrom.getTime() / 1000); |
|
|
response.name = name; |
|
|
end_day = Math.round(new Date().getTime() / 1000); |
|
|
} catch (error) { |
|
|
const targeturl = `${ |
|
|
Logger.error(error, 'CoinGeckoService'); |
|
|
this.URL |
|
|
|
|
|
}/coins/${symbol.toLowerCase()}/market_chart/range?vs_currency=${this.baseCurrency.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]; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return response; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
private async getDayStat(datein: Date, symbol: string) { |
|
|
public async getDividends({ |
|
|
let out = { marketPrice: 0 }; |
|
|
from, |
|
|
let prevday = subDays(datein, 1); |
|
|
granularity = 'day', |
|
|
datein.setHours(0, 0, 1, 1); |
|
|
symbol, |
|
|
let start_day = Math.round(datein.getTime() / 1000); |
|
|
to |
|
|
let prev_day = Math.round(prevday.getTime() / 1000); |
|
|
}: { |
|
|
out['marketPrice'] = this.DB[start_day]; |
|
|
from: Date; |
|
|
if (prev_day in this.DB) { |
|
|
granularity: Granularity; |
|
|
out['performance'] = this.DB[start_day] / this.DB[prev_day]; |
|
|
symbol: string; |
|
|
} else { |
|
|
to: Date; |
|
|
out['performance'] = 0; |
|
|
}) { |
|
|
} |
|
|
return {}; |
|
|
return out; |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
public async getHistorical( |
|
|
public async getHistorical( |
|
@ -107,22 +77,44 @@ export class CoinGeckoService implements DataProviderInterface { |
|
|
): Promise<{ |
|
|
): Promise<{ |
|
|
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; |
|
|
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; |
|
|
}> { |
|
|
}> { |
|
|
let out = {}; |
|
|
try { |
|
|
out[aSymbol] = {}; |
|
|
const get = bent( |
|
|
const totalDays = Math.abs(differenceInDays(from, to)) + 1; |
|
|
`${ |
|
|
await this.populateDatabase(from, aSymbol); |
|
|
this.URL |
|
|
for (const iter of Array(totalDays).keys()) { |
|
|
}/coins/${aSymbol}/market_chart/range?vs_currency=${this.baseCurrency.toLowerCase()}&from=${getUnixTime( |
|
|
let day = addDays(from, iter); |
|
|
from |
|
|
let datestr = format(day, DATE_FORMAT); |
|
|
)}&to=${getUnixTime(to)}`,
|
|
|
out[aSymbol][datestr] = await this.getDayStat(day, aSymbol); |
|
|
'GET', |
|
|
} |
|
|
'json', |
|
|
|
|
|
200 |
|
|
|
|
|
); |
|
|
|
|
|
const { prices } = await get(); |
|
|
|
|
|
|
|
|
|
|
|
const result: { |
|
|
|
|
|
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; |
|
|
|
|
|
} = { |
|
|
|
|
|
[aSymbol]: {} |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
return out; |
|
|
for (const [timestamp, marketPrice] of prices) { |
|
|
|
|
|
result[aSymbol][format(fromUnixTime(timestamp / 1000), DATE_FORMAT)] = { |
|
|
|
|
|
marketPrice |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return result; |
|
|
|
|
|
} catch (error) { |
|
|
|
|
|
throw new Error( |
|
|
|
|
|
`Could not get historical market data for ${aSymbol} (${this.getName()}) from ${format( |
|
|
|
|
|
from, |
|
|
|
|
|
DATE_FORMAT |
|
|
|
|
|
)} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}` |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
public getMaxNumberOfSymbolsPerRequest() { |
|
|
public getMaxNumberOfSymbolsPerRequest() { |
|
|
// Safe Rate Limit: https://www.coingecko.com/en/api/pricing#general
|
|
|
return 50; |
|
|
return 20; |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
public getName(): DataSource { |
|
|
public getName(): DataSource { |
|
@ -132,56 +124,68 @@ export class CoinGeckoService implements DataProviderInterface { |
|
|
public async getQuotes( |
|
|
public async getQuotes( |
|
|
aSymbols: string[] |
|
|
aSymbols: string[] |
|
|
): Promise<{ [symbol: string]: IDataProviderResponse }> { |
|
|
): Promise<{ [symbol: string]: IDataProviderResponse }> { |
|
|
var results = {}; |
|
|
const results: { [symbol: string]: IDataProviderResponse } = {}; |
|
|
|
|
|
|
|
|
if (aSymbols.length <= 0) { |
|
|
if (aSymbols.length <= 0) { |
|
|
return {}; |
|
|
return {}; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
try { |
|
|
try { |
|
|
for (const coin of aSymbols) { |
|
|
const get = bent( |
|
|
const req = bent( |
|
|
`${this.URL}/simple/price?ids=${aSymbols.join( |
|
|
`${ |
|
|
',' |
|
|
this.URL |
|
|
)}&vs_currencies=${this.baseCurrency.toLowerCase()}`,
|
|
|
}/simple/price?ids=${coin.toLowerCase()}&vs_currencies=${this.baseCurrency.toLowerCase()}`,
|
|
|
'GET', |
|
|
'GET', |
|
|
'json', |
|
|
'json', |
|
|
200 |
|
|
200 |
|
|
); |
|
|
); |
|
|
const response = await get(); |
|
|
const response = await req(); |
|
|
|
|
|
const price = |
|
|
|
|
|
response[coin.toLowerCase()][this.baseCurrency.toLowerCase()]; |
|
|
|
|
|
|
|
|
|
|
|
results[coin] = { |
|
|
|
|
|
currency: this.baseCurrency, |
|
|
|
|
|
dataSource: DataSource.COINGECKO, |
|
|
|
|
|
marketPrice: price, |
|
|
|
|
|
marketState: 'closed' |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return results; |
|
|
for (const symbol in response) { |
|
|
|
|
|
if (Object.prototype.hasOwnProperty.call(response, symbol)) { |
|
|
|
|
|
results[symbol] = { |
|
|
|
|
|
currency: this.baseCurrency, |
|
|
|
|
|
dataSource: DataSource.COINGECKO, |
|
|
|
|
|
marketPrice: response[symbol][this.baseCurrency.toLowerCase()], |
|
|
|
|
|
marketState: 'open' |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} catch (error) { |
|
|
} catch (error) { |
|
|
Logger.error(error, 'CoinGecko'); |
|
|
Logger.error(error, 'CoinGeckoService'); |
|
|
return {}; |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return results; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
public async search(aQuery: string): Promise<{ items: LookupItem[] }> { |
|
|
public async search(aQuery: string): Promise<{ items: LookupItem[] }> { |
|
|
const items: LookupItem[] = []; |
|
|
let items: LookupItem[] = []; |
|
|
|
|
|
|
|
|
if (aQuery.length <= 2) { |
|
|
if (aQuery.length <= 2) { |
|
|
return { items }; |
|
|
return { items }; |
|
|
} |
|
|
} |
|
|
const req = bent(`${this.URL}/search?query=${aQuery}`, 'GET', 'json', 200); |
|
|
|
|
|
const response = await req(); |
|
|
try { |
|
|
for (const coiniter of response.coins) { |
|
|
const get = bent( |
|
|
if (coiniter.id.toLowerCase().includes(aQuery)) { |
|
|
`${this.URL}/search?query=${aQuery}`, |
|
|
items.push({ |
|
|
'GET', |
|
|
symbol: coiniter.id.toUpperCase(), |
|
|
'json', |
|
|
|
|
|
200 |
|
|
|
|
|
); |
|
|
|
|
|
const { coins } = await get(); |
|
|
|
|
|
|
|
|
|
|
|
items = coins.map(({ id: symbol, name }) => { |
|
|
|
|
|
return { |
|
|
|
|
|
name, |
|
|
|
|
|
symbol, |
|
|
currency: this.baseCurrency, |
|
|
currency: this.baseCurrency, |
|
|
dataSource: this.getName(), |
|
|
dataSource: this.getName() |
|
|
name: `${coiniter.name} (From CoinGecko)` |
|
|
}; |
|
|
}); |
|
|
}); |
|
|
} |
|
|
} catch (error) { |
|
|
|
|
|
Logger.error(error, 'CoinGeckoService'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
return { items }; |
|
|
return { items }; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|