ghostfolio/apps/api/src/services/data-provider/data-provider.service.ts
2021-09-06 22:55:08 +02:00

207 lines
6.2 KiB
TypeScript

import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import {
IDataGatheringItem,
IDataProviderHistoricalResponse,
IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import {
DATE_FORMAT,
isGhostfolioScraperApiSymbol,
isRakutenRapidApiSymbol
} from '@ghostfolio/common/helper';
import { Granularity } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common';
import { DataSource, MarketData } from '@prisma/client';
import { format } from 'date-fns';
import { AlphaVantageService } from './alpha-vantage/alpha-vantage.service';
import { GhostfolioScraperApiService } from './ghostfolio-scraper-api/ghostfolio-scraper-api.service';
import { RakutenRapidApiService } from './rakuten-rapid-api/rakuten-rapid-api.service';
import {
convertToYahooFinanceSymbol,
YahooFinanceService
} from './yahoo-finance/yahoo-finance.service';
@Injectable()
export class DataProviderService {
public constructor(
private readonly alphaVantageService: AlphaVantageService,
private readonly configurationService: ConfigurationService,
private readonly ghostfolioScraperApiService: GhostfolioScraperApiService,
private readonly prismaService: PrismaService,
private readonly rakutenRapidApiService: RakutenRapidApiService,
private readonly yahooFinanceService: YahooFinanceService
) {
this.rakutenRapidApiService?.setPrisma(this.prismaService);
}
public async get(
aSymbols: string[]
): Promise<{ [symbol: string]: IDataProviderResponse }> {
if (aSymbols.length === 1) {
const symbol = aSymbols[0];
if (isGhostfolioScraperApiSymbol(symbol)) {
return this.ghostfolioScraperApiService.get(aSymbols);
} else if (isRakutenRapidApiSymbol(symbol)) {
return this.rakutenRapidApiService.get(aSymbols);
}
}
const yahooFinanceSymbols = aSymbols
.filter((symbol) => {
return (
!isGhostfolioScraperApiSymbol(symbol) &&
!isRakutenRapidApiSymbol(symbol)
);
})
.map((symbol) => {
return convertToYahooFinanceSymbol(symbol);
});
const response = await this.yahooFinanceService.get(yahooFinanceSymbols);
const ghostfolioScraperApiSymbols = aSymbols.filter((symbol) => {
return isGhostfolioScraperApiSymbol(symbol);
});
for (const symbol of ghostfolioScraperApiSymbols) {
if (symbol) {
const ghostfolioScraperApiResult =
await this.ghostfolioScraperApiService.get([symbol]);
response[symbol] = ghostfolioScraperApiResult[symbol];
}
}
const rakutenRapidApiSymbols = aSymbols.filter((symbol) => {
return isRakutenRapidApiSymbol(symbol);
});
for (const symbol of rakutenRapidApiSymbols) {
if (symbol) {
const rakutenRapidApiResult =
await this.ghostfolioScraperApiService.get([symbol]);
response[symbol] = rakutenRapidApiResult[symbol];
}
}
return response;
}
public async getHistorical(
aSymbols: string[],
aGranularity: Granularity = 'month',
from: Date,
to: Date
): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
}> {
let response: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
} = {};
const granularityQuery =
aGranularity === 'month'
? `AND (date_part('day', date) = 1 OR date >= TIMESTAMP 'yesterday')`
: '';
const rangeQuery =
from && to
? `AND date >= '${format(from, DATE_FORMAT)}' AND date <= '${format(
to,
DATE_FORMAT
)}'`
: '';
try {
const queryRaw = `SELECT * FROM "MarketData" WHERE "symbol" IN ('${aSymbols.join(
`','`
)}') ${granularityQuery} ${rangeQuery} ORDER BY date;`;
const marketDataByGranularity: MarketData[] =
await this.prismaService.$queryRaw(queryRaw);
response = marketDataByGranularity.reduce((r, marketData) => {
const { date, marketPrice, symbol } = marketData;
r[symbol] = {
...(r[symbol] || {}),
[format(new Date(date), DATE_FORMAT)]: { marketPrice }
};
return r;
}, {});
} catch (error) {
console.error(error);
} finally {
return response;
}
}
public async getHistoricalRaw(
aDataGatheringItems: IDataGatheringItem[],
from: Date,
to: Date
): Promise<{
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
}> {
const result: {
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
} = {};
const promises: Promise<{
data: { [date: string]: IDataProviderHistoricalResponse };
symbol: string;
}>[] = [];
for (const { dataSource, symbol } of aDataGatheringItems) {
const dataProvider = this.getDataProvider(dataSource);
if (dataProvider.canHandle(symbol)) {
promises.push(
dataProvider
.getHistorical([symbol], undefined, from, to)
.then((data) => ({ data: data?.[symbol], symbol }))
);
}
}
const allData = await Promise.all(promises);
for (const { data, symbol } of allData) {
result[symbol] = data;
}
return result;
}
public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
const { items } = await this.getDataProvider(
<DataSource>this.configurationService.get('DATA_SOURCES')[0]
).search(aSymbol);
const filteredItems = items.filter((item) => {
// Only allow symbols with supported currency
return item.currency ? true : false;
});
return {
items: filteredItems
};
}
private getDataProvider(providerName: DataSource) {
switch (providerName) {
case DataSource.ALPHA_VANTAGE:
return this.alphaVantageService;
case DataSource.GHOSTFOLIO:
return this.ghostfolioScraperApiService;
case DataSource.RAKUTEN:
return this.rakutenRapidApiService;
case DataSource.YAHOO:
return this.yahooFinanceService;
default:
throw new Error('No data provider has been found.');
}
}
}