2021-09-11 11:14:55 +02:00
|
|
|
import {
|
|
|
|
benchmarks,
|
|
|
|
ghostfolioFearAndGreedIndexSymbol
|
|
|
|
} from '@ghostfolio/common/config';
|
2021-09-18 19:32:22 +02:00
|
|
|
import { DATE_FORMAT, getUtc, resetHours } from '@ghostfolio/common/helper';
|
2021-04-13 21:53:58 +02:00
|
|
|
import { Injectable } from '@nestjs/common';
|
2021-05-27 20:50:10 +02:00
|
|
|
import { DataSource } from '@prisma/client';
|
2021-04-13 21:53:58 +02:00
|
|
|
import {
|
|
|
|
differenceInHours,
|
|
|
|
format,
|
|
|
|
getDate,
|
|
|
|
getMonth,
|
|
|
|
getYear,
|
|
|
|
isBefore,
|
|
|
|
subDays
|
|
|
|
} from 'date-fns';
|
|
|
|
|
2021-04-18 19:06:54 +02:00
|
|
|
import { ConfigurationService } from './configuration.service';
|
2021-08-14 16:55:40 +02:00
|
|
|
import { DataProviderService } from './data-provider/data-provider.service';
|
2021-04-27 21:03:22 +02:00
|
|
|
import { GhostfolioScraperApiService } from './data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
2021-09-24 21:09:48 +02:00
|
|
|
import { ExchangeRateDataService } from './exchange-rate-data.service';
|
2021-05-27 20:50:10 +02:00
|
|
|
import { IDataGatheringItem } from './interfaces/interfaces';
|
2021-04-13 21:53:58 +02:00
|
|
|
import { PrismaService } from './prisma.service';
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
export class DataGatheringService {
|
|
|
|
public constructor(
|
2021-04-18 19:06:54 +02:00
|
|
|
private readonly configurationService: ConfigurationService,
|
|
|
|
private readonly dataProviderService: DataProviderService,
|
2021-09-24 21:09:48 +02:00
|
|
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
2021-04-27 21:03:22 +02:00
|
|
|
private readonly ghostfolioScraperApi: GhostfolioScraperApiService,
|
2021-08-07 22:38:07 +02:00
|
|
|
private readonly prismaService: PrismaService
|
2021-04-13 21:53:58 +02:00
|
|
|
) {}
|
|
|
|
|
|
|
|
public async gather7Days() {
|
|
|
|
const isDataGatheringNeeded = await this.isDataGatheringNeeded();
|
|
|
|
|
|
|
|
if (isDataGatheringNeeded) {
|
|
|
|
console.log('7d data gathering has been started.');
|
2021-08-22 22:19:10 +02:00
|
|
|
console.time('data-gathering-7d');
|
2021-04-13 21:53:58 +02:00
|
|
|
|
2021-08-07 22:38:07 +02:00
|
|
|
await this.prismaService.property.create({
|
2021-04-13 21:53:58 +02:00
|
|
|
data: {
|
|
|
|
key: 'LOCKED_DATA_GATHERING',
|
|
|
|
value: new Date().toISOString()
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
const symbols = await this.getSymbols7D();
|
|
|
|
|
|
|
|
try {
|
|
|
|
await this.gatherSymbols(symbols);
|
|
|
|
|
2021-08-07 22:38:07 +02:00
|
|
|
await this.prismaService.property.upsert({
|
2021-04-13 21:53:58 +02:00
|
|
|
create: {
|
|
|
|
key: 'LAST_DATA_GATHERING',
|
|
|
|
value: new Date().toISOString()
|
|
|
|
},
|
|
|
|
update: { value: new Date().toISOString() },
|
|
|
|
where: { key: 'LAST_DATA_GATHERING' }
|
|
|
|
});
|
|
|
|
} catch (error) {
|
|
|
|
console.error(error);
|
|
|
|
}
|
|
|
|
|
2021-08-07 22:38:07 +02:00
|
|
|
await this.prismaService.property.delete({
|
2021-04-13 21:53:58 +02:00
|
|
|
where: {
|
|
|
|
key: 'LOCKED_DATA_GATHERING'
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
console.log('7d data gathering has been completed.');
|
2021-08-22 22:19:10 +02:00
|
|
|
console.timeEnd('data-gathering-7d');
|
2021-04-13 21:53:58 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public async gatherMax() {
|
2021-08-07 22:38:07 +02:00
|
|
|
const isDataGatheringLocked = await this.prismaService.property.findUnique({
|
2021-04-18 19:06:54 +02:00
|
|
|
where: { key: 'LOCKED_DATA_GATHERING' }
|
|
|
|
});
|
2021-04-13 21:53:58 +02:00
|
|
|
|
2021-04-18 19:06:54 +02:00
|
|
|
if (!isDataGatheringLocked) {
|
2021-04-13 21:53:58 +02:00
|
|
|
console.log('Max data gathering has been started.');
|
2021-08-22 22:19:10 +02:00
|
|
|
console.time('data-gathering-max');
|
2021-04-13 21:53:58 +02:00
|
|
|
|
2021-08-07 22:38:07 +02:00
|
|
|
await this.prismaService.property.create({
|
2021-04-13 21:53:58 +02:00
|
|
|
data: {
|
|
|
|
key: 'LOCKED_DATA_GATHERING',
|
|
|
|
value: new Date().toISOString()
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
const symbols = await this.getSymbolsMax();
|
|
|
|
|
|
|
|
try {
|
|
|
|
await this.gatherSymbols(symbols);
|
|
|
|
|
2021-08-07 22:38:07 +02:00
|
|
|
await this.prismaService.property.upsert({
|
2021-04-13 21:53:58 +02:00
|
|
|
create: {
|
|
|
|
key: 'LAST_DATA_GATHERING',
|
|
|
|
value: new Date().toISOString()
|
|
|
|
},
|
|
|
|
update: { value: new Date().toISOString() },
|
|
|
|
where: { key: 'LAST_DATA_GATHERING' }
|
|
|
|
});
|
|
|
|
} catch (error) {
|
|
|
|
console.error(error);
|
|
|
|
}
|
|
|
|
|
2021-08-07 22:38:07 +02:00
|
|
|
await this.prismaService.property.delete({
|
2021-04-13 21:53:58 +02:00
|
|
|
where: {
|
|
|
|
key: 'LOCKED_DATA_GATHERING'
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
console.log('Max data gathering has been completed.');
|
2021-08-22 22:19:10 +02:00
|
|
|
console.timeEnd('data-gathering-max');
|
2021-04-13 21:53:58 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-18 19:32:22 +02:00
|
|
|
public async gatherProfileData(aDataGatheringItems?: IDataGatheringItem[]) {
|
2021-07-24 21:13:48 +02:00
|
|
|
console.log('Profile data gathering has been started.');
|
2021-08-22 22:19:10 +02:00
|
|
|
console.time('data-gathering-profile');
|
2021-07-24 21:13:48 +02:00
|
|
|
|
2021-09-18 19:32:22 +02:00
|
|
|
let dataGatheringItems = aDataGatheringItems;
|
2021-07-24 21:13:48 +02:00
|
|
|
|
2021-09-18 19:32:22 +02:00
|
|
|
if (!dataGatheringItems) {
|
|
|
|
dataGatheringItems = await this.getSymbolsProfileData();
|
2021-07-24 21:13:48 +02:00
|
|
|
}
|
|
|
|
|
2021-09-18 19:32:22 +02:00
|
|
|
const currentData = await this.dataProviderService.get(dataGatheringItems);
|
2021-07-24 21:13:48 +02:00
|
|
|
|
2021-08-08 19:27:58 +02:00
|
|
|
for (const [
|
|
|
|
symbol,
|
2021-08-24 20:24:18 +02:00
|
|
|
{ assetClass, assetSubClass, countries, currency, dataSource, name }
|
2021-08-08 19:27:58 +02:00
|
|
|
] of Object.entries(currentData)) {
|
2021-07-24 21:13:48 +02:00
|
|
|
try {
|
2021-08-07 22:38:07 +02:00
|
|
|
await this.prismaService.symbolProfile.upsert({
|
2021-07-24 21:13:48 +02:00
|
|
|
create: {
|
2021-08-08 19:27:58 +02:00
|
|
|
assetClass,
|
2021-08-22 22:19:10 +02:00
|
|
|
assetSubClass,
|
2021-08-24 20:24:18 +02:00
|
|
|
countries,
|
2021-07-24 21:13:48 +02:00
|
|
|
currency,
|
|
|
|
dataSource,
|
|
|
|
name,
|
|
|
|
symbol
|
|
|
|
},
|
|
|
|
update: {
|
2021-08-08 19:27:58 +02:00
|
|
|
assetClass,
|
2021-08-22 22:19:10 +02:00
|
|
|
assetSubClass,
|
2021-08-24 20:24:18 +02:00
|
|
|
countries,
|
2021-07-24 21:13:48 +02:00
|
|
|
currency,
|
|
|
|
name
|
|
|
|
},
|
|
|
|
where: {
|
|
|
|
dataSource_symbol: {
|
|
|
|
dataSource,
|
|
|
|
symbol
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} catch (error) {
|
|
|
|
console.error(`${symbol}: ${error?.meta?.cause}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log('Profile data gathering has been completed.');
|
2021-08-22 22:19:10 +02:00
|
|
|
console.timeEnd('data-gathering-profile');
|
2021-07-24 21:13:48 +02:00
|
|
|
}
|
|
|
|
|
2021-05-27 20:50:10 +02:00
|
|
|
public async gatherSymbols(aSymbolsWithStartDate: IDataGatheringItem[]) {
|
2021-04-13 21:53:58 +02:00
|
|
|
let hasError = false;
|
|
|
|
|
2021-05-27 20:50:10 +02:00
|
|
|
for (const { dataSource, date, symbol } of aSymbolsWithStartDate) {
|
2021-04-13 21:53:58 +02:00
|
|
|
try {
|
|
|
|
const historicalData = await this.dataProviderService.getHistoricalRaw(
|
2021-05-27 20:50:10 +02:00
|
|
|
[{ dataSource, symbol }],
|
2021-04-13 21:53:58 +02:00
|
|
|
date,
|
|
|
|
new Date()
|
|
|
|
);
|
|
|
|
|
|
|
|
let currentDate = date;
|
|
|
|
let lastMarketPrice: number;
|
|
|
|
|
|
|
|
while (
|
|
|
|
isBefore(
|
|
|
|
currentDate,
|
|
|
|
new Date(
|
|
|
|
Date.UTC(
|
|
|
|
getYear(new Date()),
|
|
|
|
getMonth(new Date()),
|
|
|
|
getDate(new Date()),
|
|
|
|
0
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
if (
|
2021-07-28 16:11:19 +02:00
|
|
|
historicalData[symbol]?.[format(currentDate, DATE_FORMAT)]
|
2021-04-13 21:53:58 +02:00
|
|
|
?.marketPrice
|
|
|
|
) {
|
|
|
|
lastMarketPrice =
|
2021-07-28 16:11:19 +02:00
|
|
|
historicalData[symbol]?.[format(currentDate, DATE_FORMAT)]
|
2021-04-13 21:53:58 +02:00
|
|
|
?.marketPrice;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
2021-08-07 22:38:07 +02:00
|
|
|
await this.prismaService.marketData.create({
|
2021-04-13 21:53:58 +02:00
|
|
|
data: {
|
2021-09-18 19:32:22 +02:00
|
|
|
dataSource,
|
2021-04-13 21:53:58 +02:00
|
|
|
symbol,
|
|
|
|
date: currentDate,
|
|
|
|
marketPrice: lastMarketPrice
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} catch {}
|
|
|
|
|
|
|
|
// Count month one up for iteration
|
|
|
|
currentDate = new Date(
|
|
|
|
Date.UTC(
|
|
|
|
getYear(currentDate),
|
|
|
|
getMonth(currentDate),
|
|
|
|
getDate(currentDate) + 1,
|
|
|
|
0
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
hasError = true;
|
|
|
|
console.error(error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-24 21:09:48 +02:00
|
|
|
await this.exchangeRateDataService.initialize();
|
|
|
|
|
2021-04-13 21:53:58 +02:00
|
|
|
if (hasError) {
|
|
|
|
throw '';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-27 20:50:10 +02:00
|
|
|
public async getCustomSymbolsToGather(
|
|
|
|
startDate?: Date
|
|
|
|
): Promise<IDataGatheringItem[]> {
|
2021-07-03 11:32:03 +02:00
|
|
|
const scraperConfigurations =
|
|
|
|
await this.ghostfolioScraperApi.getScraperConfigurations();
|
2021-04-27 21:03:22 +02:00
|
|
|
|
|
|
|
return scraperConfigurations.map((scraperConfiguration) => {
|
|
|
|
return {
|
2021-05-27 20:50:10 +02:00
|
|
|
dataSource: DataSource.GHOSTFOLIO,
|
2021-04-27 21:03:22 +02:00
|
|
|
date: startDate,
|
|
|
|
symbol: scraperConfiguration.symbol
|
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-08-09 21:11:35 +02:00
|
|
|
public async getIsInProgress() {
|
|
|
|
return await this.prismaService.property.findUnique({
|
|
|
|
where: { key: 'LOCKED_DATA_GATHERING' }
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
public async getLastDataGathering() {
|
|
|
|
const lastDataGathering = await this.prismaService.property.findUnique({
|
|
|
|
where: { key: 'LAST_DATA_GATHERING' }
|
|
|
|
});
|
|
|
|
|
|
|
|
if (lastDataGathering?.value) {
|
|
|
|
return new Date(lastDataGathering.value);
|
|
|
|
}
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
public async reset() {
|
|
|
|
console.log('Data gathering has been reset.');
|
|
|
|
|
|
|
|
await this.prismaService.property.deleteMany({
|
|
|
|
where: {
|
|
|
|
OR: [{ key: 'LAST_DATA_GATHERING' }, { key: 'LOCKED_DATA_GATHERING' }]
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-05-27 20:50:10 +02:00
|
|
|
private getBenchmarksToGather(startDate: Date): IDataGatheringItem[] {
|
|
|
|
const benchmarksToGather = benchmarks.map(({ dataSource, symbol }) => {
|
2021-04-18 19:06:54 +02:00
|
|
|
return {
|
2021-05-27 20:50:10 +02:00
|
|
|
dataSource,
|
2021-04-18 19:06:54 +02:00
|
|
|
symbol,
|
|
|
|
date: startDate
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
if (this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) {
|
|
|
|
benchmarksToGather.push({
|
2021-05-27 20:50:10 +02:00
|
|
|
dataSource: DataSource.RAKUTEN,
|
2021-04-18 19:06:54 +02:00
|
|
|
date: startDate,
|
2021-09-11 11:14:55 +02:00
|
|
|
symbol: ghostfolioFearAndGreedIndexSymbol
|
2021-04-18 19:06:54 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return benchmarksToGather;
|
|
|
|
}
|
|
|
|
|
2021-05-27 20:50:10 +02:00
|
|
|
private async getSymbols7D(): Promise<IDataGatheringItem[]> {
|
2021-04-13 21:53:58 +02:00
|
|
|
const startDate = subDays(resetHours(new Date()), 7);
|
|
|
|
|
2021-09-13 21:26:23 +02:00
|
|
|
const symbolProfilesToGather = (
|
|
|
|
await this.prismaService.symbolProfile.findMany({
|
|
|
|
orderBy: [{ symbol: 'asc' }],
|
|
|
|
select: {
|
|
|
|
dataSource: true,
|
|
|
|
symbol: true
|
2021-07-03 11:32:03 +02:00
|
|
|
}
|
2021-04-20 21:52:01 +02:00
|
|
|
})
|
2021-09-13 21:26:23 +02:00
|
|
|
).map((symbolProfile) => {
|
|
|
|
return {
|
|
|
|
...symbolProfile,
|
|
|
|
date: startDate
|
|
|
|
};
|
|
|
|
});
|
2021-04-13 21:53:58 +02:00
|
|
|
|
2021-09-24 21:09:48 +02:00
|
|
|
const currencyPairsToGather = this.exchangeRateDataService
|
|
|
|
.getCurrencyPairs()
|
|
|
|
.map(({ dataSource, symbol }) => {
|
2021-05-27 20:50:10 +02:00
|
|
|
return {
|
|
|
|
dataSource,
|
|
|
|
symbol,
|
|
|
|
date: startDate
|
|
|
|
};
|
2021-09-24 21:09:48 +02:00
|
|
|
});
|
2021-04-13 21:53:58 +02:00
|
|
|
|
2021-08-09 21:33:58 +02:00
|
|
|
const customSymbolsToGather =
|
|
|
|
await this.ghostfolioScraperApi.getCustomSymbolsToGather(startDate);
|
2021-04-19 22:25:52 +02:00
|
|
|
|
2021-04-13 21:53:58 +02:00
|
|
|
return [
|
2021-04-18 19:06:54 +02:00
|
|
|
...this.getBenchmarksToGather(startDate),
|
2021-04-19 22:25:52 +02:00
|
|
|
...customSymbolsToGather,
|
2021-04-13 21:53:58 +02:00
|
|
|
...currencyPairsToGather,
|
2021-09-13 21:26:23 +02:00
|
|
|
...symbolProfilesToGather
|
2021-04-13 21:53:58 +02:00
|
|
|
];
|
|
|
|
}
|
|
|
|
|
2021-05-27 20:50:10 +02:00
|
|
|
private async getSymbolsMax(): Promise<IDataGatheringItem[]> {
|
2021-04-21 18:58:21 +02:00
|
|
|
const startDate = new Date(getUtc('2015-01-01'));
|
2021-04-13 21:53:58 +02:00
|
|
|
|
2021-08-09 21:33:58 +02:00
|
|
|
const customSymbolsToGather =
|
|
|
|
await this.ghostfolioScraperApi.getCustomSymbolsToGather(startDate);
|
2021-04-13 21:53:58 +02:00
|
|
|
|
2021-09-24 21:09:48 +02:00
|
|
|
const currencyPairsToGather = this.exchangeRateDataService
|
|
|
|
.getCurrencyPairs()
|
|
|
|
.map(({ dataSource, symbol }) => {
|
2021-05-27 20:50:10 +02:00
|
|
|
return {
|
|
|
|
dataSource,
|
|
|
|
symbol,
|
|
|
|
date: startDate
|
|
|
|
};
|
2021-09-24 21:09:48 +02:00
|
|
|
});
|
2021-04-13 21:53:58 +02:00
|
|
|
|
2021-09-13 21:26:23 +02:00
|
|
|
const symbolProfilesToGather =
|
|
|
|
await this.prismaService.symbolProfile.findMany({
|
|
|
|
orderBy: [{ symbol: 'asc' }],
|
|
|
|
select: {
|
|
|
|
dataSource: true,
|
|
|
|
symbol: true
|
2021-07-03 11:32:03 +02:00
|
|
|
}
|
2021-09-13 21:26:23 +02:00
|
|
|
});
|
2021-04-19 22:25:52 +02:00
|
|
|
|
2021-04-18 19:06:54 +02:00
|
|
|
return [
|
|
|
|
...this.getBenchmarksToGather(startDate),
|
2021-04-19 22:25:52 +02:00
|
|
|
...customSymbolsToGather,
|
2021-04-18 19:06:54 +02:00
|
|
|
...currencyPairsToGather,
|
2021-09-13 21:26:23 +02:00
|
|
|
...symbolProfilesToGather
|
2021-04-18 19:06:54 +02:00
|
|
|
];
|
2021-04-13 21:53:58 +02:00
|
|
|
}
|
|
|
|
|
2021-07-24 21:13:48 +02:00
|
|
|
private async getSymbolsProfileData(): Promise<IDataGatheringItem[]> {
|
|
|
|
const startDate = subDays(resetHours(new Date()), 7);
|
|
|
|
|
2021-08-07 22:38:07 +02:00
|
|
|
const distinctOrders = await this.prismaService.order.findMany({
|
2021-07-24 21:13:48 +02:00
|
|
|
distinct: ['symbol'],
|
|
|
|
orderBy: [{ symbol: 'asc' }],
|
|
|
|
select: { dataSource: true, symbol: true }
|
|
|
|
});
|
|
|
|
|
|
|
|
return [...this.getBenchmarksToGather(startDate), ...distinctOrders].filter(
|
|
|
|
(distinctOrder) => {
|
|
|
|
return (
|
|
|
|
distinctOrder.dataSource !== DataSource.GHOSTFOLIO &&
|
|
|
|
distinctOrder.dataSource !== DataSource.RAKUTEN
|
|
|
|
);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-04-13 21:53:58 +02:00
|
|
|
private async isDataGatheringNeeded() {
|
2021-08-09 21:11:35 +02:00
|
|
|
const lastDataGathering = await this.getLastDataGathering();
|
2021-04-13 21:53:58 +02:00
|
|
|
|
2021-08-07 22:38:07 +02:00
|
|
|
const isDataGatheringLocked = await this.prismaService.property.findUnique({
|
2021-04-13 21:53:58 +02:00
|
|
|
where: { key: 'LOCKED_DATA_GATHERING' }
|
|
|
|
});
|
|
|
|
|
2021-08-09 21:11:35 +02:00
|
|
|
const diffInHours = differenceInHours(new Date(), lastDataGathering);
|
2021-04-13 21:53:58 +02:00
|
|
|
|
|
|
|
return (diffInHours >= 1 || !lastDataGathering) && !isDataGatheringLocked;
|
|
|
|
}
|
|
|
|
}
|