Feature/refactor exchange rate service (#289)
* Refactor exchange rate service * Update changelog
This commit is contained in:
parent
3330ae70b6
commit
b898c0678d
@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Changed
|
||||
|
||||
- Refactored the exchange rate service
|
||||
|
||||
## 1.37.0 - 13.08.2021
|
||||
|
||||
### Added
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { DataSource } from '@prisma/client';
|
||||
import { Currency, DataSource } from '@prisma/client';
|
||||
|
||||
export interface LookupItem {
|
||||
currency: Currency;
|
||||
dataSource: DataSource;
|
||||
name: string;
|
||||
symbol: string;
|
||||
|
@ -39,6 +39,7 @@ export class SymbolService {
|
||||
const ghostfolioSymbolProfiles =
|
||||
await this.prismaService.symbolProfile.findMany({
|
||||
select: {
|
||||
currency: true,
|
||||
dataSource: true,
|
||||
name: true,
|
||||
symbol: true
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||
import {
|
||||
DATE_FORMAT,
|
||||
isGhostfolioScraperApiSymbol,
|
||||
@ -166,10 +167,19 @@ export class DataProviderService {
|
||||
return result;
|
||||
}
|
||||
|
||||
public async search(aSymbol: string) {
|
||||
return this.getDataProvider(
|
||||
public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
|
||||
const { items } = await this.getDataProvider(
|
||||
<DataSource>this.configurationService.get('DATA_SOURCES')[0]
|
||||
).search(aSymbol);
|
||||
|
||||
const filteredItems = items.filter((item) => {
|
||||
// Only allow symbols with supported currency
|
||||
return item.currency ? true : false;
|
||||
});
|
||||
|
||||
return {
|
||||
items: filteredItems
|
||||
};
|
||||
}
|
||||
|
||||
private getDataProvider(providerName: DataSource) {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||
import {
|
||||
DATE_FORMAT,
|
||||
getYesterday,
|
||||
@ -143,7 +144,7 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
||||
return [];
|
||||
}
|
||||
|
||||
public async search(aSymbol: string) {
|
||||
public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
|
||||
return { items: [] };
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||
import {
|
||||
DATE_FORMAT,
|
||||
getToday,
|
||||
@ -129,7 +130,7 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
||||
return {};
|
||||
}
|
||||
|
||||
public async search(aSymbol: string) {
|
||||
public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
|
||||
return { items: [] };
|
||||
}
|
||||
|
||||
|
@ -137,7 +137,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
}
|
||||
|
||||
public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
|
||||
let items = [];
|
||||
let items: LookupItem[] = [];
|
||||
|
||||
try {
|
||||
const get = bent(
|
||||
@ -180,15 +180,11 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
return symbol.includes(Currency.USD);
|
||||
}
|
||||
|
||||
if (!marketData[symbol]?.currency) {
|
||||
// Only allow symbols with supported currency
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.map(({ longname, shortname, symbol }) => {
|
||||
return {
|
||||
currency: marketData[symbol]?.currency,
|
||||
dataSource: DataSource.YAHOO,
|
||||
name: longname || shortname,
|
||||
symbol: convertFromYahooSymbol(symbol)
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { currencyPairs } from '@ghostfolio/common/config';
|
||||
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Currency } from '@prisma/client';
|
||||
@ -8,29 +9,27 @@ import { DataProviderService } from './data-provider.service';
|
||||
|
||||
@Injectable()
|
||||
export class ExchangeRateDataService {
|
||||
private currencies = {};
|
||||
private pairs: string[] = [];
|
||||
private currencyPairs: string[] = [];
|
||||
private exchangeRates: { [currencyPair: string]: number } = {};
|
||||
|
||||
public constructor(private dataProviderService: DataProviderService) {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
public async initialize() {
|
||||
this.pairs = [];
|
||||
this.currencyPairs = [];
|
||||
this.exchangeRates = {};
|
||||
|
||||
this.addPairs(Currency.CHF, Currency.EUR);
|
||||
this.addPairs(Currency.CHF, Currency.GBP);
|
||||
this.addPairs(Currency.CHF, Currency.USD);
|
||||
this.addPairs(Currency.EUR, Currency.GBP);
|
||||
this.addPairs(Currency.EUR, Currency.USD);
|
||||
this.addPairs(Currency.GBP, Currency.USD);
|
||||
for (const { currency1, currency2 } of currencyPairs) {
|
||||
this.addCurrencyPairs(currency1, currency2);
|
||||
}
|
||||
|
||||
await this.loadCurrencies();
|
||||
}
|
||||
|
||||
public async loadCurrencies() {
|
||||
const result = await this.dataProviderService.getHistorical(
|
||||
this.pairs,
|
||||
this.currencyPairs,
|
||||
'day',
|
||||
getYesterday(),
|
||||
getYesterday()
|
||||
@ -50,20 +49,21 @@ export class ExchangeRateDataService {
|
||||
};
|
||||
});
|
||||
|
||||
this.pairs.forEach((pair) => {
|
||||
this.currencyPairs.forEach((pair) => {
|
||||
const [currency1, currency2] = pair.match(/.{1,3}/g);
|
||||
const date = format(getYesterday(), DATE_FORMAT);
|
||||
|
||||
this.currencies[pair] = resultExtended[pair]?.[date]?.marketPrice;
|
||||
this.exchangeRates[pair] = resultExtended[pair]?.[date]?.marketPrice;
|
||||
|
||||
if (!this.currencies[pair]) {
|
||||
if (!this.exchangeRates[pair]) {
|
||||
// Not found, calculate indirectly via USD
|
||||
this.currencies[pair] =
|
||||
this.exchangeRates[pair] =
|
||||
resultExtended[`${currency1}${Currency.USD}`]?.[date]?.marketPrice *
|
||||
resultExtended[`${Currency.USD}${currency2}`]?.[date]?.marketPrice;
|
||||
|
||||
// Calculate the opposite direction
|
||||
this.currencies[`${currency2}${currency1}`] = 1 / this.currencies[pair];
|
||||
this.exchangeRates[`${currency2}${currency1}`] =
|
||||
1 / this.exchangeRates[pair];
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -73,7 +73,7 @@ export class ExchangeRateDataService {
|
||||
aFromCurrency: Currency,
|
||||
aToCurrency: Currency
|
||||
) {
|
||||
if (isNaN(this.currencies[`${Currency.USD}${Currency.CHF}`])) {
|
||||
if (isNaN(this.exchangeRates[`${Currency.USD}${Currency.CHF}`])) {
|
||||
// Reinitialize if data is not loaded correctly
|
||||
this.initialize();
|
||||
}
|
||||
@ -81,7 +81,17 @@ export class ExchangeRateDataService {
|
||||
let factor = 1;
|
||||
|
||||
if (aFromCurrency !== aToCurrency) {
|
||||
factor = this.currencies[`${aFromCurrency}${aToCurrency}`];
|
||||
if (this.exchangeRates[`${aFromCurrency}${aToCurrency}`]) {
|
||||
factor = this.exchangeRates[`${aFromCurrency}${aToCurrency}`];
|
||||
} else {
|
||||
// Calculate indirectly via USD
|
||||
const factor1 = this.exchangeRates[`${aFromCurrency}${Currency.USD}`];
|
||||
const factor2 = this.exchangeRates[`${Currency.USD}${aToCurrency}`];
|
||||
|
||||
factor = factor1 * factor2;
|
||||
|
||||
this.exchangeRates[`${aFromCurrency}${aToCurrency}`] = factor;
|
||||
}
|
||||
}
|
||||
|
||||
if (isNumber(factor)) {
|
||||
@ -95,8 +105,8 @@ export class ExchangeRateDataService {
|
||||
return aValue;
|
||||
}
|
||||
|
||||
private addPairs(aCurrency1: Currency, aCurrency2: Currency) {
|
||||
this.pairs.push(`${aCurrency1}${aCurrency2}`);
|
||||
this.pairs.push(`${aCurrency2}${aCurrency1}`);
|
||||
private addCurrencyPairs(aCurrency1: Currency, aCurrency2: Currency) {
|
||||
this.currencyPairs.push(`${aCurrency1}${aCurrency2}`);
|
||||
this.currencyPairs.push(`${aCurrency2}${aCurrency1}`);
|
||||
}
|
||||
}
|
||||
|
@ -8,11 +8,23 @@ export const benchmarks: Partial<IDataGatheringItem>[] = [
|
||||
{ dataSource: DataSource.YAHOO, symbol: 'VOO' }
|
||||
];
|
||||
|
||||
export const currencyPairs: Partial<IDataGatheringItem>[] = [
|
||||
{ dataSource: DataSource.YAHOO, symbol: `${Currency.USD}${Currency.EUR}` },
|
||||
{ dataSource: DataSource.YAHOO, symbol: `${Currency.USD}${Currency.GBP}` },
|
||||
{ dataSource: DataSource.YAHOO, symbol: `${Currency.USD}${Currency.CHF}` }
|
||||
];
|
||||
export const currencyPairs: Partial<
|
||||
IDataGatheringItem & {
|
||||
currency1: Currency;
|
||||
currency2: Currency;
|
||||
}
|
||||
>[] = Object.keys(Currency)
|
||||
.filter((currency) => {
|
||||
return currency !== Currency.USD;
|
||||
})
|
||||
.map((currency) => {
|
||||
return {
|
||||
currency1: Currency.USD,
|
||||
currency2: Currency[currency],
|
||||
dataSource: DataSource.YAHOO,
|
||||
symbol: `${Currency.USD}${Currency[currency]}`
|
||||
};
|
||||
});
|
||||
|
||||
export const ghostfolioScraperApiSymbolPrefix = '_GF_';
|
||||
export const ghostfolioCashSymbol = `${ghostfolioScraperApiSymbolPrefix}CASH`;
|
||||
|
Loading…
x
Reference in New Issue
Block a user