|
|
@ -28,12 +28,15 @@ import ms from 'ms'; |
|
|
|
|
|
|
|
|
import { ExchangeRatesByCurrency } from './interfaces/exchange-rate-data.interface'; |
|
|
import { ExchangeRatesByCurrency } from './interfaces/exchange-rate-data.interface'; |
|
|
|
|
|
|
|
|
|
|
|
const EPOCH_OFFSET_DAYS = 25569; |
|
|
|
|
|
|
|
|
@Injectable() |
|
|
@Injectable() |
|
|
export class ExchangeRateDataService { |
|
|
export class ExchangeRateDataService { |
|
|
private currencies: string[] = []; |
|
|
private currencies: string[] = []; |
|
|
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, Promise<Float64Array>>(); |
|
|
|
|
|
|
|
|
public constructor( |
|
|
public constructor( |
|
|
private readonly dataProviderService: DataProviderService, |
|
|
private readonly dataProviderService: DataProviderService, |
|
|
@ -284,56 +287,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 +355,77 @@ export class ExchangeRateDataService { |
|
|
return undefined; |
|
|
return undefined; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public invalidateCache(aSymbol: string) { |
|
|
|
|
|
this.exchangeRateCache.delete(aSymbol); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private getDaysSinceEpoch(aDate: Date) { |
|
|
|
|
|
return Math.floor(aDate.getTime() / ms('1d')) + EPOCH_OFFSET_DAYS; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
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 cachePromise = this.exchangeRateCache.get(aSymbol); |
|
|
|
|
|
|
|
|
|
|
|
if (!cachePromise) { |
|
|
|
|
|
cachePromise = this.loadCache(aSymbol).catch((error) => { |
|
|
|
|
|
this.exchangeRateCache.delete(aSymbol); |
|
|
|
|
|
throw error; |
|
|
|
|
|
}); |
|
|
|
|
|
this.exchangeRateCache.set(aSymbol, cachePromise); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const cache = await cachePromise; |
|
|
|
|
|
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 getExchangeRates({ |
|
|
private async getExchangeRates({ |
|
|
currencyFrom, |
|
|
currencyFrom, |
|
|
currencyTo, |
|
|
currencyTo, |
|
|
|