|
|
|
@ -30,7 +30,7 @@ import ms from 'ms'; |
|
|
|
export class ExchangeRateDataService { |
|
|
|
private currencies: string[] = []; |
|
|
|
private currencyPairs: DataGatheringItem[] = []; |
|
|
|
private derivedCurrencyFactors = new Map<string, number>(); |
|
|
|
private derivedCurrencyFactors: { [currencyPair: string]: number } = {}; |
|
|
|
private exchangeRates: { [currencyPair: string]: number } = {}; |
|
|
|
|
|
|
|
public constructor( |
|
|
|
@ -136,13 +136,12 @@ export class ExchangeRateDataService { |
|
|
|
public async initialize() { |
|
|
|
this.currencies = await this.prepareCurrencies(); |
|
|
|
this.currencyPairs = []; |
|
|
|
this.derivedCurrencyFactors = {}; |
|
|
|
this.exchangeRates = {}; |
|
|
|
this.derivedCurrencyFactors = new Map(); |
|
|
|
|
|
|
|
// Initialize derived currencies factor map in both directions
|
|
|
|
for (const { currency, factor, rootCurrency } of DERIVED_CURRENCIES) { |
|
|
|
this.derivedCurrencyFactors.set(`${currency}${rootCurrency}`, 1 / factor); |
|
|
|
this.derivedCurrencyFactors.set(`${rootCurrency}${currency}`, factor); |
|
|
|
this.derivedCurrencyFactors[`${currency}${rootCurrency}`] = 1 / factor; |
|
|
|
this.derivedCurrencyFactors[`${rootCurrency}${currency}`] = factor; |
|
|
|
} |
|
|
|
|
|
|
|
for (const { |
|
|
|
@ -274,70 +273,65 @@ export class ExchangeRateDataService { |
|
|
|
return this.toCurrency(aValue, aFromCurrency, aToCurrency); |
|
|
|
} |
|
|
|
|
|
|
|
const derivedCurrencyFactor = |
|
|
|
this.derivedCurrencyFactors[`${aFromCurrency}${aToCurrency}`]; |
|
|
|
let factor: number; |
|
|
|
|
|
|
|
if (aFromCurrency === aToCurrency) { |
|
|
|
factor = 1; |
|
|
|
} else if (derivedCurrencyFactor) { |
|
|
|
factor = derivedCurrencyFactor; |
|
|
|
} else { |
|
|
|
const derivedCurrencyFactor = this.getDerivedCurrencyFactor( |
|
|
|
aFromCurrency, |
|
|
|
aToCurrency |
|
|
|
); |
|
|
|
const dataSource = |
|
|
|
this.dataProviderService.getDataSourceForExchangeRates(); |
|
|
|
const symbol = `${aFromCurrency}${aToCurrency}`; |
|
|
|
|
|
|
|
if (derivedCurrencyFactor !== null) { |
|
|
|
factor = derivedCurrencyFactor; |
|
|
|
const marketData = await this.marketDataService.get({ |
|
|
|
dataSource, |
|
|
|
symbol, |
|
|
|
date: aDate |
|
|
|
}); |
|
|
|
|
|
|
|
if (marketData?.marketPrice) { |
|
|
|
factor = marketData?.marketPrice; |
|
|
|
} else { |
|
|
|
const dataSource = |
|
|
|
this.dataProviderService.getDataSourceForExchangeRates(); |
|
|
|
const symbol = `${aFromCurrency}${aToCurrency}`; |
|
|
|
// Calculate indirectly via base currency
|
|
|
|
|
|
|
|
const marketData = await this.marketDataService.get({ |
|
|
|
dataSource, |
|
|
|
symbol, |
|
|
|
date: aDate |
|
|
|
}); |
|
|
|
let marketPriceBaseCurrencyFromCurrency: number; |
|
|
|
let marketPriceBaseCurrencyToCurrency: number; |
|
|
|
|
|
|
|
if (marketData?.marketPrice) { |
|
|
|
factor = marketData?.marketPrice; |
|
|
|
} else { |
|
|
|
// Calculate indirectly via base currency
|
|
|
|
|
|
|
|
let marketPriceBaseCurrencyFromCurrency: number; |
|
|
|
let marketPriceBaseCurrencyToCurrency: number; |
|
|
|
|
|
|
|
try { |
|
|
|
if (aFromCurrency === DEFAULT_CURRENCY) { |
|
|
|
marketPriceBaseCurrencyFromCurrency = 1; |
|
|
|
} else { |
|
|
|
marketPriceBaseCurrencyFromCurrency = ( |
|
|
|
await this.marketDataService.get({ |
|
|
|
dataSource, |
|
|
|
date: aDate, |
|
|
|
symbol: `${DEFAULT_CURRENCY}${aFromCurrency}` |
|
|
|
}) |
|
|
|
)?.marketPrice; |
|
|
|
} |
|
|
|
} catch {} |
|
|
|
|
|
|
|
try { |
|
|
|
if (aToCurrency === DEFAULT_CURRENCY) { |
|
|
|
marketPriceBaseCurrencyToCurrency = 1; |
|
|
|
} else { |
|
|
|
marketPriceBaseCurrencyToCurrency = ( |
|
|
|
await this.marketDataService.get({ |
|
|
|
dataSource, |
|
|
|
date: aDate, |
|
|
|
symbol: `${DEFAULT_CURRENCY}${aToCurrency}` |
|
|
|
}) |
|
|
|
)?.marketPrice; |
|
|
|
} |
|
|
|
} catch {} |
|
|
|
try { |
|
|
|
if (aFromCurrency === DEFAULT_CURRENCY) { |
|
|
|
marketPriceBaseCurrencyFromCurrency = 1; |
|
|
|
} else { |
|
|
|
marketPriceBaseCurrencyFromCurrency = ( |
|
|
|
await this.marketDataService.get({ |
|
|
|
dataSource, |
|
|
|
date: aDate, |
|
|
|
symbol: `${DEFAULT_CURRENCY}${aFromCurrency}` |
|
|
|
}) |
|
|
|
)?.marketPrice; |
|
|
|
} |
|
|
|
} catch {} |
|
|
|
|
|
|
|
// Calculate the opposite direction
|
|
|
|
factor = |
|
|
|
(1 / marketPriceBaseCurrencyFromCurrency) * |
|
|
|
marketPriceBaseCurrencyToCurrency; |
|
|
|
} |
|
|
|
try { |
|
|
|
if (aToCurrency === DEFAULT_CURRENCY) { |
|
|
|
marketPriceBaseCurrencyToCurrency = 1; |
|
|
|
} else { |
|
|
|
marketPriceBaseCurrencyToCurrency = ( |
|
|
|
await this.marketDataService.get({ |
|
|
|
dataSource, |
|
|
|
date: aDate, |
|
|
|
symbol: `${DEFAULT_CURRENCY}${aToCurrency}` |
|
|
|
}) |
|
|
|
)?.marketPrice; |
|
|
|
} |
|
|
|
} catch {} |
|
|
|
|
|
|
|
// Calculate the opposite direction
|
|
|
|
factor = |
|
|
|
(1 / marketPriceBaseCurrencyFromCurrency) * |
|
|
|
marketPriceBaseCurrencyToCurrency; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -374,129 +368,120 @@ export class ExchangeRateDataService { |
|
|
|
for (const date of dates) { |
|
|
|
factors[format(date, DATE_FORMAT)] = 1; |
|
|
|
} |
|
|
|
|
|
|
|
return factors; |
|
|
|
} |
|
|
|
|
|
|
|
// Check if this is a conversion between a derived currency and its root currency
|
|
|
|
const derivedCurrencyFactor = this.getDerivedCurrencyFactor( |
|
|
|
currencyFrom, |
|
|
|
currencyTo |
|
|
|
); |
|
|
|
const derivedCurrencyFactor = |
|
|
|
this.derivedCurrencyFactors[`${currencyFrom}${currencyTo}`]; |
|
|
|
|
|
|
|
if (derivedCurrencyFactor !== null) { |
|
|
|
// Use the fixed mathematical factor for derived currencies
|
|
|
|
if (derivedCurrencyFactor) { |
|
|
|
for (const date of dates) { |
|
|
|
factors[format(date, DATE_FORMAT)] = derivedCurrencyFactor; |
|
|
|
} |
|
|
|
|
|
|
|
return factors; |
|
|
|
} |
|
|
|
|
|
|
|
// Continue with standard exchange rate logic
|
|
|
|
{ |
|
|
|
const dataSource = |
|
|
|
this.dataProviderService.getDataSourceForExchangeRates(); |
|
|
|
const symbol = `${currencyFrom}${currencyTo}`; |
|
|
|
|
|
|
|
const marketData = await this.marketDataService.getRange({ |
|
|
|
assetProfileIdentifiers: [ |
|
|
|
{ |
|
|
|
dataSource, |
|
|
|
symbol |
|
|
|
} |
|
|
|
], |
|
|
|
dateQuery: { gte: startDate, lt: endDate } |
|
|
|
}); |
|
|
|
const dataSource = this.dataProviderService.getDataSourceForExchangeRates(); |
|
|
|
const symbol = `${currencyFrom}${currencyTo}`; |
|
|
|
|
|
|
|
if (marketData?.length > 0) { |
|
|
|
for (const { date, marketPrice } of marketData) { |
|
|
|
factors[format(date, DATE_FORMAT)] = marketPrice; |
|
|
|
const marketData = await this.marketDataService.getRange({ |
|
|
|
assetProfileIdentifiers: [ |
|
|
|
{ |
|
|
|
dataSource, |
|
|
|
symbol |
|
|
|
} |
|
|
|
} else { |
|
|
|
// Calculate indirectly via base currency
|
|
|
|
], |
|
|
|
dateQuery: { gte: startDate, lt: endDate } |
|
|
|
}); |
|
|
|
|
|
|
|
const marketPriceBaseCurrencyFromCurrency: { |
|
|
|
[dateString: string]: number; |
|
|
|
} = {}; |
|
|
|
const marketPriceBaseCurrencyToCurrency: { |
|
|
|
[dateString: string]: number; |
|
|
|
} = {}; |
|
|
|
if (marketData?.length > 0) { |
|
|
|
for (const { date, marketPrice } of marketData) { |
|
|
|
factors[format(date, DATE_FORMAT)] = marketPrice; |
|
|
|
} |
|
|
|
} else { |
|
|
|
// Calculate indirectly via base currency
|
|
|
|
|
|
|
|
const marketPriceBaseCurrencyFromCurrency: { |
|
|
|
[dateString: string]: number; |
|
|
|
} = {}; |
|
|
|
const marketPriceBaseCurrencyToCurrency: { |
|
|
|
[dateString: string]: number; |
|
|
|
} = {}; |
|
|
|
|
|
|
|
try { |
|
|
|
if (currencyFrom === DEFAULT_CURRENCY) { |
|
|
|
for (const date of dates) { |
|
|
|
marketPriceBaseCurrencyFromCurrency[format(date, DATE_FORMAT)] = 1; |
|
|
|
} |
|
|
|
} else { |
|
|
|
const marketData = await this.marketDataService.getRange({ |
|
|
|
assetProfileIdentifiers: [ |
|
|
|
{ |
|
|
|
dataSource, |
|
|
|
symbol: `${DEFAULT_CURRENCY}${currencyFrom}` |
|
|
|
} |
|
|
|
], |
|
|
|
dateQuery: { gte: startDate, lt: endDate } |
|
|
|
}); |
|
|
|
|
|
|
|
try { |
|
|
|
if (currencyFrom === DEFAULT_CURRENCY) { |
|
|
|
for (const date of dates) { |
|
|
|
marketPriceBaseCurrencyFromCurrency[format(date, DATE_FORMAT)] = |
|
|
|
1; |
|
|
|
} |
|
|
|
} else { |
|
|
|
const marketData = await this.marketDataService.getRange({ |
|
|
|
assetProfileIdentifiers: [ |
|
|
|
{ |
|
|
|
dataSource, |
|
|
|
symbol: `${DEFAULT_CURRENCY}${currencyFrom}` |
|
|
|
} |
|
|
|
], |
|
|
|
dateQuery: { gte: startDate, lt: endDate } |
|
|
|
}); |
|
|
|
|
|
|
|
for (const { date, marketPrice } of marketData) { |
|
|
|
marketPriceBaseCurrencyFromCurrency[format(date, DATE_FORMAT)] = |
|
|
|
marketPrice; |
|
|
|
} |
|
|
|
for (const { date, marketPrice } of marketData) { |
|
|
|
marketPriceBaseCurrencyFromCurrency[format(date, DATE_FORMAT)] = |
|
|
|
marketPrice; |
|
|
|
} |
|
|
|
} catch {} |
|
|
|
} |
|
|
|
} catch {} |
|
|
|
|
|
|
|
try { |
|
|
|
if (currencyTo === DEFAULT_CURRENCY) { |
|
|
|
for (const date of dates) { |
|
|
|
marketPriceBaseCurrencyToCurrency[format(date, DATE_FORMAT)] = 1; |
|
|
|
} |
|
|
|
} else { |
|
|
|
const marketData = await this.marketDataService.getRange({ |
|
|
|
assetProfileIdentifiers: [ |
|
|
|
{ |
|
|
|
dataSource, |
|
|
|
symbol: `${DEFAULT_CURRENCY}${currencyTo}` |
|
|
|
} |
|
|
|
], |
|
|
|
dateQuery: { |
|
|
|
gte: startDate, |
|
|
|
lt: endDate |
|
|
|
try { |
|
|
|
if (currencyTo === DEFAULT_CURRENCY) { |
|
|
|
for (const date of dates) { |
|
|
|
marketPriceBaseCurrencyToCurrency[format(date, DATE_FORMAT)] = 1; |
|
|
|
} |
|
|
|
} else { |
|
|
|
const marketData = await this.marketDataService.getRange({ |
|
|
|
assetProfileIdentifiers: [ |
|
|
|
{ |
|
|
|
dataSource, |
|
|
|
symbol: `${DEFAULT_CURRENCY}${currencyTo}` |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
for (const { date, marketPrice } of marketData) { |
|
|
|
marketPriceBaseCurrencyToCurrency[format(date, DATE_FORMAT)] = |
|
|
|
marketPrice; |
|
|
|
], |
|
|
|
dateQuery: { |
|
|
|
gte: startDate, |
|
|
|
lt: endDate |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
for (const { date, marketPrice } of marketData) { |
|
|
|
marketPriceBaseCurrencyToCurrency[format(date, DATE_FORMAT)] = |
|
|
|
marketPrice; |
|
|
|
} |
|
|
|
} catch {} |
|
|
|
} |
|
|
|
} catch {} |
|
|
|
|
|
|
|
for (const date of dates) { |
|
|
|
try { |
|
|
|
const factor = |
|
|
|
(1 / |
|
|
|
marketPriceBaseCurrencyFromCurrency[ |
|
|
|
format(date, DATE_FORMAT) |
|
|
|
]) * |
|
|
|
marketPriceBaseCurrencyToCurrency[format(date, DATE_FORMAT)]; |
|
|
|
|
|
|
|
if (isNaN(factor)) { |
|
|
|
throw new Error('Exchange rate is not a number'); |
|
|
|
} else { |
|
|
|
factors[format(date, DATE_FORMAT)] = factor; |
|
|
|
} |
|
|
|
} catch { |
|
|
|
let errorMessage = `No exchange rate has been found for ${currencyFrom}${currencyTo} at ${format( |
|
|
|
date, |
|
|
|
DATE_FORMAT |
|
|
|
)}. Please complement market data for ${DEFAULT_CURRENCY}${currencyFrom}`;
|
|
|
|
|
|
|
|
if (DEFAULT_CURRENCY !== currencyTo) { |
|
|
|
errorMessage = `${errorMessage} and ${DEFAULT_CURRENCY}${currencyTo}`; |
|
|
|
} |
|
|
|
for (const date of dates) { |
|
|
|
try { |
|
|
|
const factor = |
|
|
|
(1 / |
|
|
|
marketPriceBaseCurrencyFromCurrency[format(date, DATE_FORMAT)]) * |
|
|
|
marketPriceBaseCurrencyToCurrency[format(date, DATE_FORMAT)]; |
|
|
|
|
|
|
|
Logger.error(`${errorMessage}.`, 'ExchangeRateDataService'); |
|
|
|
if (isNaN(factor)) { |
|
|
|
throw new Error('Exchange rate is not a number'); |
|
|
|
} else { |
|
|
|
factors[format(date, DATE_FORMAT)] = factor; |
|
|
|
} |
|
|
|
} catch { |
|
|
|
let errorMessage = `No exchange rate has been found for ${currencyFrom}${currencyTo} at ${format( |
|
|
|
date, |
|
|
|
DATE_FORMAT |
|
|
|
)}. Please complement market data for ${DEFAULT_CURRENCY}${currencyFrom}`;
|
|
|
|
|
|
|
|
if (DEFAULT_CURRENCY !== currencyTo) { |
|
|
|
errorMessage = `${errorMessage} and ${DEFAULT_CURRENCY}${currencyTo}`; |
|
|
|
} |
|
|
|
|
|
|
|
Logger.error(`${errorMessage}.`, 'ExchangeRateDataService'); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
@ -504,16 +489,6 @@ export class ExchangeRateDataService { |
|
|
|
return factors; |
|
|
|
} |
|
|
|
|
|
|
|
private getDerivedCurrencyFactor( |
|
|
|
currencyFrom: string, |
|
|
|
currencyTo: string |
|
|
|
): number | null { |
|
|
|
const factor = this.derivedCurrencyFactors.get( |
|
|
|
`${currencyFrom}${currencyTo}` |
|
|
|
); |
|
|
|
return factor ?? null; |
|
|
|
} |
|
|
|
|
|
|
|
private async prepareCurrencies(): Promise<string[]> { |
|
|
|
let currencies: string[] = [DEFAULT_CURRENCY]; |
|
|
|
|
|
|
|
|