|
|
@ -16,6 +16,7 @@ import { |
|
|
} from '@ghostfolio/common/helper'; |
|
|
} from '@ghostfolio/common/helper'; |
|
|
|
|
|
|
|
|
import { Injectable, Logger } from '@nestjs/common'; |
|
|
import { Injectable, Logger } from '@nestjs/common'; |
|
|
|
|
|
import { OnEvent } from '@nestjs/event-emitter'; |
|
|
import { |
|
|
import { |
|
|
eachDayOfInterval, |
|
|
eachDayOfInterval, |
|
|
format, |
|
|
format, |
|
|
@ -26,6 +27,7 @@ import { |
|
|
import { isNumber } from 'lodash'; |
|
|
import { isNumber } from 'lodash'; |
|
|
import ms from 'ms'; |
|
|
import ms from 'ms'; |
|
|
|
|
|
|
|
|
|
|
|
import { MARKET_DATA_UPDATED } from '../../events/market-data-updated.event'; |
|
|
import { ExchangeRatesByCurrency } from './interfaces/exchange-rate-data.interface'; |
|
|
import { ExchangeRatesByCurrency } from './interfaces/exchange-rate-data.interface'; |
|
|
|
|
|
|
|
|
@Injectable() |
|
|
@Injectable() |
|
|
@ -34,6 +36,8 @@ export class ExchangeRateDataService { |
|
|
private currencyPairs: DataGatheringItem[] = []; |
|
|
private currencyPairs: DataGatheringItem[] = []; |
|
|
private derivedCurrencyFactors: { [currencyPair: string]: number } = {}; |
|
|
private derivedCurrencyFactors: { [currencyPair: string]: number } = {}; |
|
|
private exchangeRates: { [currencyPair: string]: number } = {}; |
|
|
private exchangeRates: { [currencyPair: string]: number } = {}; |
|
|
|
|
|
private exchangeRateCache = new Map<string, Float64Array>(); |
|
|
|
|
|
private pendingLoads = new Map<string, Promise<Float64Array>>(); |
|
|
|
|
|
|
|
|
public constructor( |
|
|
public constructor( |
|
|
private readonly dataProviderService: DataProviderService, |
|
|
private readonly dataProviderService: DataProviderService, |
|
|
@ -284,56 +288,56 @@ export class ExchangeRateDataService { |
|
|
} else if (derivedCurrencyFactor) { |
|
|
} else if (derivedCurrencyFactor) { |
|
|
factor = derivedCurrencyFactor; |
|
|
factor = derivedCurrencyFactor; |
|
|
} else { |
|
|
} else { |
|
|
const dataSource = |
|
|
const marketPrice = await this.getRateFromCache( |
|
|
this.dataProviderService.getDataSourceForExchangeRates(); |
|
|
`${aFromCurrency}${aToCurrency}`, |
|
|
const symbol = `${aFromCurrency}${aToCurrency}`; |
|
|
aDate |
|
|
|
|
|
); |
|
|
const marketData = await this.marketDataService.get({ |
|
|
|
|
|
dataSource, |
|
|
|
|
|
symbol, |
|
|
|
|
|
date: aDate |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
if (marketData?.marketPrice) { |
|
|
if (marketPrice !== undefined) { |
|
|
factor = marketData?.marketPrice; |
|
|
factor = marketPrice; |
|
|
} else { |
|
|
} else { |
|
|
// Calculate indirectly via base currency
|
|
|
|
|
|
|
|
|
|
|
|
let marketPriceBaseCurrencyFromCurrency: number; |
|
|
|
|
|
let marketPriceBaseCurrencyToCurrency: number; |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
try { |
|
|
if (aFromCurrency === DEFAULT_CURRENCY) { |
|
|
let baseFromPrice: number | undefined = |
|
|
marketPriceBaseCurrencyFromCurrency = 1; |
|
|
aFromCurrency === DEFAULT_CURRENCY ? 1 : undefined; |
|
|
} else { |
|
|
let baseToPrice: number | undefined = |
|
|
marketPriceBaseCurrencyFromCurrency = ( |
|
|
aToCurrency === DEFAULT_CURRENCY ? 1 : undefined; |
|
|
await this.marketDataService.get({ |
|
|
|
|
|
dataSource, |
|
|
if (aFromCurrency !== DEFAULT_CURRENCY) { |
|
|
date: aDate, |
|
|
baseFromPrice = await this.getRateFromCache( |
|
|
symbol: `${DEFAULT_CURRENCY}${aFromCurrency}` |
|
|
`${DEFAULT_CURRENCY}${aFromCurrency}`, |
|
|
}) |
|
|
aDate |
|
|
)?.marketPrice; |
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
if (baseFromPrice === undefined) { |
|
|
|
|
|
const crossPrice = await this.getRateFromCache( |
|
|
|
|
|
`${aFromCurrency}${DEFAULT_CURRENCY}`, |
|
|
|
|
|
aDate |
|
|
|
|
|
); |
|
|
|
|
|
if (crossPrice !== undefined) { |
|
|
|
|
|
baseFromPrice = 1 / crossPrice; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
} catch {} |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
if (aToCurrency !== DEFAULT_CURRENCY) { |
|
|
if (aToCurrency === DEFAULT_CURRENCY) { |
|
|
baseToPrice = await this.getRateFromCache( |
|
|
marketPriceBaseCurrencyToCurrency = 1; |
|
|
`${DEFAULT_CURRENCY}${aToCurrency}`, |
|
|
} else { |
|
|
aDate |
|
|
marketPriceBaseCurrencyToCurrency = ( |
|
|
); |
|
|
await this.marketDataService.get({ |
|
|
|
|
|
dataSource, |
|
|
if (baseToPrice === undefined) { |
|
|
date: aDate, |
|
|
const crossPrice = await this.getRateFromCache( |
|
|
symbol: `${DEFAULT_CURRENCY}${aToCurrency}` |
|
|
`${aToCurrency}${DEFAULT_CURRENCY}`, |
|
|
}) |
|
|
aDate |
|
|
)?.marketPrice; |
|
|
); |
|
|
|
|
|
if (crossPrice !== undefined) { |
|
|
|
|
|
baseToPrice = 1 / crossPrice; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
} catch {} |
|
|
|
|
|
|
|
|
|
|
|
// Calculate the opposite direction
|
|
|
factor = (1 / baseFromPrice) * baseToPrice; |
|
|
factor = |
|
|
} catch {} |
|
|
(1 / marketPriceBaseCurrencyFromCurrency) * |
|
|
|
|
|
marketPriceBaseCurrencyToCurrency; |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -352,6 +356,98 @@ export class ExchangeRateDataService { |
|
|
return undefined; |
|
|
return undefined; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@OnEvent(MARKET_DATA_UPDATED) |
|
|
|
|
|
public onMarketDataUpdated(event: { symbol: string }) { |
|
|
|
|
|
this.exchangeRateCache.delete(event.symbol); |
|
|
|
|
|
this.pendingLoads.delete(event.symbol); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private getDaysSinceEpoch(aDate: Date) { |
|
|
|
|
|
return Math.floor(aDate.getTime() / 86400000) + 25569; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private async loadCache(aSymbol: string): Promise<Float64Array> { |
|
|
|
|
|
const dataSource = this.dataProviderService.getDataSourceForExchangeRates(); |
|
|
|
|
|
const marketData = await this.prismaService.marketData.findMany({ |
|
|
|
|
|
where: { dataSource, symbol: aSymbol }, |
|
|
|
|
|
orderBy: { date: 'asc' }, |
|
|
|
|
|
select: { date: true, marketPrice: true } |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const todayDays = this.getDaysSinceEpoch(new Date()); |
|
|
|
|
|
const maxDataDays = |
|
|
|
|
|
marketData.length > 0 |
|
|
|
|
|
? this.getDaysSinceEpoch(marketData[marketData.length - 1].date) |
|
|
|
|
|
: 0; |
|
|
|
|
|
|
|
|
|
|
|
const array = new Float64Array(Math.max(todayDays, maxDataDays) + 1); |
|
|
|
|
|
|
|
|
|
|
|
if (marketData.length > 0) { |
|
|
|
|
|
let lastRate = marketData[0].marketPrice; |
|
|
|
|
|
let currentIndex = this.getDaysSinceEpoch(marketData[0].date); |
|
|
|
|
|
|
|
|
|
|
|
for (const data of marketData) { |
|
|
|
|
|
const dataIndex = this.getDaysSinceEpoch(data.date); |
|
|
|
|
|
while (currentIndex < dataIndex && currentIndex <= todayDays) { |
|
|
|
|
|
array[currentIndex++] = lastRate; |
|
|
|
|
|
} |
|
|
|
|
|
lastRate = data.marketPrice; |
|
|
|
|
|
array[dataIndex] = lastRate; |
|
|
|
|
|
currentIndex = dataIndex + 1; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
while (currentIndex <= todayDays) { |
|
|
|
|
|
array[currentIndex++] = lastRate; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return array; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private async getRateFromCache( |
|
|
|
|
|
aSymbol: string, |
|
|
|
|
|
aDate: Date |
|
|
|
|
|
): Promise<number | undefined> { |
|
|
|
|
|
let cache = this.exchangeRateCache.get(aSymbol); |
|
|
|
|
|
|
|
|
|
|
|
while (!cache) { |
|
|
|
|
|
if (this.pendingLoads.has(aSymbol)) { |
|
|
|
|
|
await this.pendingLoads.get(aSymbol); |
|
|
|
|
|
cache = this.exchangeRateCache.get(aSymbol); |
|
|
|
|
|
} else { |
|
|
|
|
|
cache = await this.loadAndCommit(aSymbol); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const days = Math.min(this.getDaysSinceEpoch(aDate), cache.length - 1); |
|
|
|
|
|
|
|
|
|
|
|
if (days >= 0) { |
|
|
|
|
|
const rate = cache[days]; |
|
|
|
|
|
return rate === 0 ? undefined : rate; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return undefined; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private async loadAndCommit(aSymbol: string): Promise<Float64Array> { |
|
|
|
|
|
const loadPromise = this.loadCache(aSymbol); |
|
|
|
|
|
this.pendingLoads.set(aSymbol, loadPromise); |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
const cache = await loadPromise; |
|
|
|
|
|
|
|
|
|
|
|
if (this.pendingLoads.get(aSymbol) === loadPromise) { |
|
|
|
|
|
this.exchangeRateCache.set(aSymbol, cache); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return this.exchangeRateCache.get(aSymbol) ?? cache; |
|
|
|
|
|
} finally { |
|
|
|
|
|
if (this.pendingLoads.get(aSymbol) === loadPromise) { |
|
|
|
|
|
this.pendingLoads.delete(aSymbol); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
private async getExchangeRates({ |
|
|
private async getExchangeRates({ |
|
|
currencyFrom, |
|
|
currencyFrom, |
|
|
currencyTo, |
|
|
currencyTo, |
|
|
|