2021-12-04 21:05:11 +01:00
|
|
|
import { PROPERTY_CURRENCIES, baseCurrency } from '@ghostfolio/common/config';
|
2021-07-28 16:11:19 +02:00
|
|
|
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
|
2021-11-07 21:25:18 +01:00
|
|
|
import { Injectable, Logger } from '@nestjs/common';
|
2021-04-13 21:53:58 +02:00
|
|
|
import { format } from 'date-fns';
|
2022-02-27 17:03:00 +01:00
|
|
|
import { isNumber, uniq } from 'lodash';
|
2021-04-13 21:53:58 +02:00
|
|
|
|
2021-08-14 16:55:40 +02:00
|
|
|
import { DataProviderService } from './data-provider/data-provider.service';
|
2021-09-18 19:32:22 +02:00
|
|
|
import { IDataGatheringItem } from './interfaces/interfaces';
|
2021-09-24 21:09:48 +02:00
|
|
|
import { PrismaService } from './prisma.service';
|
2021-12-04 21:05:11 +01:00
|
|
|
import { PropertyService } from './property/property.service';
|
2021-04-13 21:53:58 +02:00
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
export class ExchangeRateDataService {
|
2021-09-24 21:09:48 +02:00
|
|
|
private currencies: string[] = [];
|
2021-09-18 19:32:22 +02:00
|
|
|
private currencyPairs: IDataGatheringItem[] = [];
|
2021-08-14 11:06:21 +02:00
|
|
|
private exchangeRates: { [currencyPair: string]: number } = {};
|
2021-04-13 21:53:58 +02:00
|
|
|
|
2021-09-24 21:09:48 +02:00
|
|
|
public constructor(
|
|
|
|
private readonly dataProviderService: DataProviderService,
|
2021-12-04 21:05:11 +01:00
|
|
|
private readonly prismaService: PrismaService,
|
|
|
|
private readonly propertyService: PropertyService
|
2021-09-24 21:09:48 +02:00
|
|
|
) {
|
2021-04-13 21:53:58 +02:00
|
|
|
this.initialize();
|
|
|
|
}
|
|
|
|
|
2021-09-24 21:09:48 +02:00
|
|
|
public getCurrencies() {
|
|
|
|
return this.currencies?.length > 0 ? this.currencies : [baseCurrency];
|
|
|
|
}
|
|
|
|
|
|
|
|
public getCurrencyPairs() {
|
|
|
|
return this.currencyPairs;
|
|
|
|
}
|
|
|
|
|
2021-04-13 21:53:58 +02:00
|
|
|
public async initialize() {
|
2021-09-24 21:09:48 +02:00
|
|
|
this.currencies = await this.prepareCurrencies();
|
2021-08-14 11:06:21 +02:00
|
|
|
this.currencyPairs = [];
|
|
|
|
this.exchangeRates = {};
|
2021-04-18 19:06:54 +02:00
|
|
|
|
2021-09-24 21:09:48 +02:00
|
|
|
for (const {
|
|
|
|
currency1,
|
|
|
|
currency2,
|
|
|
|
dataSource
|
|
|
|
} of this.prepareCurrencyPairs(this.currencies)) {
|
2021-11-29 21:08:58 +01:00
|
|
|
this.currencyPairs.push({
|
|
|
|
dataSource,
|
|
|
|
symbol: `${currency1}${currency2}`
|
|
|
|
});
|
2021-08-14 11:06:21 +02:00
|
|
|
}
|
2021-04-13 21:53:58 +02:00
|
|
|
|
|
|
|
await this.loadCurrencies();
|
|
|
|
}
|
|
|
|
|
|
|
|
public async loadCurrencies() {
|
|
|
|
const result = await this.dataProviderService.getHistorical(
|
2021-08-14 11:06:21 +02:00
|
|
|
this.currencyPairs,
|
2021-04-13 21:53:58 +02:00
|
|
|
'day',
|
|
|
|
getYesterday(),
|
|
|
|
getYesterday()
|
|
|
|
);
|
|
|
|
|
2022-01-16 13:46:00 +01:00
|
|
|
if (Object.keys(result).length !== this.currencyPairs.length) {
|
2021-08-24 21:09:02 +02:00
|
|
|
// Load currencies directly from data provider as a fallback
|
2022-01-16 13:46:00 +01:00
|
|
|
// if historical data is not fully available
|
2022-02-27 17:03:00 +01:00
|
|
|
const historicalData = await this.dataProviderService.getQuotes(
|
2021-09-18 19:32:22 +02:00
|
|
|
this.currencyPairs.map(({ dataSource, symbol }) => {
|
|
|
|
return { dataSource, symbol };
|
2021-08-24 21:09:02 +02:00
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
Object.keys(historicalData).forEach((key) => {
|
|
|
|
result[key] = {
|
|
|
|
[format(getYesterday(), DATE_FORMAT)]: {
|
|
|
|
marketPrice: historicalData[key].marketPrice
|
|
|
|
}
|
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-04-18 19:06:54 +02:00
|
|
|
const resultExtended = result;
|
|
|
|
|
|
|
|
Object.keys(result).forEach((pair) => {
|
|
|
|
const [currency1, currency2] = pair.match(/.{1,3}/g);
|
|
|
|
const [date] = Object.keys(result[pair]);
|
|
|
|
|
|
|
|
// Calculate the opposite direction
|
|
|
|
resultExtended[`${currency2}${currency1}`] = {
|
|
|
|
[date]: {
|
|
|
|
marketPrice: 1 / result[pair][date].marketPrice
|
|
|
|
}
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
2021-11-29 21:08:58 +01:00
|
|
|
Object.keys(resultExtended).forEach((symbol) => {
|
2021-09-18 19:32:22 +02:00
|
|
|
const [currency1, currency2] = symbol.match(/.{1,3}/g);
|
2021-07-28 16:11:19 +02:00
|
|
|
const date = format(getYesterday(), DATE_FORMAT);
|
2021-04-13 21:53:58 +02:00
|
|
|
|
2021-09-18 19:32:22 +02:00
|
|
|
this.exchangeRates[symbol] = resultExtended[symbol]?.[date]?.marketPrice;
|
2021-04-13 21:53:58 +02:00
|
|
|
|
2021-09-18 19:32:22 +02:00
|
|
|
if (!this.exchangeRates[symbol]) {
|
2021-04-18 19:06:54 +02:00
|
|
|
// Not found, calculate indirectly via USD
|
2021-09-18 19:32:22 +02:00
|
|
|
this.exchangeRates[symbol] =
|
2021-09-24 21:09:48 +02:00
|
|
|
resultExtended[`${currency1}${'USD'}`]?.[date]?.marketPrice *
|
|
|
|
resultExtended[`${'USD'}${currency2}`]?.[date]?.marketPrice;
|
2021-04-18 19:06:54 +02:00
|
|
|
|
|
|
|
// Calculate the opposite direction
|
2021-08-14 11:06:21 +02:00
|
|
|
this.exchangeRates[`${currency2}${currency1}`] =
|
2021-09-18 19:32:22 +02:00
|
|
|
1 / this.exchangeRates[symbol];
|
2021-04-13 21:53:58 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
public toCurrency(
|
|
|
|
aValue: number,
|
2021-09-24 21:09:48 +02:00
|
|
|
aFromCurrency: string,
|
|
|
|
aToCurrency: string
|
2021-04-13 21:53:58 +02:00
|
|
|
) {
|
2022-03-01 21:32:19 +01:00
|
|
|
if (aValue === 0) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2021-09-24 21:09:48 +02:00
|
|
|
const hasNaN = Object.values(this.exchangeRates).some((exchangeRate) => {
|
|
|
|
return isNaN(exchangeRate);
|
|
|
|
});
|
|
|
|
|
|
|
|
if (hasNaN) {
|
2021-04-18 19:06:54 +02:00
|
|
|
// Reinitialize if data is not loaded correctly
|
|
|
|
this.initialize();
|
|
|
|
}
|
|
|
|
|
2021-04-13 21:53:58 +02:00
|
|
|
let factor = 1;
|
|
|
|
|
|
|
|
if (aFromCurrency !== aToCurrency) {
|
2021-08-14 11:06:21 +02:00
|
|
|
if (this.exchangeRates[`${aFromCurrency}${aToCurrency}`]) {
|
|
|
|
factor = this.exchangeRates[`${aFromCurrency}${aToCurrency}`];
|
|
|
|
} else {
|
|
|
|
// Calculate indirectly via USD
|
2021-09-24 21:09:48 +02:00
|
|
|
const factor1 = this.exchangeRates[`${aFromCurrency}${'USD'}`];
|
|
|
|
const factor2 = this.exchangeRates[`${'USD'}${aToCurrency}`];
|
2021-08-14 11:06:21 +02:00
|
|
|
|
|
|
|
factor = factor1 * factor2;
|
|
|
|
|
|
|
|
this.exchangeRates[`${aFromCurrency}${aToCurrency}`] = factor;
|
|
|
|
}
|
2021-04-13 21:53:58 +02:00
|
|
|
}
|
|
|
|
|
2021-09-25 16:44:24 +02:00
|
|
|
if (isNumber(factor) && !isNaN(factor)) {
|
2021-08-08 19:24:51 +02:00
|
|
|
return factor * aValue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fallback with error, if currencies are not available
|
2021-11-07 21:25:18 +01:00
|
|
|
Logger.error(
|
2021-08-08 19:24:51 +02:00
|
|
|
`No exchange rate has been found for ${aFromCurrency}${aToCurrency}`
|
|
|
|
);
|
|
|
|
return aValue;
|
2021-04-13 21:53:58 +02:00
|
|
|
}
|
2021-04-18 19:06:54 +02:00
|
|
|
|
2021-09-24 21:09:48 +02:00
|
|
|
private async prepareCurrencies(): Promise<string[]> {
|
2021-12-04 21:05:11 +01:00
|
|
|
let currencies: string[] = [];
|
2021-09-24 21:09:48 +02:00
|
|
|
|
2021-09-25 16:45:21 +02:00
|
|
|
(
|
|
|
|
await this.prismaService.account.findMany({
|
|
|
|
distinct: ['currency'],
|
|
|
|
orderBy: [{ currency: 'asc' }],
|
2021-12-25 17:08:56 +01:00
|
|
|
select: { currency: true },
|
|
|
|
where: {
|
|
|
|
currency: {
|
|
|
|
not: null
|
|
|
|
}
|
|
|
|
}
|
2021-09-25 16:45:21 +02:00
|
|
|
})
|
|
|
|
).forEach((account) => {
|
|
|
|
currencies.push(account.currency);
|
2021-09-24 21:09:48 +02:00
|
|
|
});
|
|
|
|
|
2021-09-25 16:45:21 +02:00
|
|
|
(
|
|
|
|
await this.prismaService.settings.findMany({
|
|
|
|
distinct: ['currency'],
|
|
|
|
orderBy: [{ currency: 'asc' }],
|
2021-12-25 17:08:56 +01:00
|
|
|
select: { currency: true },
|
|
|
|
where: {
|
|
|
|
currency: {
|
|
|
|
not: null
|
|
|
|
}
|
|
|
|
}
|
2021-09-25 16:45:21 +02:00
|
|
|
})
|
|
|
|
).forEach((userSettings) => {
|
|
|
|
currencies.push(userSettings.currency);
|
2021-09-24 21:09:48 +02:00
|
|
|
});
|
|
|
|
|
2021-09-25 16:45:21 +02:00
|
|
|
(
|
|
|
|
await this.prismaService.symbolProfile.findMany({
|
|
|
|
distinct: ['currency'],
|
|
|
|
orderBy: [{ currency: 'asc' }],
|
2021-12-25 17:08:56 +01:00
|
|
|
select: { currency: true },
|
|
|
|
where: {
|
|
|
|
currency: {
|
|
|
|
not: null
|
|
|
|
}
|
|
|
|
}
|
2021-09-25 16:45:21 +02:00
|
|
|
})
|
|
|
|
).forEach((symbolProfile) => {
|
|
|
|
currencies.push(symbolProfile.currency);
|
2021-09-24 21:09:48 +02:00
|
|
|
});
|
|
|
|
|
2021-12-04 21:05:11 +01:00
|
|
|
const customCurrencies = (await this.propertyService.getByKey(
|
|
|
|
PROPERTY_CURRENCIES
|
|
|
|
)) as string[];
|
|
|
|
|
|
|
|
if (customCurrencies?.length > 0) {
|
|
|
|
currencies = currencies.concat(customCurrencies);
|
|
|
|
}
|
|
|
|
|
2022-03-01 21:32:19 +01:00
|
|
|
return uniq(currencies).filter(Boolean).sort();
|
2021-09-24 21:09:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private prepareCurrencyPairs(aCurrencies: string[]) {
|
|
|
|
return aCurrencies
|
|
|
|
.filter((currency) => {
|
|
|
|
return currency !== baseCurrency;
|
|
|
|
})
|
|
|
|
.map((currency) => {
|
|
|
|
return {
|
|
|
|
currency1: baseCurrency,
|
|
|
|
currency2: currency,
|
2021-10-15 22:22:45 +02:00
|
|
|
dataSource: this.dataProviderService.getPrimaryDataSource(),
|
2021-09-24 21:09:48 +02:00
|
|
|
symbol: `${baseCurrency}${currency}`
|
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
2021-04-13 21:53:58 +02:00
|
|
|
}
|