Feature/refactor exchange rate service (#289)

* Refactor exchange rate service

* Update changelog
pull/290/head
Thomas 3 years ago committed by GitHub
parent 3330ae70b6
commit b898c0678d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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/), 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). 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 ## 1.37.0 - 13.08.2021
### Added ### Added

@ -1,6 +1,7 @@
import { DataSource } from '@prisma/client'; import { Currency, DataSource } from '@prisma/client';
export interface LookupItem { export interface LookupItem {
currency: Currency;
dataSource: DataSource; dataSource: DataSource;
name: string; name: string;
symbol: string; symbol: string;

@ -39,6 +39,7 @@ export class SymbolService {
const ghostfolioSymbolProfiles = const ghostfolioSymbolProfiles =
await this.prismaService.symbolProfile.findMany({ await this.prismaService.symbolProfile.findMany({
select: { select: {
currency: true,
dataSource: true, dataSource: true,
name: true, name: true,
symbol: true symbol: true

@ -1,3 +1,4 @@
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { import {
DATE_FORMAT, DATE_FORMAT,
isGhostfolioScraperApiSymbol, isGhostfolioScraperApiSymbol,
@ -166,10 +167,19 @@ export class DataProviderService {
return result; return result;
} }
public async search(aSymbol: string) { public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
return this.getDataProvider( const { items } = await this.getDataProvider(
<DataSource>this.configurationService.get('DATA_SOURCES')[0] <DataSource>this.configurationService.get('DATA_SOURCES')[0]
).search(aSymbol); ).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) { private getDataProvider(providerName: DataSource) {

@ -1,3 +1,4 @@
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { import {
DATE_FORMAT, DATE_FORMAT,
getYesterday, getYesterday,
@ -143,7 +144,7 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
return []; return [];
} }
public async search(aSymbol: string) { public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
return { items: [] }; return { items: [] };
} }

@ -1,3 +1,4 @@
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { import {
DATE_FORMAT, DATE_FORMAT,
getToday, getToday,
@ -129,7 +130,7 @@ export class RakutenRapidApiService implements DataProviderInterface {
return {}; return {};
} }
public async search(aSymbol: string) { public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
return { items: [] }; return { items: [] };
} }

@ -137,7 +137,7 @@ export class YahooFinanceService implements DataProviderInterface {
} }
public async search(aSymbol: string): Promise<{ items: LookupItem[] }> { public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
let items = []; let items: LookupItem[] = [];
try { try {
const get = bent( const get = bent(
@ -180,15 +180,11 @@ export class YahooFinanceService implements DataProviderInterface {
return symbol.includes(Currency.USD); return symbol.includes(Currency.USD);
} }
if (!marketData[symbol]?.currency) {
// Only allow symbols with supported currency
return false;
}
return true; return true;
}) })
.map(({ longname, shortname, symbol }) => { .map(({ longname, shortname, symbol }) => {
return { return {
currency: marketData[symbol]?.currency,
dataSource: DataSource.YAHOO, dataSource: DataSource.YAHOO,
name: longname || shortname, name: longname || shortname,
symbol: convertFromYahooSymbol(symbol) symbol: convertFromYahooSymbol(symbol)

@ -1,3 +1,4 @@
import { currencyPairs } from '@ghostfolio/common/config';
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper'; import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Currency } from '@prisma/client'; import { Currency } from '@prisma/client';
@ -8,29 +9,27 @@ import { DataProviderService } from './data-provider.service';
@Injectable() @Injectable()
export class ExchangeRateDataService { export class ExchangeRateDataService {
private currencies = {}; private currencyPairs: string[] = [];
private pairs: string[] = []; private exchangeRates: { [currencyPair: string]: number } = {};
public constructor(private dataProviderService: DataProviderService) { public constructor(private dataProviderService: DataProviderService) {
this.initialize(); this.initialize();
} }
public async initialize() { public async initialize() {
this.pairs = []; this.currencyPairs = [];
this.exchangeRates = {};
this.addPairs(Currency.CHF, Currency.EUR); for (const { currency1, currency2 } of currencyPairs) {
this.addPairs(Currency.CHF, Currency.GBP); this.addCurrencyPairs(currency1, currency2);
this.addPairs(Currency.CHF, Currency.USD); }
this.addPairs(Currency.EUR, Currency.GBP);
this.addPairs(Currency.EUR, Currency.USD);
this.addPairs(Currency.GBP, Currency.USD);
await this.loadCurrencies(); await this.loadCurrencies();
} }
public async loadCurrencies() { public async loadCurrencies() {
const result = await this.dataProviderService.getHistorical( const result = await this.dataProviderService.getHistorical(
this.pairs, this.currencyPairs,
'day', 'day',
getYesterday(), getYesterday(),
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 [currency1, currency2] = pair.match(/.{1,3}/g);
const date = format(getYesterday(), DATE_FORMAT); 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 // Not found, calculate indirectly via USD
this.currencies[pair] = this.exchangeRates[pair] =
resultExtended[`${currency1}${Currency.USD}`]?.[date]?.marketPrice * resultExtended[`${currency1}${Currency.USD}`]?.[date]?.marketPrice *
resultExtended[`${Currency.USD}${currency2}`]?.[date]?.marketPrice; resultExtended[`${Currency.USD}${currency2}`]?.[date]?.marketPrice;
// Calculate the opposite direction // 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, aFromCurrency: Currency,
aToCurrency: 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 // Reinitialize if data is not loaded correctly
this.initialize(); this.initialize();
} }
@ -81,7 +81,17 @@ export class ExchangeRateDataService {
let factor = 1; let factor = 1;
if (aFromCurrency !== aToCurrency) { 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)) { if (isNumber(factor)) {
@ -95,8 +105,8 @@ export class ExchangeRateDataService {
return aValue; return aValue;
} }
private addPairs(aCurrency1: Currency, aCurrency2: Currency) { private addCurrencyPairs(aCurrency1: Currency, aCurrency2: Currency) {
this.pairs.push(`${aCurrency1}${aCurrency2}`); this.currencyPairs.push(`${aCurrency1}${aCurrency2}`);
this.pairs.push(`${aCurrency2}${aCurrency1}`); this.currencyPairs.push(`${aCurrency2}${aCurrency1}`);
} }
} }

@ -8,11 +8,23 @@ export const benchmarks: Partial<IDataGatheringItem>[] = [
{ dataSource: DataSource.YAHOO, symbol: 'VOO' } { dataSource: DataSource.YAHOO, symbol: 'VOO' }
]; ];
export const currencyPairs: Partial<IDataGatheringItem>[] = [ export const currencyPairs: Partial<
{ dataSource: DataSource.YAHOO, symbol: `${Currency.USD}${Currency.EUR}` }, IDataGatheringItem & {
{ dataSource: DataSource.YAHOO, symbol: `${Currency.USD}${Currency.GBP}` }, currency1: Currency;
{ dataSource: DataSource.YAHOO, symbol: `${Currency.USD}${Currency.CHF}` } 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 ghostfolioScraperApiSymbolPrefix = '_GF_';
export const ghostfolioCashSymbol = `${ghostfolioScraperApiSymbolPrefix}CASH`; export const ghostfolioCashSymbol = `${ghostfolioScraperApiSymbolPrefix}CASH`;

Loading…
Cancel
Save